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, Severity};
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/// Scope used by code-size inspection.
504#[derive(
505    Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
506)]
507#[serde(rename_all = "kebab-case")]
508pub enum CodeSizeScope {
509    /// Inspect all supported source files in the repository.
510    #[default]
511    All,
512    /// Inspect supported changed source files.
513    Changed,
514    /// Inspect supported source files in affected projects.
515    Affected,
516}
517
518/// Language supported by code-size inspection.
519#[derive(
520    Clone, Copy, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
521)]
522#[serde(rename_all = "kebab-case")]
523pub enum CodeLanguage {
524    /// Rust source.
525    Rust,
526    /// TypeScript or TSX source.
527    #[serde(rename = "typescript")]
528    TypeScript,
529    /// Python source.
530    Python,
531}
532
533/// Code-size inspection rule.
534#[derive(
535    Clone, Copy, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
536)]
537#[serde(rename_all = "kebab-case")]
538pub enum CodeSizeRuleKind {
539    /// Effective file line count rule.
540    File,
541    /// Function span line count rule.
542    Function,
543    /// Nested executable block span line count rule.
544    Block,
545}
546
547/// Process exit behavior for inspection commands.
548#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
549#[serde(rename_all = "kebab-case")]
550pub enum InspectionFailOn {
551    /// Never fail the process because of findings.
552    #[default]
553    Never,
554    /// Fail when error findings exist.
555    Error,
556    /// Fail when warning or error findings exist.
557    Warning,
558}
559
560/// Generated-code inspection mode.
561#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
562#[serde(rename_all = "kebab-case")]
563pub enum GeneratedCodeInspectionMode {
564    /// Skip generated source files.
565    #[default]
566    Skip,
567    /// Inspect generated source files.
568    Inspect,
569}
570
571/// Request for code-size inspection.
572#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
573#[serde(rename_all = "camelCase")]
574pub struct CodeSizeInspectionRequest {
575    /// Optional starting path or explicit repo root.
576    pub repo: Option<PathBuf>,
577    /// Inspection scope.
578    pub scope: CodeSizeScope,
579    /// Optional base git ref.
580    pub base: Option<String>,
581    /// Optional head git ref.
582    pub head: Option<String>,
583    /// Explicit changed source files.
584    pub changed_files: Vec<RepoRelativePath>,
585    /// Include transitively affected projects for affected scope.
586    pub include_transitive: bool,
587    /// Optional language filter.
588    pub languages: Vec<CodeLanguage>,
589    /// Optional rule filter.
590    pub rules: Vec<CodeSizeRuleKind>,
591    /// Process exit behavior.
592    pub fail_on: InspectionFailOn,
593}
594
595/// Code-size inspection report.
596#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
597#[serde(rename_all = "camelCase")]
598pub struct CodeSizeInspectionReport {
599    /// Inspection scope.
600    pub scope: CodeSizeScope,
601    /// Optional base git ref.
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub base: Option<String>,
604    /// Optional head git ref.
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub head: Option<String>,
607    /// Summary counts and duration.
608    pub summary: CodeSizeInspectionSummary,
609    /// Stable configuration explanation.
610    pub config: CodeSizeResolvedConfigSummary,
611    /// Oversize findings.
612    pub findings: Vec<CodeSizeFinding>,
613    /// Operational diagnostics.
614    pub diagnostics: Vec<Diagnostic>,
615    /// Bounded skipped-file reason counts.
616    pub skipped: Vec<CodeSizeSkippedReason>,
617}
618
619/// Summary of one code-size inspection run.
620#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
621#[serde(rename_all = "camelCase")]
622pub struct CodeSizeInspectionSummary {
623    /// Number of files selected before source-level filters.
624    pub files_considered: u64,
625    /// Number of files scanned.
626    pub files_scanned: u64,
627    /// Number of selected files skipped.
628    pub files_skipped: u64,
629    /// Number of selected files that produced read or parse errors.
630    pub files_errored: u64,
631    /// Total finding count.
632    pub finding_count: u64,
633    /// Number of files with at least one finding.
634    pub files_with_findings: u64,
635    /// Inspection duration in milliseconds.
636    pub duration_millis: u64,
637}
638
639/// Summary of resolved configuration values.
640#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
641#[serde(rename_all = "camelCase")]
642pub struct CodeSizeResolvedConfigSummary {
643    /// Whether inspection is enabled.
644    pub enabled: bool,
645    /// Generated-code handling mode.
646    pub generated_code: GeneratedCodeInspectionMode,
647    /// Maximum selected files.
648    pub max_files: NonZeroUsize,
649    /// Maximum bytes read from one file.
650    pub max_file_bytes: NonZeroUsize,
651    /// Rule defaults after repository-level config.
652    pub rules: CodeSizeRuleConfigSet,
653}
654
655impl Default for CodeSizeResolvedConfigSummary {
656    fn default() -> Self {
657        let config = CodeSizeConfig::default();
658        Self {
659            enabled: config.enabled,
660            generated_code: config.generated_code,
661            max_files: config.max_files,
662            max_file_bytes: config.max_file_bytes,
663            rules: config.rules,
664        }
665    }
666}
667
668/// Code-size finding.
669#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
670#[serde(rename_all = "camelCase")]
671pub struct CodeSizeFinding {
672    /// Triggered rule.
673    pub rule: CodeSizeRuleKind,
674    /// Configured severity.
675    pub severity: Severity,
676    /// Repo-relative source path.
677    pub path: RepoRelativePath,
678    /// Owning project when known.
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub project: Option<ProjectName>,
681    /// Source language.
682    pub language: CodeLanguage,
683    /// Symbol name when available.
684    #[serde(skip_serializing_if = "Option::is_none")]
685    pub symbol: Option<String>,
686    /// Syntax node kind when available.
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub node_kind: Option<String>,
689    /// One-based start line.
690    pub start_line: NonZeroU32,
691    /// One-based end line.
692    pub end_line: NonZeroU32,
693    /// Measured line count.
694    pub measured_lines: NonZeroU32,
695    /// Physical file line count for file-size findings.
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub physical_lines: Option<NonZeroU32>,
698    /// Configured rule limit.
699    pub limit: NonZeroU32,
700    /// Human-readable explanation.
701    pub message: String,
702}
703
704/// Bounded count for skipped-file reasons.
705#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
706#[serde(rename_all = "camelCase")]
707pub struct CodeSizeSkippedReason {
708    /// Stable reason code.
709    pub reason: String,
710    /// Number of skipped files with this reason.
711    pub count: u64,
712}
713
714/// Repository-level inspection configuration.
715#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
716#[serde(rename_all = "camelCase")]
717pub struct RepoInspectionConfig {
718    /// Code-size inspection configuration.
719    pub code_size: CodeSizeConfig,
720}
721
722/// Code-size inspection configuration.
723#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
724#[serde(rename_all = "camelCase")]
725pub struct CodeSizeConfig {
726    /// Enables code-size inspection.
727    pub enabled: bool,
728    /// Generated-code handling mode.
729    pub generated_code: GeneratedCodeInspectionMode,
730    /// Maximum files selected for one scan.
731    pub max_files: NonZeroUsize,
732    /// Maximum bytes read from one file.
733    pub max_file_bytes: NonZeroUsize,
734    /// Rule configuration.
735    pub rules: CodeSizeRuleConfigSet,
736    /// Per-language enablement.
737    pub languages: BTreeMap<CodeLanguage, CodeLanguageConfig>,
738    /// Repo-relative exclusion globs.
739    pub excludes: Vec<RepoGlob>,
740    /// Path overrides in manifest order.
741    pub overrides: Vec<CodeSizeOverride>,
742}
743
744impl Default for CodeSizeConfig {
745    fn default() -> Self {
746        Self {
747            enabled: true,
748            generated_code: GeneratedCodeInspectionMode::Skip,
749            max_files: nonzero_usize(50_000),
750            max_file_bytes: nonzero_usize(2_000_000),
751            rules: CodeSizeRuleConfigSet::default(),
752            languages: default_code_size_languages(),
753            excludes: default_code_size_excludes(),
754            overrides: Vec::new(),
755        }
756    }
757}
758
759/// Per-language code-size settings.
760#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
761#[serde(rename_all = "camelCase")]
762pub struct CodeLanguageConfig {
763    /// Enables inspection for this language.
764    pub enabled: bool,
765}
766
767impl Default for CodeLanguageConfig {
768    fn default() -> Self {
769        Self { enabled: true }
770    }
771}
772
773/// Code-size rule configuration set.
774#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
775#[serde(rename_all = "camelCase")]
776pub struct CodeSizeRuleConfigSet {
777    /// File-size rule configuration.
778    pub file: CodeSizeRuleConfig,
779    /// Function-size rule configuration.
780    pub function: CodeSizeRuleConfig,
781    /// Block-size rule configuration.
782    pub block: CodeSizeRuleConfig,
783}
784
785impl Default for CodeSizeRuleConfigSet {
786    fn default() -> Self {
787        Self {
788            file: CodeSizeRuleConfig {
789                enabled: true,
790                max_lines: nonzero_u32(1_000),
791                severity: Severity::Warning,
792                include_tests: false,
793            },
794            function: CodeSizeRuleConfig {
795                enabled: true,
796                max_lines: nonzero_u32(250),
797                severity: Severity::Warning,
798                include_tests: true,
799            },
800            block: CodeSizeRuleConfig {
801                enabled: true,
802                max_lines: nonzero_u32(50),
803                severity: Severity::Warning,
804                include_tests: true,
805            },
806        }
807    }
808}
809
810impl CodeSizeRuleConfigSet {
811    /// Returns config for one rule.
812    pub fn get(&self, rule: CodeSizeRuleKind) -> &CodeSizeRuleConfig {
813        match rule {
814            CodeSizeRuleKind::File => &self.file,
815            CodeSizeRuleKind::Function => &self.function,
816            CodeSizeRuleKind::Block => &self.block,
817        }
818    }
819
820    /// Returns mutable config for one rule.
821    pub fn get_mut(&mut self, rule: CodeSizeRuleKind) -> &mut CodeSizeRuleConfig {
822        match rule {
823            CodeSizeRuleKind::File => &mut self.file,
824            CodeSizeRuleKind::Function => &mut self.function,
825            CodeSizeRuleKind::Block => &mut self.block,
826        }
827    }
828}
829
830/// Code-size rule settings.
831#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
832#[serde(rename_all = "camelCase")]
833pub struct CodeSizeRuleConfig {
834    /// Enables this rule.
835    pub enabled: bool,
836    /// Maximum allowed lines.
837    pub max_lines: NonZeroU32,
838    /// Finding severity.
839    pub severity: Severity,
840    /// Whether this rule includes test paths and test ranges.
841    pub include_tests: bool,
842}
843
844/// Path-specific code-size override.
845#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
846#[serde(rename_all = "camelCase")]
847pub struct CodeSizeOverride {
848    /// Matching path globs.
849    pub paths: Vec<RepoGlob>,
850    /// Rule patches applied by this override.
851    pub rules: CodeSizeRuleConfigPatchSet,
852    /// Required human reason for the override.
853    pub reason: String,
854}
855
856/// Optional rule patch set.
857#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
858#[serde(rename_all = "camelCase")]
859pub struct CodeSizeRuleConfigPatchSet {
860    /// File rule patch.
861    #[serde(skip_serializing_if = "Option::is_none")]
862    pub file: Option<CodeSizeRuleConfigPatch>,
863    /// Function rule patch.
864    #[serde(skip_serializing_if = "Option::is_none")]
865    pub function: Option<CodeSizeRuleConfigPatch>,
866    /// Block rule patch.
867    #[serde(skip_serializing_if = "Option::is_none")]
868    pub block: Option<CodeSizeRuleConfigPatch>,
869}
870
871impl CodeSizeRuleConfigPatchSet {
872    /// Returns patch for one rule.
873    pub fn get(&self, rule: CodeSizeRuleKind) -> Option<&CodeSizeRuleConfigPatch> {
874        match rule {
875            CodeSizeRuleKind::File => self.file.as_ref(),
876            CodeSizeRuleKind::Function => self.function.as_ref(),
877            CodeSizeRuleKind::Block => self.block.as_ref(),
878        }
879    }
880}
881
882/// Optional code-size rule patch.
883#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
884#[serde(rename_all = "camelCase")]
885pub struct CodeSizeRuleConfigPatch {
886    /// Enables or disables this rule.
887    #[serde(skip_serializing_if = "Option::is_none")]
888    pub enabled: Option<bool>,
889    /// Maximum allowed lines.
890    #[serde(skip_serializing_if = "Option::is_none")]
891    pub max_lines: Option<NonZeroU32>,
892    /// Finding severity.
893    #[serde(skip_serializing_if = "Option::is_none")]
894    pub severity: Option<Severity>,
895    /// Whether this rule includes test paths and test ranges.
896    #[serde(skip_serializing_if = "Option::is_none")]
897    pub include_tests: Option<bool>,
898}
899
900/// Validated repo-level manifest.
901#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
902#[serde(rename_all = "camelCase")]
903pub struct RepoManifest {
904    /// Manifest schema id.
905    pub schema: SchemaId,
906    /// Repository name.
907    pub name: RepoName,
908    /// Required repository layout.
909    pub layout: RepoLayout,
910    /// Default owner used when project manifests omit owners.
911    pub default_owner: Option<OwnerHandle>,
912    /// Proto source root.
913    pub protos_root: RepoRelativePath,
914    /// Core infrastructure root.
915    pub core_infra_root: RepoRelativePath,
916    /// Agent skills root.
917    pub agent_skills_root: RepoRelativePath,
918    /// Claude skills root.
919    pub claude_skills_root: RepoRelativePath,
920    /// Context output root.
921    pub context_output: RepoRelativePath,
922    /// Generated-code policy.
923    pub generated_code_policy: GeneratedCodePolicy,
924    /// Global policy settings.
925    pub policies: RepoPolicySet,
926    /// Repository inspection settings.
927    #[serde(default)]
928    pub inspection: RepoInspectionConfig,
929}
930
931/// Repo-level policy configuration.
932#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
933#[serde(rename_all = "camelCase")]
934pub struct RepoPolicySet {
935    /// Cross-app dependency policy mode.
936    pub cross_app_dependency: PolicyMode,
937    /// Framework internal dependency policy mode.
938    pub framework_internal_dependency: PolicyMode,
939    /// Generated-code direct-edit policy mode.
940    pub generated_code_direct_edit: PolicyMode,
941    /// Required owners for production changes.
942    pub prod_change_required_owners: Vec<OwnerHandle>,
943}
944
945/// Policy enforcement mode.
946#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
947#[serde(rename_all = "kebab-case")]
948pub enum PolicyMode {
949    /// Deny the matched condition.
950    #[default]
951    Deny,
952    /// Warn on the matched condition.
953    Warn,
954    /// Allow the matched condition.
955    Allow,
956}
957
958/// Validated workspace specification.
959#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
960#[serde(rename_all = "camelCase")]
961pub struct WorkspaceSpec {
962    /// Workspace id.
963    pub name: WorkspaceName,
964    /// Workspace language.
965    pub language: WorkspaceLanguage,
966    /// Optional execution toolchain such as `npm`, `bun`, or `uv`.
967    pub toolchain: Option<Toolchain>,
968    /// Workspace root relative to the project root.
969    pub root: ProjectRelativePath,
970    /// Workspace manifest path relative to the project root.
971    pub manifest: ProjectRelativePath,
972    /// Optional lockfile path relative to the project root.
973    pub lockfile: Option<ProjectRelativePath>,
974    /// Optional repo-level target directory.
975    pub target_dir: Option<RepoRelativePath>,
976    /// Optional repo-level cache directory.
977    pub cache_dir: Option<RepoRelativePath>,
978}
979
980/// Task command declared for a workspace.
981#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
982#[serde(rename_all = "camelCase")]
983pub struct TaskCommand {
984    /// Workspace that should execute the command.
985    pub workspace: WorkspaceName,
986    /// Command in argv form.
987    pub command: CommandSpec,
988    /// Commands that must run before this command.
989    pub depends_on: Vec<TaskDependency>,
990}
991
992/// Task prerequisite edge.
993#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
994#[serde(rename_all = "camelCase")]
995pub struct TaskDependency {
996    /// Project containing the prerequisite task.
997    pub project: ProjectName,
998    /// Workspace containing the prerequisite task.
999    pub workspace: WorkspaceName,
1000    /// Task to run first.
1001    pub task: TaskName,
1002}
1003
1004/// Task run plan command in argv form.
1005#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1006#[serde(rename_all = "camelCase")]
1007pub struct CommandSpec {
1008    /// Executable program.
1009    pub program: String,
1010    /// Command arguments.
1011    pub args: Vec<String>,
1012}
1013
1014impl CommandSpec {
1015    /// Parses a PRD-compatible command string into argv form.
1016    pub fn parse(value: &str) -> Result<Self, Diagnostic> {
1017        if value.trim().is_empty() || value.len() > COMMAND_PART_MAX_BYTES * COMMAND_ARG_LIMIT {
1018            return Err(Diagnostic::error(
1019                "manifest.command.invalid",
1020                "command must be non-empty and length-bounded",
1021            ));
1022        }
1023        reject_shell_syntax(value)?;
1024        let parts = split_command(value)?;
1025        if parts.is_empty() {
1026            return Err(Diagnostic::error(
1027                "manifest.command.invalid",
1028                "command must include a program",
1029            ));
1030        }
1031        if parts.len() > COMMAND_ARG_LIMIT {
1032            return Err(Diagnostic::error(
1033                "manifest.command.too_many_args",
1034                "command has too many arguments",
1035            ));
1036        }
1037        for part in &parts {
1038            if part.len() > COMMAND_PART_MAX_BYTES {
1039                return Err(Diagnostic::error(
1040                    "manifest.command.part_too_long",
1041                    "command part exceeds byte limit",
1042                ));
1043            }
1044        }
1045        let mut parts = parts.into_iter();
1046        let program = parts.next().ok_or_else(|| {
1047            Diagnostic::error("manifest.command.invalid", "command must include a program")
1048        })?;
1049        Ok(Self {
1050            program,
1051            args: parts.collect(),
1052        })
1053    }
1054}
1055
1056/// Dependency target surface requested by a manifest or discovered adapter.
1057#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1058#[serde(rename_all = "kebab-case")]
1059pub enum DependencySurface {
1060    /// No specific surface was requested.
1061    Unspecified,
1062    /// Public facade of a framework.
1063    FrameworkFacade,
1064    /// Internal area of a framework.
1065    FrameworkInternal,
1066    /// Public client of a foundation service.
1067    FoundationPublicClient,
1068    /// Internal area of a foundation service.
1069    FoundationInternal,
1070    /// Public module of core infrastructure.
1071    CoreInfraPublicModule,
1072    /// Internal module of core infrastructure.
1073    CoreInfraInternalModule,
1074}
1075
1076/// Dependency target.
1077#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1078#[serde(rename_all = "camelCase")]
1079pub enum DependencyTarget {
1080    /// Dependency points to a project.
1081    Project(ProjectName),
1082    /// Dependency points to a proto package.
1083    ProtoPackage(ProtoPackageName),
1084}
1085
1086/// Validated dependency declaration.
1087#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1088#[serde(rename_all = "camelCase")]
1089pub struct ProjectDependency {
1090    /// Raw dependency id as written in the manifest.
1091    pub id: String,
1092    /// Resolved target.
1093    pub target: DependencyTarget,
1094    /// Requested dependency surface.
1095    pub surface: DependencySurface,
1096}
1097
1098impl ProjectDependency {
1099    /// Parses a dependency declaration.
1100    pub fn parse(value: impl Into<String>) -> Result<Self, Diagnostic> {
1101        let value = value.into();
1102        if value.starts_with("protos.") {
1103            return Ok(Self {
1104                target: DependencyTarget::ProtoPackage(ProtoPackageName::new(value.clone())?),
1105                id: value,
1106                surface: DependencySurface::Unspecified,
1107            });
1108        }
1109        let (target, surface) = if let Some(stripped) = value.strip_suffix(".client") {
1110            (
1111                ProjectName::new(stripped.to_string())?,
1112                DependencySurface::FoundationPublicClient,
1113            )
1114        } else if let Some(stripped) = value.strip_suffix(".internal") {
1115            let surface = if stripped.starts_with("frameworks.") {
1116                DependencySurface::FrameworkInternal
1117            } else if stripped.starts_with("foundations.") {
1118                DependencySurface::FoundationInternal
1119            } else {
1120                DependencySurface::Unspecified
1121            };
1122            (ProjectName::new(stripped.to_string())?, surface)
1123        } else if let Some(stripped) = value.strip_suffix(".facade") {
1124            (
1125                ProjectName::new(stripped.to_string())?,
1126                DependencySurface::FrameworkFacade,
1127            )
1128        } else {
1129            (
1130                ProjectName::new(value.clone())?,
1131                DependencySurface::Unspecified,
1132            )
1133        };
1134        Ok(Self {
1135            id: value,
1136            target: DependencyTarget::Project(target),
1137            surface,
1138        })
1139    }
1140}
1141
1142/// Project proto ownership and consumption specification.
1143#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1144#[serde(rename_all = "camelCase")]
1145pub struct ProjectProtoSpec {
1146    /// Proto glob patterns owned by the project.
1147    pub owns: Vec<RepoGlob>,
1148    /// Proto glob patterns consumed by the project.
1149    pub consumes: Vec<RepoGlob>,
1150}
1151
1152/// Project `IaC` specification.
1153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1154#[serde(rename_all = "camelCase")]
1155pub struct IacSpec {
1156    /// `IaC` root relative to the project root.
1157    pub root: ProjectRelativePath,
1158    /// `IaC` provider.
1159    pub provider: IacProvider,
1160    /// Declared stack names.
1161    pub stacks: Vec<String>,
1162}
1163
1164/// Project deployment specification.
1165#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1166#[serde(rename_all = "camelCase")]
1167pub struct DeploySpec {
1168    /// Deploy root relative to the project root.
1169    pub root: ProjectRelativePath,
1170    /// Environment names.
1171    pub environments: Vec<String>,
1172}
1173
1174/// DNS intent declared by a project manifest.
1175#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1176#[serde(rename_all = "camelCase")]
1177pub struct ProjectDnsSpec {
1178    /// DNS provider name when known.
1179    #[serde(skip_serializing_if = "Option::is_none")]
1180    pub provider: Option<String>,
1181    /// DNS records owned or verified by the project.
1182    pub records: Vec<DnsRecordSpec>,
1183}
1184
1185/// DNS record intent.
1186#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1187#[serde(rename_all = "camelCase")]
1188pub struct DnsRecordSpec {
1189    /// DNS record name.
1190    pub name: String,
1191    /// DNS record type.
1192    pub record_type: String,
1193    /// Expected target value or output selector.
1194    pub target: String,
1195    /// Expected Cloudflare proxy setting when known.
1196    #[serde(skip_serializing_if = "Option::is_none")]
1197    pub proxied: Option<bool>,
1198    /// Expected TTL in seconds when declared.
1199    #[serde(skip_serializing_if = "Option::is_none")]
1200    pub ttl: Option<u32>,
1201}
1202
1203/// CDN intent declared by a project manifest.
1204#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1205#[serde(rename_all = "camelCase")]
1206pub struct CdnSpec {
1207    /// CDN provider name.
1208    pub provider: String,
1209    /// Host aliases expected to be served by the CDN.
1210    pub aliases: Vec<String>,
1211    /// Expected response header patterns such as `via: *CloudFront*`.
1212    pub expected_response_headers: Vec<String>,
1213}
1214
1215/// Operations metadata declared by a project manifest.
1216#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1217#[serde(rename_all = "camelCase")]
1218pub struct ProjectOpsSpec {
1219    /// Non-secret smoke probes.
1220    pub probes: Vec<ProbeSpec>,
1221    /// Runtime dependencies that should be verified with this project.
1222    pub runtime_dependencies: Vec<RuntimeDependencySpec>,
1223    /// Known manual state records to reconcile.
1224    pub manual_state: Vec<ManualStateRecord>,
1225}
1226
1227/// Operational smoke probe.
1228#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1229#[serde(rename_all = "camelCase")]
1230pub struct ProbeSpec {
1231    /// Probe name.
1232    pub name: String,
1233    /// HTTP method.
1234    pub method: String,
1235    /// URL to probe.
1236    pub url: String,
1237    /// Expected response properties.
1238    pub expect: ProbeExpectation,
1239    /// Failure class assigned by verification.
1240    #[serde(skip_serializing_if = "Option::is_none")]
1241    pub classification: Option<String>,
1242}
1243
1244/// Expected probe result.
1245#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1246#[serde(rename_all = "camelCase")]
1247pub struct ProbeExpectation {
1248    /// Expected status code.
1249    #[serde(skip_serializing_if = "Option::is_none")]
1250    pub status: Option<u16>,
1251    /// Header match patterns keyed by header name.
1252    pub headers: BTreeMap<String, String>,
1253    /// Body substring expected in a non-secret response.
1254    #[serde(skip_serializing_if = "Option::is_none")]
1255    pub body_contains: Option<String>,
1256}
1257
1258/// Runtime dependency that should be healthy before declaring an operation complete.
1259#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1260#[serde(rename_all = "camelCase")]
1261pub struct RuntimeDependencySpec {
1262    /// Depended-on project id.
1263    pub project: ProjectName,
1264    /// Dependency endpoint.
1265    pub endpoint: String,
1266    /// Human-readable purpose.
1267    pub purpose: String,
1268}
1269
1270/// Manual cloud or infrastructure state that must be reconciled back into `IaC`.
1271#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1272#[serde(rename_all = "camelCase")]
1273pub struct ManualStateRecord {
1274    /// Stable manual action kind.
1275    pub kind: String,
1276    /// Resource identifier.
1277    pub resource: String,
1278    /// Current reconciliation status.
1279    pub status: String,
1280    /// Managed equivalent when `IaC` now owns the state.
1281    #[serde(skip_serializing_if = "Option::is_none")]
1282    pub managed_equivalent: Option<String>,
1283    /// Cleanup command in argv form when available.
1284    #[serde(skip_serializing_if = "Option::is_none")]
1285    pub cleanup_command: Option<ProcessCommand>,
1286}
1287
1288/// Project AI edit policy.
1289#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1290#[serde(rename_all = "camelCase")]
1291pub struct ProjectAiSpec {
1292    /// Editable path globs.
1293    pub editable: Vec<RepoGlob>,
1294    /// Do-not-edit path globs.
1295    pub do_not_edit: Vec<RepoGlob>,
1296    /// Documentation paths.
1297    pub docs: Vec<ProjectRelativePath>,
1298}
1299
1300/// Public and internal project areas used for boundary classification.
1301#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1302#[serde(rename_all = "camelCase")]
1303pub struct ProjectAreas {
1304    /// Public facade paths keyed by language.
1305    pub public_facades: BTreeMap<String, Vec<ProjectRelativePath>>,
1306    /// Public client paths keyed by language.
1307    pub public_clients: BTreeMap<String, Vec<ProjectRelativePath>>,
1308    /// Internal paths keyed by language.
1309    pub internal: BTreeMap<String, Vec<ProjectRelativePath>>,
1310    /// Public core-infra modules.
1311    pub public_modules: Vec<ProjectRelativePath>,
1312    /// Internal core-infra modules.
1313    pub internal_modules: Vec<ProjectRelativePath>,
1314}
1315
1316/// Validated project manifest.
1317#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1318#[serde(rename_all = "camelCase")]
1319pub struct ProjectManifest {
1320    /// Manifest schema id.
1321    pub schema: SchemaId,
1322    /// Project id.
1323    pub name: ProjectName,
1324    /// Project kind.
1325    pub kind: ProjectKind,
1326    /// Project root path in the repository.
1327    pub path: RepoRelativePath,
1328    /// Project owners.
1329    pub owners: Vec<OwnerHandle>,
1330    /// Project visibility.
1331    pub visibility: Visibility,
1332    /// Workspaces declared by the project.
1333    pub workspaces: Vec<WorkspaceSpec>,
1334    /// Project dependencies declared in the manifest.
1335    pub depends_on: Vec<ProjectDependency>,
1336    /// Task definitions keyed by task name.
1337    pub tasks: BTreeMap<TaskName, Vec<TaskCommand>>,
1338    /// Optional `IaC` specification.
1339    pub iac: Option<IacSpec>,
1340    /// Optional deployment specification.
1341    pub deploy: Option<DeploySpec>,
1342    /// Optional DNS intent.
1343    pub dns: ProjectDnsSpec,
1344    /// Optional CDN intent.
1345    pub cdn: Option<CdnSpec>,
1346    /// Optional operations metadata.
1347    pub ops: ProjectOpsSpec,
1348    /// Optional proto specification.
1349    pub protos: ProjectProtoSpec,
1350    /// Optional AI policy specification.
1351    pub ai: ProjectAiSpec,
1352    /// Public and internal areas.
1353    pub areas: ProjectAreas,
1354    /// Project-local policy flags.
1355    pub policies: BTreeMap<String, serde_json::Value>,
1356    /// Source manifest path.
1357    pub source: RepoRelativePath,
1358}
1359
1360impl ProjectManifest {
1361    /// Returns the project node id.
1362    pub fn node_id(&self) -> String {
1363        format!("project:{}", self.name)
1364    }
1365
1366    /// Returns true when the project owns the given repo-relative path.
1367    pub fn contains_path(&self, path: &RepoRelativePath) -> bool {
1368        path.starts_with(&self.path)
1369    }
1370}
1371
1372/// Template manifest.
1373#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1374#[serde(rename_all = "camelCase")]
1375pub struct TemplateManifest {
1376    /// Manifest schema id.
1377    pub schema: SchemaId,
1378    /// Template name.
1379    pub name: String,
1380    /// Template kind.
1381    pub kind: String,
1382    /// Template engine.
1383    pub engine: String,
1384    /// Template inputs.
1385    pub inputs: Vec<TemplateInput>,
1386    /// Template file mappings.
1387    pub files: Vec<TemplateFile>,
1388    /// Post-render validation commands.
1389    pub post_render_validate: Vec<CommandSpec>,
1390}
1391
1392/// Template source selector.
1393#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1394#[serde(rename_all = "camelCase", tag = "kind")]
1395pub enum TemplateSource {
1396    /// Built-in template by stable name.
1397    Builtin {
1398        /// Built-in template name.
1399        name: String,
1400    },
1401    /// Repository-local template root.
1402    Local {
1403        /// Repo-relative template root.
1404        root: RepoRelativePath,
1405    },
1406}
1407
1408/// Resolved template source and manifest.
1409#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1410#[serde(rename_all = "camelCase")]
1411pub struct ResolvedTemplateSource {
1412    /// Source root used to resolve template files.
1413    pub root: RepoRelativePath,
1414    /// Parsed template manifest.
1415    pub manifest: TemplateManifest,
1416}
1417
1418/// Template input declaration.
1419#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1420#[serde(rename_all = "camelCase")]
1421pub struct TemplateInput {
1422    /// Input name.
1423    pub name: String,
1424    /// Input type.
1425    pub input_type: String,
1426    /// Whether the input is required.
1427    pub required: bool,
1428    /// Optional JSON default value.
1429    pub default: Option<serde_json::Value>,
1430}
1431
1432/// Template file declaration.
1433#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1434#[serde(rename_all = "camelCase")]
1435pub struct TemplateFile {
1436    /// Template source path.
1437    pub source: ProjectRelativePath,
1438    /// Render target expression.
1439    pub target: String,
1440    /// Render mode.
1441    pub mode: String,
1442    /// Optional condition expression.
1443    pub when: Option<String>,
1444}
1445
1446/// Graph node kind.
1447#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1448#[serde(rename_all = "kebab-case")]
1449pub enum GraphNodeKind {
1450    /// Project node.
1451    Project,
1452    /// Workspace node.
1453    Workspace,
1454    /// Proto package or glob node.
1455    ProtoPackage,
1456    /// Infrastructure target node.
1457    IacTarget,
1458    /// Template node.
1459    Template,
1460}
1461
1462/// Graph node.
1463#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1464#[serde(rename_all = "camelCase")]
1465pub struct GraphNode {
1466    /// Stable graph node id.
1467    pub id: String,
1468    /// Node kind.
1469    pub kind: GraphNodeKind,
1470    /// Human-readable label.
1471    pub label: String,
1472    /// Owning project when applicable.
1473    pub project: Option<ProjectName>,
1474    /// Owning workspace when applicable.
1475    pub workspace: Option<WorkspaceName>,
1476}
1477
1478/// Graph edge kind.
1479#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1480#[serde(rename_all = "kebab-case")]
1481pub enum EdgeKind {
1482    /// Project dependency edge.
1483    DependsOnProject,
1484    /// Project contains workspace edge.
1485    ContainsWorkspace,
1486    /// Project consumes proto edge.
1487    ConsumesProto,
1488    /// Project owns proto edge.
1489    OwnsProto,
1490    /// Project uses framework facade edge.
1491    UsesFrameworkFacade,
1492    /// Project uses framework internal edge.
1493    UsesFrameworkInternal,
1494    /// Project uses foundation public client edge.
1495    UsesFoundationClient,
1496    /// Project uses foundation internal edge.
1497    UsesFoundationInternal,
1498    /// Project uses public core infrastructure module edge.
1499    UsesCoreInfraModule,
1500    /// Project uses internal core infrastructure module edge.
1501    UsesCoreInfraInternalModule,
1502    /// Project owns `IaC` target edge.
1503    OwnsIac,
1504    /// Project runs task edge.
1505    RunsTask,
1506}
1507
1508/// Graph edge.
1509#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1510#[serde(rename_all = "camelCase")]
1511pub struct GraphEdge {
1512    /// Source node id.
1513    pub from: String,
1514    /// Target node id.
1515    pub to: String,
1516    /// Edge kind.
1517    pub kind: EdgeKind,
1518    /// Evidence path or manifest value.
1519    pub evidence: Option<String>,
1520}
1521
1522/// Repository graph.
1523#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1524#[serde(rename_all = "camelCase")]
1525pub struct RepoGraph {
1526    /// Graph nodes.
1527    pub nodes: Vec<GraphNode>,
1528    /// Graph edges.
1529    pub edges: Vec<GraphEdge>,
1530}
1531
1532impl RepoGraph {
1533    /// Adds a graph node if it is not already present.
1534    pub fn add_node(&mut self, node: GraphNode) {
1535        if !self.nodes.iter().any(|existing| existing.id == node.id) {
1536            self.nodes.push(node);
1537        }
1538    }
1539
1540    /// Adds a graph edge if it is not already present.
1541    pub fn add_edge(&mut self, edge: GraphEdge) {
1542        if !self.edges.iter().any(|existing| existing == &edge) {
1543            self.edges.push(edge);
1544        }
1545    }
1546}
1547
1548/// Snapshot of the discovered repository.
1549#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1550#[serde(rename_all = "camelCase")]
1551pub struct RepoSnapshot {
1552    /// Absolute repository root.
1553    pub root: RepoRoot,
1554    /// Repo-level manifest.
1555    pub repo_manifest: RepoManifest,
1556    /// Discovered project manifests.
1557    pub projects: Vec<ProjectManifest>,
1558    /// Built repository graph.
1559    pub graph: RepoGraph,
1560    /// Generated-code policy.
1561    pub generated_policy: GeneratedCodePolicy,
1562    /// Unix timestamp in seconds when discovery completed.
1563    pub discovered_at_unix: u64,
1564}
1565
1566impl RepoSnapshot {
1567    /// Creates a repository snapshot.
1568    pub fn new(
1569        root: RepoRoot,
1570        repo_manifest: RepoManifest,
1571        projects: Vec<ProjectManifest>,
1572        graph: RepoGraph,
1573    ) -> Self {
1574        let discovered_at_unix = SystemTime::now()
1575            .duration_since(UNIX_EPOCH)
1576            .map_or(0, |duration| duration.as_secs());
1577        let generated_policy = repo_manifest.generated_code_policy.clone();
1578        Self {
1579            root,
1580            repo_manifest,
1581            projects,
1582            graph,
1583            generated_policy,
1584            discovered_at_unix,
1585        }
1586    }
1587
1588    /// Finds a project by name.
1589    pub fn project(&self, name: &ProjectName) -> Option<&ProjectManifest> {
1590        self.projects.iter().find(|project| &project.name == name)
1591    }
1592}
1593
1594/// Request to discover a repository.
1595#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1596#[serde(rename_all = "camelCase")]
1597pub struct DiscoverRequest {
1598    /// Optional starting path or explicit repo root.
1599    pub repo: Option<PathBuf>,
1600}
1601
1602/// Request to validate a graph and boundary policies.
1603#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1604#[serde(rename_all = "camelCase")]
1605pub struct GraphValidateRequest {
1606    /// Optional starting path or explicit repo root.
1607    pub repo: Option<PathBuf>,
1608    /// Changed files used by path-based policy rules.
1609    pub changed_files: Vec<RepoRelativePath>,
1610    /// Validation depth.
1611    pub mode: ValidationMode,
1612}
1613
1614/// Graph validation mode.
1615#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1616#[serde(rename_all = "kebab-case")]
1617pub enum ValidationMode {
1618    /// Structural manifest and policy checks only.
1619    #[default]
1620    Structural,
1621    /// Structural checks plus offline metadata checks when available.
1622    Metadata,
1623    /// Metadata checks plus full task-planning/environment checks.
1624    Full,
1625}
1626
1627/// Request to print a graph.
1628#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1629#[serde(rename_all = "camelCase")]
1630pub struct GraphPrintRequest {
1631    /// Optional starting path or explicit repo root.
1632    pub repo: Option<PathBuf>,
1633}
1634
1635/// Report returned for graph printing.
1636#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1637#[serde(rename_all = "camelCase")]
1638pub struct GraphPrintReport {
1639    /// Repository snapshot.
1640    pub snapshot: RepoSnapshot,
1641}
1642
1643/// Request to explain a selector.
1644#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1645#[serde(rename_all = "camelCase")]
1646pub struct ExplainRequest {
1647    /// Optional starting path or explicit repo root.
1648    pub repo: Option<PathBuf>,
1649    /// Project name or graph node id.
1650    pub selector: String,
1651}
1652
1653/// Explanation report for a graph selector.
1654#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1655#[serde(rename_all = "camelCase")]
1656pub struct ExplainReport {
1657    /// Requested selector.
1658    pub selector: String,
1659    /// Matching graph nodes.
1660    pub nodes: Vec<GraphNode>,
1661    /// Edges touching the matching nodes.
1662    pub edges: Vec<GraphEdge>,
1663    /// Diagnostics produced while explaining.
1664    pub diagnostics: Vec<Diagnostic>,
1665}
1666
1667/// Request to lint boundaries.
1668#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1669#[serde(rename_all = "camelCase")]
1670pub struct BoundaryLintRequest {
1671    /// Optional starting path or explicit repo root.
1672    pub repo: Option<PathBuf>,
1673    /// Changed files used by path-based policy rules.
1674    pub changed_files: Vec<RepoRelativePath>,
1675}
1676
1677/// Boundary lint report.
1678#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1679#[serde(rename_all = "camelCase")]
1680pub struct BoundaryLintReport {
1681    /// Boundary diagnostics.
1682    pub diagnostics: Vec<Diagnostic>,
1683}
1684
1685/// Init profile.
1686#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1687#[serde(rename_all = "kebab-case")]
1688pub enum InitProfile {
1689    /// Startup profile.
1690    Startup,
1691    /// Enterprise profile.
1692    Enterprise,
1693}
1694
1695/// Request for repository initialization.
1696#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1697#[serde(rename_all = "camelCase")]
1698pub struct InitRequest {
1699    /// Target repo root.
1700    pub repo_root: PathBuf,
1701    /// Repository name.
1702    pub name: RepoName,
1703    /// Initialization profile.
1704    pub profile: InitProfile,
1705    /// Repository layout.
1706    pub layout: RepoLayout,
1707    /// Whether to only plan writes.
1708    pub dry_run: bool,
1709}
1710
1711/// Planned file operation.
1712#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1713#[serde(rename_all = "camelCase")]
1714pub struct FileOperation {
1715    /// Target path.
1716    pub path: RepoRelativePath,
1717    /// Operation kind.
1718    pub operation: String,
1719    /// UTF-8 content to write for file operations.
1720    #[serde(skip_serializing_if = "Option::is_none")]
1721    pub content: Option<String>,
1722}
1723
1724/// Repository initialization plan.
1725#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1726#[serde(rename_all = "camelCase")]
1727pub struct InitPlan {
1728    /// Planned operations.
1729    pub operations: Vec<FileOperation>,
1730    /// Warnings.
1731    pub warnings: Vec<Diagnostic>,
1732    /// Recommended next steps.
1733    pub next_steps: Vec<String>,
1734}
1735
1736/// Request to create a new project.
1737#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1738#[serde(rename_all = "camelCase")]
1739pub struct NewProjectRequest {
1740    /// Optional starting path or explicit repo root.
1741    pub repo: Option<PathBuf>,
1742    /// Project kind.
1743    pub kind: ProjectKind,
1744    /// Project path.
1745    pub path: RepoRelativePath,
1746    /// Requested stack entries such as `rust-api`, `bun-web`, and `uv-jobs`.
1747    pub stack: Vec<String>,
1748    /// Requested languages for framework/foundation generation.
1749    pub languages: Vec<String>,
1750    /// Requested public clients for foundation generation.
1751    pub clients: Vec<String>,
1752    /// Whether framework generation should include facade/internal areas.
1753    pub facade: bool,
1754    /// Optional `IaC` provider.
1755    pub iac: Option<IacProvider>,
1756    /// Optional proto package.
1757    pub proto: Option<ProtoPackageName>,
1758    /// Optional owner handle.
1759    pub owner: Option<OwnerHandle>,
1760    /// Whether to only plan writes.
1761    pub dry_run: bool,
1762}
1763
1764/// Render plan returned by project and template operations.
1765#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1766#[serde(rename_all = "camelCase")]
1767pub struct RenderPlan {
1768    /// Planned file operations.
1769    pub operations: Vec<FileOperation>,
1770    /// Diagnostics.
1771    pub diagnostics: Vec<Diagnostic>,
1772}
1773
1774/// Template listing request.
1775#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1776#[serde(rename_all = "camelCase")]
1777pub struct TemplateListRequest {
1778    /// Optional starting path or explicit repo root.
1779    pub repo: Option<PathBuf>,
1780}
1781
1782/// Template summary.
1783#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1784#[serde(rename_all = "camelCase")]
1785pub struct TemplateSummary {
1786    /// Template source label.
1787    pub source: String,
1788    /// Template name.
1789    pub name: String,
1790    /// Template kind.
1791    pub kind: String,
1792}
1793
1794/// Template listing report.
1795#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1796#[serde(rename_all = "camelCase")]
1797pub struct TemplateListReport {
1798    /// Available templates.
1799    pub templates: Vec<TemplateSummary>,
1800    /// Diagnostics.
1801    pub diagnostics: Vec<Diagnostic>,
1802}
1803
1804/// Template render request.
1805#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1806#[serde(rename_all = "camelCase")]
1807pub struct TemplateRenderRequest {
1808    /// Optional starting path or explicit repo root.
1809    pub repo: Option<PathBuf>,
1810    /// Source to render.
1811    pub source: TemplateSource,
1812    /// JSON input values.
1813    pub inputs: serde_json::Value,
1814    /// Whether to only plan writes.
1815    pub dry_run: bool,
1816}
1817
1818/// Request for affected analysis.
1819#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1820#[serde(rename_all = "camelCase")]
1821pub struct AffectedRequest {
1822    /// Optional starting path or explicit repo root.
1823    pub repo: Option<PathBuf>,
1824    /// Optional base git ref.
1825    pub base: Option<String>,
1826    /// Optional head git ref.
1827    pub head: Option<String>,
1828    /// Changed files.
1829    pub changed_files: Vec<RepoRelativePath>,
1830    /// Requested task names for affected reports.
1831    pub tasks: Vec<TaskName>,
1832}
1833
1834/// Affected analysis report.
1835#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1836#[serde(rename_all = "camelCase")]
1837pub struct AffectedReport {
1838    /// Directly affected project ids.
1839    pub directly_affected: Vec<ProjectName>,
1840    /// Transitively affected project ids.
1841    pub transitively_affected: Vec<ProjectName>,
1842    /// Affected workspaces in `project:workspace` form.
1843    pub workspaces: Vec<String>,
1844    /// Affected task ids in `project:workspace:task` form.
1845    pub tasks: Vec<String>,
1846    /// Risk flags emitted by affected analysis.
1847    pub risk_flags: Vec<String>,
1848    /// Explainable reasons.
1849    pub reasons: Vec<AffectedReason>,
1850    /// Suggested reviewers.
1851    pub suggested_reviewers: Vec<OwnerHandle>,
1852    /// Diagnostics.
1853    pub diagnostics: Vec<Diagnostic>,
1854}
1855
1856/// Reason attached to an affected result.
1857#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1858#[serde(rename_all = "camelCase")]
1859pub struct AffectedReason {
1860    /// Changed or propagated source.
1861    pub source: String,
1862    /// Affected target.
1863    pub target: String,
1864    /// Human-readable explanation.
1865    pub reason: String,
1866}
1867
1868/// Request to run repo tasks.
1869#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1870#[serde(rename_all = "camelCase")]
1871pub struct TaskRunRequest {
1872    /// Optional starting path or explicit repo root.
1873    pub repo: Option<PathBuf>,
1874    /// Requested task names.
1875    pub tasks: Vec<TaskName>,
1876    /// Optional project filter.
1877    pub projects: Vec<ProjectName>,
1878    /// Optional workspace filter in `project:workspace` form.
1879    pub workspaces: Vec<String>,
1880    /// Run only affected tasks.
1881    pub affected: bool,
1882    /// Changed files used when `affected` is true.
1883    pub changed_files: Vec<RepoRelativePath>,
1884    /// Optional base git ref used when `affected` is true.
1885    pub base: Option<String>,
1886    /// Optional head git ref used when `affected` is true.
1887    pub head: Option<String>,
1888    /// Maximum task concurrency.
1889    pub concurrency: Option<NonZeroU32>,
1890    /// Plan tasks without executing them.
1891    pub dry_run: bool,
1892}
1893
1894/// Task run plan.
1895#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1896#[serde(rename_all = "camelCase")]
1897pub struct TaskRunPlan {
1898    /// Planned process commands.
1899    pub commands: Vec<ProcessCommand>,
1900    /// Maximum concurrency.
1901    pub concurrency: NonZeroUsize,
1902}
1903
1904/// Task run report.
1905#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1906#[serde(rename_all = "camelCase")]
1907pub struct TaskRunReport {
1908    /// Planned task commands.
1909    pub commands: Vec<ProcessCommand>,
1910    /// Command outputs.
1911    pub outputs: Vec<TaskCommandOutput>,
1912    /// Diagnostics.
1913    pub diagnostics: Vec<Diagnostic>,
1914}
1915
1916/// Output from one task command.
1917#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1918#[serde(rename_all = "camelCase")]
1919pub struct TaskCommandOutput {
1920    /// Project id.
1921    pub project: ProjectName,
1922    /// Workspace id.
1923    pub workspace: WorkspaceName,
1924    /// Task id.
1925    pub task: TaskName,
1926    /// Process output.
1927    pub output: ProcessOutput,
1928}
1929
1930/// Process command passed to a runner.
1931#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1932#[serde(rename_all = "camelCase")]
1933pub struct ProcessCommand {
1934    /// Project id.
1935    #[serde(skip_serializing_if = "Option::is_none")]
1936    pub project: Option<ProjectName>,
1937    /// Workspace id.
1938    #[serde(skip_serializing_if = "Option::is_none")]
1939    pub workspace: Option<WorkspaceName>,
1940    /// Task id.
1941    #[serde(skip_serializing_if = "Option::is_none")]
1942    pub task: Option<TaskName>,
1943    /// Working directory.
1944    pub cwd: RepoRelativePath,
1945    /// Absolute working directory for local execution.
1946    #[serde(skip_serializing_if = "Option::is_none")]
1947    pub absolute_cwd: Option<PathBuf>,
1948    /// Program.
1949    pub program: String,
1950    /// Arguments.
1951    pub args: Vec<String>,
1952    /// Environment overlay.
1953    pub env: BTreeMap<String, String>,
1954}
1955
1956/// Process output from a runner.
1957#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1958#[serde(rename_all = "camelCase")]
1959pub struct ProcessOutput {
1960    /// Exit status code.
1961    pub status: i32,
1962    /// Captured stdout.
1963    pub stdout: String,
1964    /// Captured stderr.
1965    pub stderr: String,
1966}
1967
1968/// Request for CI matrix generation.
1969#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1970#[serde(rename_all = "camelCase")]
1971pub struct CiMatrixRequest {
1972    /// Optional starting path or explicit repo root.
1973    pub repo: Option<PathBuf>,
1974    /// Requested task names.
1975    pub tasks: Vec<TaskName>,
1976    /// Changed files used for affected CI matrix generation.
1977    pub changed_files: Vec<RepoRelativePath>,
1978    /// Optional base git ref.
1979    pub base: Option<String>,
1980    /// Optional head git ref.
1981    pub head: Option<String>,
1982    /// Behavior when affected-file detection cannot select entries.
1983    pub fallback: CiFallback,
1984}
1985
1986/// CI fallback behavior.
1987#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1988#[serde(rename_all = "kebab-case")]
1989pub enum CiFallback {
1990    /// Include all projects with requested tasks.
1991    All,
1992    /// Include no projects.
1993    #[default]
1994    None,
1995    /// Fail when no changed-file signal is available.
1996    Error,
1997}
1998
1999/// CI matrix report.
2000#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2001#[serde(rename_all = "camelCase")]
2002pub struct CiMatrixReport {
2003    /// JSON matrix entries.
2004    pub entries: Vec<serde_json::Value>,
2005    /// GitHub Actions-safe matrix object.
2006    pub github_actions: serde_json::Value,
2007}
2008
2009/// Supported CI workflow provider.
2010#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
2011#[serde(rename_all = "kebab-case")]
2012pub enum CiProvider {
2013    /// GitHub Actions.
2014    #[default]
2015    GitHubActions,
2016}
2017
2018/// Request to render a CI workflow.
2019#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2020#[serde(rename_all = "camelCase")]
2021pub struct CiWorkflowRequest {
2022    /// Optional starting path or explicit repo root.
2023    pub repo: Option<PathBuf>,
2024    /// Workflow provider.
2025    pub provider: CiProvider,
2026    /// Write the generated workflow file.
2027    pub write: bool,
2028}
2029
2030/// Rendered CI workflow.
2031#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2032#[serde(rename_all = "camelCase")]
2033pub struct CiWorkflowReport {
2034    /// Workflow file path.
2035    pub path: RepoRelativePath,
2036    /// Workflow content.
2037    pub content: String,
2038    /// Planned or applied operations.
2039    pub operations: Vec<FileOperation>,
2040    /// Diagnostics.
2041    pub diagnostics: Vec<Diagnostic>,
2042}
2043
2044/// Hygiene request.
2045#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2046#[serde(rename_all = "camelCase")]
2047pub struct HygieneCheckRequest {
2048    /// Optional starting path or explicit repo root.
2049    pub repo: Option<PathBuf>,
2050}
2051
2052/// Hygiene clean request.
2053#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2054#[serde(rename_all = "camelCase")]
2055pub struct HygieneCleanRequest {
2056    /// Optional starting path or explicit repo root.
2057    pub repo: Option<PathBuf>,
2058    /// Plan without deleting files.
2059    pub dry_run: bool,
2060}
2061
2062/// Hygiene report.
2063#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2064#[serde(rename_all = "camelCase")]
2065pub struct HygieneReport {
2066    /// Hygiene diagnostics.
2067    pub diagnostics: Vec<Diagnostic>,
2068    /// Cleanable generated-artifact operations.
2069    pub operations: Vec<FileOperation>,
2070}
2071
2072/// Dependency rewrite mode for adoption planning.
2073#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
2074#[serde(rename_all = "kebab-case")]
2075pub enum DependencyRewriteMode {
2076    /// Rewrite supported dependencies automatically.
2077    #[default]
2078    Auto,
2079    /// Do not inspect or rewrite dependencies.
2080    Off,
2081    /// Report rewrite candidates without changing copied files.
2082    ReportOnly,
2083}
2084
2085/// CI behavior for adoption planning.
2086#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
2087#[serde(rename_all = "kebab-case")]
2088pub enum AdoptionCiMode {
2089    /// Update generated CI files.
2090    #[default]
2091    Update,
2092    /// Do not generate CI changes.
2093    Off,
2094    /// Report CI changes without applying them.
2095    ReportOnly,
2096}
2097
2098/// Adoption output format.
2099#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
2100#[serde(rename_all = "kebab-case")]
2101pub enum AdoptionOutputFormat {
2102    /// Human-readable report.
2103    #[default]
2104    Human,
2105    /// JSON report.
2106    Json,
2107    /// GitHub Actions format.
2108    GitHubActions,
2109}
2110
2111/// Request to create an adoption plan.
2112#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2113#[serde(rename_all = "camelCase")]
2114pub struct AdoptionPlanRequest {
2115    /// Directory containing source repositories or a single source repository.
2116    pub source: PathBuf,
2117    /// Destination initialized monorepo.
2118    pub dest: PathBuf,
2119    /// Source names to include.
2120    pub include: Vec<String>,
2121    /// Source names to exclude.
2122    pub exclude: Vec<String>,
2123    /// Placement overrides keyed by source name.
2124    pub map: BTreeMap<String, RepoRelativePath>,
2125    /// Project-kind overrides keyed by source name.
2126    pub kind: BTreeMap<String, ProjectKind>,
2127    /// Owner overrides keyed by source name.
2128    pub owner: BTreeMap<String, OwnerHandle>,
2129    /// Dependency rewrite mode.
2130    pub rewrite_deps: DependencyRewriteMode,
2131    /// CI generation behavior.
2132    pub ci: AdoptionCiMode,
2133    /// Verification mode.
2134    pub verification: ValidationMode,
2135    /// Requested output format.
2136    pub format: AdoptionOutputFormat,
2137}
2138
2139/// Request to apply a reviewed adoption plan.
2140#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2141#[serde(rename_all = "camelCase")]
2142pub struct AdoptionApplyRequest {
2143    /// Plan JSON file.
2144    pub plan: PathBuf,
2145    /// Recompute inference before applying.
2146    pub refresh: bool,
2147}
2148
2149/// Request to verify a reviewed adoption plan.
2150#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2151#[serde(rename_all = "camelCase")]
2152pub struct AdoptionVerifyRequest {
2153    /// Plan JSON file.
2154    pub plan: PathBuf,
2155}
2156
2157/// Adoption plan.
2158#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
2159#[serde(rename_all = "camelCase")]
2160pub struct AdoptionPlan {
2161    /// Source root.
2162    pub source_root: Utf8PathBuf,
2163    /// Destination repo root.
2164    pub dest_root: Utf8PathBuf,
2165    /// Source repository decisions.
2166    pub sources: Vec<AdoptedSource>,
2167    /// Copy operations.
2168    pub operations: Vec<AdoptionFileOperation>,
2169    /// Dependency rewrite operations.
2170    pub dependency_rewrites: Vec<DependencyRewrite>,
2171    /// Synthesized manifests.
2172    pub manifest_syntheses: Vec<ProjectManifestSynthesis>,
2173    /// CI file operations.
2174    pub ci_operations: Vec<FileOperation>,
2175    /// Verification plan.
2176    pub verification: VerificationPlan,
2177    /// Diagnostics.
2178    pub diagnostics: Vec<Diagnostic>,
2179}
2180
2181/// Source repository selected for adoption.
2182#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
2183#[serde(rename_all = "camelCase")]
2184pub struct AdoptedSource {
2185    /// Source repository name.
2186    pub name: String,
2187    /// Source path.
2188    pub source_path: Utf8PathBuf,
2189    /// Destination path.
2190    pub destination_path: RepoRelativePath,
2191    /// Inferred kind.
2192    pub inferred_kind: ProjectKind,
2193    /// Confidence score from 0.0 to 1.0.
2194    pub confidence: f32,
2195    /// Inference reasons.
2196    pub reasons: Vec<String>,
2197    /// Inventory snapshot.
2198    pub inventory: SourceInventory,
2199    /// Whether this source is skipped.
2200    pub skipped: bool,
2201    /// Whether placement was explicitly overridden.
2202    pub override_applied: bool,
2203}
2204
2205/// Source inventory collected before planning.
2206#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2207#[serde(rename_all = "camelCase")]
2208pub struct SourceInventory {
2209    /// Repository name.
2210    pub name: String,
2211    /// Whether a VCS marker exists.
2212    pub has_vcs: bool,
2213    /// README first heading or summary.
2214    pub readme_summary: Option<String>,
2215    /// Primary manifest paths.
2216    pub manifests: Vec<String>,
2217    /// Top-level directories.
2218    pub top_level_dirs: Vec<String>,
2219    /// Dependency package names referenced by source manifests.
2220    pub dependency_references: Vec<String>,
2221    /// Generated artifact paths found in the source.
2222    pub generated_artifacts: Vec<String>,
2223    /// Required local tools inferred from manifests and build scripts.
2224    pub required_tools: Vec<String>,
2225}
2226
2227/// Adoption file operation.
2228#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2229#[serde(rename_all = "camelCase")]
2230pub struct AdoptionFileOperation {
2231    /// Operation id.
2232    pub id: String,
2233    /// Operation kind.
2234    pub operation: String,
2235    /// Optional source file path.
2236    #[serde(skip_serializing_if = "Option::is_none")]
2237    pub source_path: Option<Utf8PathBuf>,
2238    /// Destination path relative to the destination repo root.
2239    pub destination_path: RepoRelativePath,
2240    /// Optional UTF-8 replacement content.
2241    #[serde(skip_serializing_if = "Option::is_none")]
2242    pub content: Option<String>,
2243    /// File checksum when known.
2244    #[serde(skip_serializing_if = "Option::is_none")]
2245    pub checksum: Option<String>,
2246}
2247
2248/// Dependency rewrite record.
2249#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2250#[serde(rename_all = "camelCase")]
2251pub struct DependencyRewrite {
2252    /// File being rewritten.
2253    pub file: RepoRelativePath,
2254    /// Package or crate dependency name.
2255    pub package: String,
2256    /// Original dependency value.
2257    pub from: String,
2258    /// Replacement dependency value.
2259    pub to: String,
2260    /// Dependency surface.
2261    pub surface: DependencySurface,
2262    /// Owning project.
2263    pub owner_project: ProjectName,
2264}
2265
2266/// Synthesized project manifest record.
2267#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2268#[serde(rename_all = "camelCase")]
2269pub struct ProjectManifestSynthesis {
2270    /// Source repository name.
2271    pub source: String,
2272    /// Project name.
2273    pub project: ProjectName,
2274    /// Manifest path.
2275    pub manifest_path: RepoRelativePath,
2276    /// Synthesized YAML.
2277    pub content: String,
2278}
2279
2280/// Verification plan generated for adoption.
2281#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2282#[serde(rename_all = "camelCase")]
2283pub struct VerificationPlan {
2284    /// Tool prerequisites.
2285    pub prerequisites: Vec<ToolPrerequisite>,
2286    /// Ordered commands.
2287    pub commands: Vec<ProcessCommand>,
2288}
2289
2290/// Required local tool.
2291#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2292#[serde(rename_all = "camelCase")]
2293pub struct ToolPrerequisite {
2294    /// Tool name.
2295    pub tool: String,
2296    /// Why it is needed.
2297    pub reason: String,
2298}
2299
2300/// Request for PR summary.
2301#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2302#[serde(rename_all = "camelCase")]
2303pub struct PrSummaryRequest {
2304    /// Optional starting path or explicit repo root.
2305    pub repo: Option<PathBuf>,
2306    /// Optional base git ref.
2307    pub base: Option<String>,
2308    /// Optional head git ref.
2309    pub head: Option<String>,
2310    /// Explicit changed files.
2311    pub changed_files: Vec<RepoRelativePath>,
2312}
2313
2314/// PR summary report.
2315#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2316#[serde(rename_all = "camelCase")]
2317pub struct PrSummary {
2318    /// Markdown body.
2319    pub markdown: String,
2320    /// Machine-readable impact payload.
2321    pub impact: serde_json::Value,
2322}
2323
2324/// Request for AI context.
2325#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2326#[serde(rename_all = "camelCase")]
2327pub struct AiContextRequest {
2328    /// Optional starting path or explicit repo root.
2329    pub repo: Option<PathBuf>,
2330    /// Project to build context for.
2331    pub project: ProjectName,
2332    /// Context audience.
2333    pub audience: String,
2334}
2335
2336/// AI context report.
2337#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2338#[serde(rename_all = "camelCase")]
2339pub struct AiContext {
2340    /// JSON context payload.
2341    pub payload: serde_json::Value,
2342}
2343
2344/// Generated-code check request.
2345#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2346#[serde(rename_all = "camelCase")]
2347pub struct CodegenCheckRequest {
2348    /// Optional starting path or explicit repo root.
2349    pub repo: Option<PathBuf>,
2350    /// Optional base git ref.
2351    pub base: Option<String>,
2352    /// Optional head git ref.
2353    pub head: Option<String>,
2354    /// Explicit changed files.
2355    pub changed_files: Vec<RepoRelativePath>,
2356}
2357
2358/// Generated-code check report.
2359#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2360#[serde(rename_all = "camelCase")]
2361pub struct CodegenCheckReport {
2362    /// Diagnostics.
2363    pub diagnostics: Vec<Diagnostic>,
2364}
2365
2366/// Proto facade request.
2367#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2368#[serde(rename_all = "camelCase")]
2369pub struct ProtoFacadeRequest {
2370    /// Optional starting path or explicit repo root.
2371    pub repo: Option<PathBuf>,
2372    /// Proto operation.
2373    pub operation: ProtoOperation,
2374    /// Proto path or package selector.
2375    #[serde(skip_serializing_if = "Option::is_none")]
2376    pub selector: Option<String>,
2377    /// Optional base git ref for changed-file checks.
2378    pub base: Option<String>,
2379    /// Optional head git ref for changed-file checks.
2380    pub head: Option<String>,
2381    /// Explicit changed files for generated-code policy checks.
2382    pub changed_files: Vec<RepoRelativePath>,
2383}
2384
2385/// Proto operation.
2386#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2387#[serde(rename_all = "kebab-case")]
2388pub enum ProtoOperation {
2389    /// Check proto toolchain and generated-code policy.
2390    #[default]
2391    Check,
2392    /// Find owners for a proto path.
2393    Owners,
2394    /// Find consumers for a proto path or package.
2395    Consumers,
2396}
2397
2398/// Proto facade report.
2399#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2400#[serde(rename_all = "camelCase")]
2401pub struct ProtoFacadeReport {
2402    /// Matching owner projects.
2403    pub owners: Vec<ProjectName>,
2404    /// Matching consumer projects.
2405    pub consumers: Vec<ProjectName>,
2406    /// Commands planned or executed by proto toolchains.
2407    pub commands: Vec<ProcessCommand>,
2408    /// Diagnostics.
2409    pub diagnostics: Vec<Diagnostic>,
2410}
2411
2412/// `IaC` facade request.
2413#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2414#[serde(rename_all = "camelCase")]
2415pub struct IacFacadeRequest {
2416    /// Optional starting path or explicit repo root.
2417    pub repo: Option<PathBuf>,
2418    /// Plan only affected `IaC` targets.
2419    pub affected: bool,
2420    /// Explicit project selector.
2421    #[serde(skip_serializing_if = "Option::is_none")]
2422    pub project: Option<ProjectName>,
2423    /// Environment or stack name.
2424    #[serde(skip_serializing_if = "Option::is_none")]
2425    pub env: Option<String>,
2426    /// Plan core infrastructure.
2427    pub core: bool,
2428    /// Optional base git ref for affected selection.
2429    pub base: Option<String>,
2430    /// Optional head git ref for affected selection.
2431    pub head: Option<String>,
2432    /// Explicit changed files for affected selection and risk classification.
2433    pub changed_files: Vec<RepoRelativePath>,
2434    /// Plan without executing provider commands.
2435    pub dry_run: bool,
2436}
2437
2438/// `IaC` facade report.
2439#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2440#[serde(rename_all = "camelCase")]
2441pub struct IacFacadeReport {
2442    /// Provider plan commands.
2443    pub commands: Vec<ProcessCommand>,
2444    /// Risk flags.
2445    pub risk_flags: Vec<String>,
2446    /// Diagnostics.
2447    pub diagnostics: Vec<Diagnostic>,
2448}
2449
2450/// Request for an operations plan.
2451#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2452#[serde(rename_all = "camelCase")]
2453pub struct OpsPlanRequest {
2454    /// Optional starting path or explicit repo root.
2455    pub repo: Option<PathBuf>,
2456    /// Optional base git ref.
2457    pub base: Option<String>,
2458    /// Optional head git ref.
2459    pub head: Option<String>,
2460    /// Explicit changed files.
2461    pub changed_files: Vec<RepoRelativePath>,
2462    /// Target environment names.
2463    pub environments: Vec<String>,
2464    /// Requested task names.
2465    pub tasks: Vec<TaskName>,
2466    /// Optional plan output path.
2467    #[serde(skip_serializing_if = "Option::is_none")]
2468    pub output: Option<PathBuf>,
2469}
2470
2471/// Operations plan for day-2 infrastructure work.
2472#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2473#[serde(rename_all = "camelCase")]
2474pub struct OpsPlan {
2475    /// Stable plan id.
2476    pub id: String,
2477    /// Repository root.
2478    #[serde(skip_serializing_if = "Option::is_none")]
2479    pub repo_root: Option<RepoRoot>,
2480    /// Optional base git ref.
2481    #[serde(skip_serializing_if = "Option::is_none")]
2482    pub base: Option<String>,
2483    /// Optional head git ref.
2484    #[serde(skip_serializing_if = "Option::is_none")]
2485    pub head: Option<String>,
2486    /// Planned environments.
2487    pub environments: Vec<String>,
2488    /// Affected report used to build the plan.
2489    pub affected: AffectedReport,
2490    /// Deduplicated task dry-run report.
2491    pub task_plan: TaskRunReport,
2492    /// Ordered `IaC` operations.
2493    pub iac: Vec<IacOperation>,
2494    /// DNS verification operations.
2495    pub dns: Vec<DnsOperation>,
2496    /// CDN checks.
2497    pub cdn: Vec<CdnCheck>,
2498    /// Provider capability diagnostics.
2499    pub provider_capabilities: Vec<ProviderCapabilityReport>,
2500    /// Runtime smoke probes.
2501    pub probes: Vec<ProbeSpec>,
2502    /// Manual-state reconciliation records.
2503    pub manual_reconciliation: Vec<ManualStateRecord>,
2504    /// Required environment variable names.
2505    pub required_env: Vec<String>,
2506    /// Unresolved production gaps.
2507    pub production_gaps: Vec<String>,
2508    /// Diagnostics.
2509    pub diagnostics: Vec<Diagnostic>,
2510}
2511
2512/// One `IaC` operation in an operations plan.
2513#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2514#[serde(rename_all = "camelCase")]
2515pub struct IacOperation {
2516    /// Project id when the operation belongs to a project.
2517    #[serde(skip_serializing_if = "Option::is_none")]
2518    pub project: Option<ProjectName>,
2519    /// `IaC` workspace or root.
2520    pub workspace: String,
2521    /// `IaC` provider.
2522    pub provider: IacProvider,
2523    /// Environment or stack name.
2524    pub environment: String,
2525    /// Stack name.
2526    pub stack: String,
2527    /// Non-mutating preview command.
2528    pub preview_command: ProcessCommand,
2529    /// Mutating apply command, gated and not run by default.
2530    #[serde(skip_serializing_if = "Option::is_none")]
2531    pub apply_command: Option<ProcessCommand>,
2532    /// Risk flags.
2533    pub risk: Vec<String>,
2534}
2535
2536/// DNS verification operation.
2537#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2538#[serde(rename_all = "camelCase")]
2539pub struct DnsOperation {
2540    /// Zone name inferred from the record.
2541    pub zone: String,
2542    /// DNS provider.
2543    pub provider: String,
2544    /// Record name.
2545    pub record: String,
2546    /// Expected target.
2547    pub expected_target: String,
2548    /// Expected proxied setting.
2549    #[serde(skip_serializing_if = "Option::is_none")]
2550    pub expected_proxied: Option<bool>,
2551    /// Verification commands.
2552    pub verification: Vec<ProcessCommand>,
2553}
2554
2555/// CDN serving-layer check.
2556#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2557#[serde(rename_all = "camelCase")]
2558pub struct CdnCheck {
2559    /// CDN provider.
2560    pub provider: String,
2561    /// Alias checked.
2562    pub alias: String,
2563    /// Header patterns that should prove serving layer.
2564    pub expected_response_headers: Vec<String>,
2565    /// Verification command.
2566    pub verification: ProcessCommand,
2567}
2568
2569/// Request for non-mutating operations verification.
2570#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2571#[serde(rename_all = "camelCase")]
2572pub struct OpsVerifyRequest {
2573    /// Optional starting path or explicit repo root.
2574    pub repo: Option<PathBuf>,
2575    /// Plan JSON path.
2576    pub plan: PathBuf,
2577}
2578
2579/// Operations verification report.
2580#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2581#[serde(rename_all = "camelCase")]
2582pub struct OpsVerifyReport {
2583    /// Commands that are safe to run for verification.
2584    pub commands: Vec<ProcessCommand>,
2585    /// Mutating commands intentionally skipped.
2586    pub skipped_mutating_commands: Vec<ProcessCommand>,
2587    /// Diagnostics.
2588    pub diagnostics: Vec<Diagnostic>,
2589}
2590
2591/// Request for manual-state reconciliation.
2592#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2593#[serde(rename_all = "camelCase")]
2594pub struct OpsReconcileRequest {
2595    /// Optional starting path or explicit repo root.
2596    pub repo: Option<PathBuf>,
2597    /// Plan JSON path.
2598    pub plan: PathBuf,
2599}
2600
2601/// Manual-state reconciliation report.
2602#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2603#[serde(rename_all = "camelCase")]
2604pub struct OpsReconcileReport {
2605    /// Manual state records from the plan.
2606    pub records: Vec<ManualStateRecord>,
2607    /// Cleanup commands for unreconciled temporary state.
2608    pub cleanup_commands: Vec<ProcessCommand>,
2609    /// Diagnostics.
2610    pub diagnostics: Vec<Diagnostic>,
2611}
2612
2613/// Request for provider capability diagnostics.
2614#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2615#[serde(rename_all = "camelCase")]
2616pub struct ProviderCapabilityRequest {
2617    /// Optional starting path or explicit repo root.
2618    pub repo: Option<PathBuf>,
2619    /// Workspace selector in `project:workspace` form or repo-relative path form.
2620    #[serde(skip_serializing_if = "Option::is_none")]
2621    pub workspace: Option<String>,
2622    /// Optional base git ref.
2623    pub base: Option<String>,
2624    /// Optional head git ref.
2625    pub head: Option<String>,
2626    /// Explicit changed files.
2627    pub changed_files: Vec<RepoRelativePath>,
2628}
2629
2630/// Provider capability diagnostic.
2631#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2632#[serde(rename_all = "camelCase")]
2633pub struct ProviderCapabilityReport {
2634    /// Workspace selector or path.
2635    pub workspace: String,
2636    /// Provider package name.
2637    pub package: String,
2638    /// Installed or declared package version.
2639    pub version: String,
2640    /// Resource with a known capability gap.
2641    pub resource: String,
2642    /// Field with a known capability gap.
2643    pub field: String,
2644    /// Diagnostic status.
2645    pub status: String,
2646    /// Operator advice.
2647    pub advice: String,
2648    /// Diagnostics.
2649    pub diagnostics: Vec<Diagnostic>,
2650}
2651
2652/// Local operations session journal.
2653#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2654#[serde(rename_all = "camelCase")]
2655pub struct SessionJournal {
2656    /// Session id.
2657    pub id: String,
2658    /// Human-readable session name.
2659    pub name: String,
2660    /// Associated operations plan id.
2661    #[serde(skip_serializing_if = "Option::is_none")]
2662    pub plan_id: Option<String>,
2663    /// Journal entries.
2664    pub entries: Vec<SessionEntry>,
2665}
2666
2667/// One operations session entry.
2668#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2669#[serde(rename_all = "camelCase")]
2670pub struct SessionEntry {
2671    /// Entry kind.
2672    pub kind: String,
2673    /// Unix timestamp in seconds.
2674    pub timestamp: u64,
2675    /// Redacted command text when applicable.
2676    #[serde(skip_serializing_if = "Option::is_none")]
2677    pub command: Option<String>,
2678    /// Exit status when applicable.
2679    #[serde(skip_serializing_if = "Option::is_none")]
2680    pub exit_status: Option<i32>,
2681    /// Redacted message or selected evidence.
2682    #[serde(skip_serializing_if = "Option::is_none")]
2683    pub message: Option<String>,
2684    /// Associated plan id.
2685    #[serde(skip_serializing_if = "Option::is_none")]
2686    pub plan_id: Option<String>,
2687}
2688
2689/// Journal command request.
2690#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2691#[serde(rename_all = "camelCase")]
2692pub struct OpsJournalRequest {
2693    /// Optional starting path or explicit repo root.
2694    pub repo: Option<PathBuf>,
2695    /// Journal action.
2696    pub action: OpsJournalAction,
2697}
2698
2699/// Journal action.
2700#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2701#[serde(rename_all = "camelCase", tag = "kind")]
2702pub enum OpsJournalAction {
2703    /// Start a journal.
2704    Start {
2705        /// Session name.
2706        name: String,
2707        /// Optional plan id.
2708        plan_id: Option<String>,
2709    },
2710    /// Add a command entry.
2711    AddCommand {
2712        /// Session id or name.
2713        session: String,
2714        /// Command text.
2715        command: String,
2716        /// Optional exit status.
2717        exit_status: Option<i32>,
2718    },
2719    /// Add a note entry.
2720    AddNote {
2721        /// Session id or name.
2722        session: String,
2723        /// Note kind.
2724        note_kind: String,
2725        /// Note message.
2726        message: String,
2727    },
2728    /// Render a summary.
2729    Summary {
2730        /// Session id or name.
2731        session: String,
2732    },
2733}
2734
2735/// Journal command report.
2736#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2737#[serde(rename_all = "camelCase")]
2738pub struct OpsJournalReport {
2739    /// Journal path.
2740    #[serde(skip_serializing_if = "Option::is_none")]
2741    pub path: Option<PathBuf>,
2742    /// Current journal.
2743    #[serde(skip_serializing_if = "Option::is_none")]
2744    pub journal: Option<SessionJournal>,
2745    /// Markdown summary when requested.
2746    #[serde(skip_serializing_if = "Option::is_none")]
2747    pub markdown: Option<String>,
2748    /// Diagnostics.
2749    pub diagnostics: Vec<Diagnostic>,
2750}
2751
2752/// Skills facade request.
2753#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2754#[serde(rename_all = "camelCase")]
2755pub struct SkillsFacadeRequest {
2756    /// Optional starting path or explicit repo root.
2757    pub repo: Option<PathBuf>,
2758    /// Whether to write missing or stale skills.
2759    pub sync: bool,
2760    /// Whether to only plan writes.
2761    pub dry_run: bool,
2762}
2763
2764/// Skills facade report.
2765#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2766#[serde(rename_all = "camelCase")]
2767pub struct SkillsFacadeReport {
2768    /// Diagnostics.
2769    pub diagnostics: Vec<Diagnostic>,
2770}
2771
2772/// Validates project path conventions for the supported kinds.
2773pub fn validate_project_convention(project: &ProjectManifest) -> Vec<Diagnostic> {
2774    let expected_prefix = match project.kind {
2775        ProjectKind::App => "apps/",
2776        ProjectKind::Framework => "frameworks/",
2777        ProjectKind::FoundationService => "foundations/",
2778        ProjectKind::ProtoRoot => "protos",
2779        ProjectKind::CoreInfra => "core-infra",
2780        ProjectKind::CoreInfraComponent => "core-infra/",
2781        ProjectKind::Tool => "tools/",
2782    };
2783    let valid = match project.kind {
2784        ProjectKind::ProtoRoot | ProjectKind::CoreInfra => project.path.as_str() == expected_prefix,
2785        ProjectKind::App
2786        | ProjectKind::Framework
2787        | ProjectKind::FoundationService
2788        | ProjectKind::CoreInfraComponent
2789        | ProjectKind::Tool => project.path.as_str().starts_with(expected_prefix),
2790    };
2791    if valid {
2792        Vec::new()
2793    } else {
2794        vec![
2795            Diagnostic::error(
2796                "manifest.project.path_convention",
2797                format!(
2798                    "project `{}` of kind {:?} must live under `{expected_prefix}`",
2799                    project.name, project.kind
2800                ),
2801            )
2802            .with_path(project.source.as_str()),
2803        ]
2804    }
2805}
2806
2807/// Converts a repo path to a UTF-8 path.
2808pub fn utf8_path_buf(path: PathBuf) -> Result<Utf8PathBuf, Diagnostic> {
2809    Utf8PathBuf::from_path_buf(path).map_err(|path| {
2810        Diagnostic::error(
2811            "path.non_utf8",
2812            format!("path is not valid UTF-8: {}", path.display()),
2813        )
2814    })
2815}
2816
2817fn validate_ascii_identifier(
2818    label: &str,
2819    value: &str,
2820    max_bytes: usize,
2821    allow_dot: bool,
2822) -> Result<(), Diagnostic> {
2823    if value.is_empty() || value.len() > max_bytes {
2824        return Err(Diagnostic::error(
2825            "manifest.identifier.invalid",
2826            format!("{label} must be non-empty and length-bounded"),
2827        ));
2828    }
2829    let mut previous_dot = false;
2830    for byte in value.bytes() {
2831        let is_dot = byte == b'.';
2832        let allowed = byte.is_ascii_lowercase()
2833            || byte.is_ascii_digit()
2834            || matches!(byte, b'-' | b'_')
2835            || (allow_dot && is_dot);
2836        if !allowed {
2837            return Err(Diagnostic::error(
2838                "manifest.identifier.invalid",
2839                format!("{label} contains unsupported characters"),
2840            ));
2841        }
2842        if allow_dot && is_dot && previous_dot {
2843            return Err(Diagnostic::error(
2844                "manifest.identifier.invalid",
2845                format!("{label} cannot contain consecutive dots"),
2846            ));
2847        }
2848        previous_dot = is_dot;
2849    }
2850    if allow_dot && (value.starts_with('.') || value.ends_with('.')) {
2851        return Err(Diagnostic::error(
2852            "manifest.identifier.invalid",
2853            format!("{label} cannot start or end with a dot"),
2854        ));
2855    }
2856    Ok(())
2857}
2858
2859fn normalize_relative_path(value: String, allow_root: bool) -> Result<String, Diagnostic> {
2860    validate_path_text(&value, false)?;
2861    if value == "." {
2862        return if allow_root {
2863            Ok(value)
2864        } else {
2865            Err(Diagnostic::error(
2866                "manifest.path.invalid",
2867                "repo-relative path cannot be the root path",
2868            ))
2869        };
2870    }
2871    let mut parts = Vec::new();
2872    for part in value.split('/') {
2873        if part.is_empty() || part == "." {
2874            continue;
2875        }
2876        if part == ".." {
2877            return Err(Diagnostic::error(
2878                "manifest.path.traversal",
2879                "relative path cannot contain ..",
2880            ));
2881        }
2882        parts.push(part);
2883    }
2884    if parts.is_empty() {
2885        return if allow_root {
2886            Ok(".".to_string())
2887        } else {
2888            Err(Diagnostic::error(
2889                "manifest.path.invalid",
2890                "relative path must not be empty",
2891            ))
2892        };
2893    }
2894    Ok(parts.join("/"))
2895}
2896
2897fn validate_path_text(value: &str, allow_glob: bool) -> Result<(), Diagnostic> {
2898    if value.is_empty() || value.len() > PATH_MAX_BYTES {
2899        return Err(Diagnostic::error(
2900            "manifest.path.invalid",
2901            "path must be non-empty and length-bounded",
2902        ));
2903    }
2904    if value.contains('\0') || value.contains('\\') {
2905        return Err(Diagnostic::error(
2906            "manifest.path.invalid",
2907            "path must not contain NUL bytes or platform separators",
2908        ));
2909    }
2910    if value.starts_with('/') {
2911        return Err(Diagnostic::error(
2912            "manifest.path.absolute",
2913            "path must be relative",
2914        ));
2915    }
2916    if value
2917        .split('/')
2918        .any(|part| part == ".." || (!allow_glob && part.contains('*')))
2919    {
2920        return Err(Diagnostic::error(
2921            "manifest.path.traversal",
2922            "path must not contain traversal or unsupported glob segments",
2923        ));
2924    }
2925    Ok(())
2926}
2927
2928fn reject_shell_syntax(value: &str) -> Result<(), Diagnostic> {
2929    const REJECTED: [&str; 13] = [
2930        "&&", "||", "|", ";", ">", "<", "$(", "`", "\n", "\r", "*", "?", "[",
2931    ];
2932    if let Some(token) = REJECTED.iter().find(|token| value.contains(**token)) {
2933        return Err(Diagnostic::error(
2934            "manifest.command.shell_syntax",
2935            format!("command uses shell-only syntax `{token}`"),
2936        ));
2937    }
2938    Ok(())
2939}
2940
2941fn split_command(value: &str) -> Result<Vec<String>, Diagnostic> {
2942    let mut parts = Vec::new();
2943    let mut current = String::new();
2944    let mut quote: Option<char> = None;
2945    let mut escaped = false;
2946    for character in value.chars() {
2947        if escaped {
2948            current.push(character);
2949            escaped = false;
2950            continue;
2951        }
2952        if character == '\\' {
2953            escaped = true;
2954            continue;
2955        }
2956        match quote {
2957            Some(active) if character == active => quote = None,
2958            None if character == '\'' || character == '"' => quote = Some(character),
2959            None if character.is_whitespace() => {
2960                if !current.is_empty() {
2961                    parts.push(std::mem::take(&mut current));
2962                }
2963            }
2964            Some(_) | None => current.push(character),
2965        }
2966    }
2967    if escaped || quote.is_some() {
2968        return Err(Diagnostic::error(
2969            "manifest.command.invalid",
2970            "command contains an unfinished escape or quote",
2971        ));
2972    }
2973    if !current.is_empty() {
2974        parts.push(current);
2975    }
2976    Ok(parts)
2977}
2978
2979fn nonzero_u32(value: u32) -> NonZeroU32 {
2980    NonZeroU32::new(value).unwrap_or(NonZeroU32::MIN)
2981}
2982
2983fn nonzero_usize(value: usize) -> NonZeroUsize {
2984    NonZeroUsize::new(value).unwrap_or(NonZeroUsize::MIN)
2985}
2986
2987fn default_code_size_languages() -> BTreeMap<CodeLanguage, CodeLanguageConfig> {
2988    [
2989        (CodeLanguage::Rust, CodeLanguageConfig::default()),
2990        (CodeLanguage::TypeScript, CodeLanguageConfig::default()),
2991        (CodeLanguage::Python, CodeLanguageConfig::default()),
2992    ]
2993    .into_iter()
2994    .collect()
2995}
2996
2997fn default_code_size_excludes() -> Vec<RepoGlob> {
2998    [
2999        "**/target/**",
3000        "**/node_modules/**",
3001        "**/dist/**",
3002        "**/.next/**",
3003    ]
3004    .into_iter()
3005    .filter_map(|pattern| RepoGlob::new(pattern).ok())
3006    .collect()
3007}
3008
3009#[cfg(test)]
3010mod tests {
3011    use super::{
3012        CommandSpec, OwnerHandle, ProjectDependency, ProjectRelativePath, RepoRelativePath,
3013    };
3014
3015    #[test]
3016    fn test_should_parse_argv_command() {
3017        let command = CommandSpec::parse("cargo check --workspace").expect("command parses");
3018        assert_eq!(command.program, "cargo");
3019        assert_eq!(command.args, ["check", "--workspace"]);
3020    }
3021
3022    #[test]
3023    fn test_should_reject_shell_syntax_in_command() {
3024        let error = CommandSpec::parse("cargo check && rm -rf target").expect_err("rejects shell");
3025        assert_eq!(error.code.as_ref(), "manifest.command.shell_syntax");
3026    }
3027
3028    #[test]
3029    fn test_should_reject_invalid_owner() {
3030        let error = OwnerHandle::new("platform").expect_err("owner requires @");
3031        assert_eq!(error.code.as_ref(), "manifest.owner.invalid");
3032    }
3033
3034    #[test]
3035    fn test_should_reject_path_traversal() {
3036        let error = RepoRelativePath::new("../outside").expect_err("rejects traversal");
3037        assert_eq!(error.code.as_ref(), "manifest.path.traversal");
3038    }
3039
3040    #[test]
3041    fn test_should_allow_project_root_path() {
3042        let path = ProjectRelativePath::new(".").expect("root is valid");
3043        assert_eq!(path.as_str(), ".");
3044    }
3045
3046    #[test]
3047    fn test_should_parse_foundation_client_dependency() {
3048        let dependency =
3049            ProjectDependency::parse("foundations.identity.client").expect("dependency parses");
3050        assert_eq!(dependency.id, "foundations.identity.client");
3051    }
3052}