1use 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
28#[serde(rename_all = "kebab-case")]
29pub enum RepoLayout {
30 Functional,
32}
33
34#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
36#[serde(transparent)]
37pub struct RepoName(String);
38
39impl RepoName {
40 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
61#[serde(transparent)]
62pub struct ProjectName(String);
63
64impl ProjectName {
65 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
86#[serde(transparent)]
87pub struct WorkspaceName(String);
88
89impl WorkspaceName {
90 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
111#[serde(transparent)]
112pub struct OwnerHandle(String);
113
114impl OwnerHandle {
115 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
142#[serde(transparent)]
143pub struct TaskName(String);
144
145impl TaskName {
146 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
167#[serde(transparent)]
168pub struct SchemaId(String);
169
170impl SchemaId {
171 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
206#[serde(transparent)]
207pub struct ProtoPackageName(String);
208
209impl ProtoPackageName {
210 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
231#[serde(transparent)]
232pub struct RepoRelativePath(String);
233
234impl RepoRelativePath {
235 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
237 normalize_relative_path(value.into(), false).map(Self)
238 }
239
240 pub fn root() -> Self {
242 Self(".".to_string())
243 }
244
245 pub fn as_str(&self) -> &str {
247 &self.0
248 }
249
250 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
283#[serde(transparent)]
284pub struct ProjectRelativePath(String);
285
286impl ProjectRelativePath {
287 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
289 normalize_relative_path(value.into(), true).map(Self)
290 }
291
292 pub fn root() -> Self {
294 Self(".".to_string())
295 }
296
297 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
311#[serde(transparent)]
312pub struct RepoGlob(String);
313
314impl RepoGlob {
315 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 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
342#[serde(rename_all = "camelCase")]
343pub struct RepoRoot {
344 pub absolute: Utf8PathBuf,
346}
347
348impl RepoRoot {
349 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
372#[serde(rename_all = "kebab-case")]
373pub enum ProjectKind {
374 App,
376 Framework,
378 FoundationService,
380 ProtoRoot,
382 CoreInfra,
384 CoreInfraComponent,
386 Tool,
388}
389
390#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
392#[serde(rename_all = "kebab-case")]
393pub enum Toolchain {
394 Cargo,
396 Npm,
398 Pnpm,
400 Yarn,
402 Bun,
404 Uv,
406 Custom(String),
408}
409
410impl Toolchain {
411 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 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#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
455#[serde(rename_all = "kebab-case")]
456pub enum WorkspaceLanguage {
457 Rust,
459 #[serde(rename = "typescript")]
461 TypeScript,
462 Python,
464 Proto,
466 Iac,
468}
469
470#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
472#[serde(rename_all = "kebab-case")]
473pub enum Visibility {
474 #[default]
476 Internal,
477 Public,
479}
480
481#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
483#[serde(rename_all = "kebab-case")]
484pub enum IacProvider {
485 Pulumi,
487 Terraform,
489 #[serde(rename = "opentofu")]
491 OpenTofu,
492}
493
494#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
496#[serde(rename_all = "kebab-case")]
497pub enum GeneratedCodePolicy {
498 #[default]
500 ConsumerLocal,
501}
502
503#[derive(
505 Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
506)]
507#[serde(rename_all = "kebab-case")]
508pub enum CodeSizeScope {
509 #[default]
511 All,
512 Changed,
514 Affected,
516}
517
518#[derive(
520 Clone, Copy, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
521)]
522#[serde(rename_all = "kebab-case")]
523pub enum CodeLanguage {
524 Rust,
526 #[serde(rename = "typescript")]
528 TypeScript,
529 Python,
531}
532
533#[derive(
535 Clone, Copy, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
536)]
537#[serde(rename_all = "kebab-case")]
538pub enum CodeSizeRuleKind {
539 File,
541 Function,
543 Block,
545}
546
547#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
549#[serde(rename_all = "kebab-case")]
550pub enum InspectionFailOn {
551 #[default]
553 Never,
554 Error,
556 Warning,
558}
559
560#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
562#[serde(rename_all = "kebab-case")]
563pub enum GeneratedCodeInspectionMode {
564 #[default]
566 Skip,
567 Inspect,
569}
570
571#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
573#[serde(rename_all = "camelCase")]
574pub struct CodeSizeInspectionRequest {
575 pub repo: Option<PathBuf>,
577 pub scope: CodeSizeScope,
579 pub base: Option<String>,
581 pub head: Option<String>,
583 pub changed_files: Vec<RepoRelativePath>,
585 pub include_transitive: bool,
587 pub languages: Vec<CodeLanguage>,
589 pub rules: Vec<CodeSizeRuleKind>,
591 pub fail_on: InspectionFailOn,
593}
594
595#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
597#[serde(rename_all = "camelCase")]
598pub struct CodeSizeInspectionReport {
599 pub scope: CodeSizeScope,
601 #[serde(skip_serializing_if = "Option::is_none")]
603 pub base: Option<String>,
604 #[serde(skip_serializing_if = "Option::is_none")]
606 pub head: Option<String>,
607 pub summary: CodeSizeInspectionSummary,
609 pub config: CodeSizeResolvedConfigSummary,
611 pub findings: Vec<CodeSizeFinding>,
613 pub diagnostics: Vec<Diagnostic>,
615 pub skipped: Vec<CodeSizeSkippedReason>,
617}
618
619#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
621#[serde(rename_all = "camelCase")]
622pub struct CodeSizeInspectionSummary {
623 pub files_considered: u64,
625 pub files_scanned: u64,
627 pub files_skipped: u64,
629 pub files_errored: u64,
631 pub finding_count: u64,
633 pub files_with_findings: u64,
635 pub duration_millis: u64,
637}
638
639#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
641#[serde(rename_all = "camelCase")]
642pub struct CodeSizeResolvedConfigSummary {
643 pub enabled: bool,
645 pub generated_code: GeneratedCodeInspectionMode,
647 pub max_files: NonZeroUsize,
649 pub max_file_bytes: NonZeroUsize,
651 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
670#[serde(rename_all = "camelCase")]
671pub struct CodeSizeFinding {
672 pub rule: CodeSizeRuleKind,
674 pub severity: Severity,
676 pub path: RepoRelativePath,
678 #[serde(skip_serializing_if = "Option::is_none")]
680 pub project: Option<ProjectName>,
681 pub language: CodeLanguage,
683 #[serde(skip_serializing_if = "Option::is_none")]
685 pub symbol: Option<String>,
686 #[serde(skip_serializing_if = "Option::is_none")]
688 pub node_kind: Option<String>,
689 pub start_line: NonZeroU32,
691 pub end_line: NonZeroU32,
693 pub measured_lines: NonZeroU32,
695 #[serde(skip_serializing_if = "Option::is_none")]
697 pub physical_lines: Option<NonZeroU32>,
698 pub limit: NonZeroU32,
700 pub message: String,
702}
703
704#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
706#[serde(rename_all = "camelCase")]
707pub struct CodeSizeSkippedReason {
708 pub reason: String,
710 pub count: u64,
712}
713
714#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
716#[serde(rename_all = "camelCase")]
717pub struct RepoInspectionConfig {
718 pub code_size: CodeSizeConfig,
720}
721
722#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
724#[serde(rename_all = "camelCase")]
725pub struct CodeSizeConfig {
726 pub enabled: bool,
728 pub generated_code: GeneratedCodeInspectionMode,
730 pub max_files: NonZeroUsize,
732 pub max_file_bytes: NonZeroUsize,
734 pub rules: CodeSizeRuleConfigSet,
736 pub languages: BTreeMap<CodeLanguage, CodeLanguageConfig>,
738 pub excludes: Vec<RepoGlob>,
740 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
761#[serde(rename_all = "camelCase")]
762pub struct CodeLanguageConfig {
763 pub enabled: bool,
765}
766
767impl Default for CodeLanguageConfig {
768 fn default() -> Self {
769 Self { enabled: true }
770 }
771}
772
773#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
775#[serde(rename_all = "camelCase")]
776pub struct CodeSizeRuleConfigSet {
777 pub file: CodeSizeRuleConfig,
779 pub function: CodeSizeRuleConfig,
781 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 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 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
832#[serde(rename_all = "camelCase")]
833pub struct CodeSizeRuleConfig {
834 pub enabled: bool,
836 pub max_lines: NonZeroU32,
838 pub severity: Severity,
840 pub include_tests: bool,
842}
843
844#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
846#[serde(rename_all = "camelCase")]
847pub struct CodeSizeOverride {
848 pub paths: Vec<RepoGlob>,
850 pub rules: CodeSizeRuleConfigPatchSet,
852 pub reason: String,
854}
855
856#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
858#[serde(rename_all = "camelCase")]
859pub struct CodeSizeRuleConfigPatchSet {
860 #[serde(skip_serializing_if = "Option::is_none")]
862 pub file: Option<CodeSizeRuleConfigPatch>,
863 #[serde(skip_serializing_if = "Option::is_none")]
865 pub function: Option<CodeSizeRuleConfigPatch>,
866 #[serde(skip_serializing_if = "Option::is_none")]
868 pub block: Option<CodeSizeRuleConfigPatch>,
869}
870
871impl CodeSizeRuleConfigPatchSet {
872 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#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
884#[serde(rename_all = "camelCase")]
885pub struct CodeSizeRuleConfigPatch {
886 #[serde(skip_serializing_if = "Option::is_none")]
888 pub enabled: Option<bool>,
889 #[serde(skip_serializing_if = "Option::is_none")]
891 pub max_lines: Option<NonZeroU32>,
892 #[serde(skip_serializing_if = "Option::is_none")]
894 pub severity: Option<Severity>,
895 #[serde(skip_serializing_if = "Option::is_none")]
897 pub include_tests: Option<bool>,
898}
899
900#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
902#[serde(rename_all = "camelCase")]
903pub struct RepoManifest {
904 pub schema: SchemaId,
906 pub name: RepoName,
908 pub layout: RepoLayout,
910 pub default_owner: Option<OwnerHandle>,
912 pub protos_root: RepoRelativePath,
914 pub core_infra_root: RepoRelativePath,
916 pub agent_skills_root: RepoRelativePath,
918 pub claude_skills_root: RepoRelativePath,
920 pub context_output: RepoRelativePath,
922 pub generated_code_policy: GeneratedCodePolicy,
924 pub policies: RepoPolicySet,
926 #[serde(default)]
928 pub inspection: RepoInspectionConfig,
929}
930
931#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
933#[serde(rename_all = "camelCase")]
934pub struct RepoPolicySet {
935 pub cross_app_dependency: PolicyMode,
937 pub framework_internal_dependency: PolicyMode,
939 pub generated_code_direct_edit: PolicyMode,
941 pub prod_change_required_owners: Vec<OwnerHandle>,
943}
944
945#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
947#[serde(rename_all = "kebab-case")]
948pub enum PolicyMode {
949 #[default]
951 Deny,
952 Warn,
954 Allow,
956}
957
958#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
960#[serde(rename_all = "camelCase")]
961pub struct WorkspaceSpec {
962 pub name: WorkspaceName,
964 pub language: WorkspaceLanguage,
966 pub toolchain: Option<Toolchain>,
968 pub root: ProjectRelativePath,
970 pub manifest: ProjectRelativePath,
972 pub lockfile: Option<ProjectRelativePath>,
974 pub target_dir: Option<RepoRelativePath>,
976 pub cache_dir: Option<RepoRelativePath>,
978}
979
980#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
982#[serde(rename_all = "camelCase")]
983pub struct TaskCommand {
984 pub workspace: WorkspaceName,
986 pub command: CommandSpec,
988 pub depends_on: Vec<TaskDependency>,
990}
991
992#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
994#[serde(rename_all = "camelCase")]
995pub struct TaskDependency {
996 pub project: ProjectName,
998 pub workspace: WorkspaceName,
1000 pub task: TaskName,
1002}
1003
1004#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1006#[serde(rename_all = "camelCase")]
1007pub struct CommandSpec {
1008 pub program: String,
1010 pub args: Vec<String>,
1012}
1013
1014impl CommandSpec {
1015 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1058#[serde(rename_all = "kebab-case")]
1059pub enum DependencySurface {
1060 Unspecified,
1062 FrameworkFacade,
1064 FrameworkInternal,
1066 FoundationPublicClient,
1068 FoundationInternal,
1070 CoreInfraPublicModule,
1072 CoreInfraInternalModule,
1074}
1075
1076#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1078#[serde(rename_all = "camelCase")]
1079pub enum DependencyTarget {
1080 Project(ProjectName),
1082 ProtoPackage(ProtoPackageName),
1084}
1085
1086#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1088#[serde(rename_all = "camelCase")]
1089pub struct ProjectDependency {
1090 pub id: String,
1092 pub target: DependencyTarget,
1094 pub surface: DependencySurface,
1096}
1097
1098impl ProjectDependency {
1099 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#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1144#[serde(rename_all = "camelCase")]
1145pub struct ProjectProtoSpec {
1146 pub owns: Vec<RepoGlob>,
1148 pub consumes: Vec<RepoGlob>,
1150}
1151
1152#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1154#[serde(rename_all = "camelCase")]
1155pub struct IacSpec {
1156 pub root: ProjectRelativePath,
1158 pub provider: IacProvider,
1160 pub stacks: Vec<String>,
1162}
1163
1164#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1166#[serde(rename_all = "camelCase")]
1167pub struct DeploySpec {
1168 pub root: ProjectRelativePath,
1170 pub environments: Vec<String>,
1172}
1173
1174#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1176#[serde(rename_all = "camelCase")]
1177pub struct ProjectDnsSpec {
1178 #[serde(skip_serializing_if = "Option::is_none")]
1180 pub provider: Option<String>,
1181 pub records: Vec<DnsRecordSpec>,
1183}
1184
1185#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1187#[serde(rename_all = "camelCase")]
1188pub struct DnsRecordSpec {
1189 pub name: String,
1191 pub record_type: String,
1193 pub target: String,
1195 #[serde(skip_serializing_if = "Option::is_none")]
1197 pub proxied: Option<bool>,
1198 #[serde(skip_serializing_if = "Option::is_none")]
1200 pub ttl: Option<u32>,
1201}
1202
1203#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1205#[serde(rename_all = "camelCase")]
1206pub struct CdnSpec {
1207 pub provider: String,
1209 pub aliases: Vec<String>,
1211 pub expected_response_headers: Vec<String>,
1213}
1214
1215#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1217#[serde(rename_all = "camelCase")]
1218pub struct ProjectOpsSpec {
1219 pub probes: Vec<ProbeSpec>,
1221 pub runtime_dependencies: Vec<RuntimeDependencySpec>,
1223 pub manual_state: Vec<ManualStateRecord>,
1225}
1226
1227#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1229#[serde(rename_all = "camelCase")]
1230pub struct ProbeSpec {
1231 pub name: String,
1233 pub method: String,
1235 pub url: String,
1237 pub expect: ProbeExpectation,
1239 #[serde(skip_serializing_if = "Option::is_none")]
1241 pub classification: Option<String>,
1242}
1243
1244#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1246#[serde(rename_all = "camelCase")]
1247pub struct ProbeExpectation {
1248 #[serde(skip_serializing_if = "Option::is_none")]
1250 pub status: Option<u16>,
1251 pub headers: BTreeMap<String, String>,
1253 #[serde(skip_serializing_if = "Option::is_none")]
1255 pub body_contains: Option<String>,
1256}
1257
1258#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1260#[serde(rename_all = "camelCase")]
1261pub struct RuntimeDependencySpec {
1262 pub project: ProjectName,
1264 pub endpoint: String,
1266 pub purpose: String,
1268}
1269
1270#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1272#[serde(rename_all = "camelCase")]
1273pub struct ManualStateRecord {
1274 pub kind: String,
1276 pub resource: String,
1278 pub status: String,
1280 #[serde(skip_serializing_if = "Option::is_none")]
1282 pub managed_equivalent: Option<String>,
1283 #[serde(skip_serializing_if = "Option::is_none")]
1285 pub cleanup_command: Option<ProcessCommand>,
1286}
1287
1288#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1290#[serde(rename_all = "camelCase")]
1291pub struct ProjectAiSpec {
1292 pub editable: Vec<RepoGlob>,
1294 pub do_not_edit: Vec<RepoGlob>,
1296 pub docs: Vec<ProjectRelativePath>,
1298}
1299
1300#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1302#[serde(rename_all = "camelCase")]
1303pub struct ProjectAreas {
1304 pub public_facades: BTreeMap<String, Vec<ProjectRelativePath>>,
1306 pub public_clients: BTreeMap<String, Vec<ProjectRelativePath>>,
1308 pub internal: BTreeMap<String, Vec<ProjectRelativePath>>,
1310 pub public_modules: Vec<ProjectRelativePath>,
1312 pub internal_modules: Vec<ProjectRelativePath>,
1314}
1315
1316#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1318#[serde(rename_all = "camelCase")]
1319pub struct ProjectManifest {
1320 pub schema: SchemaId,
1322 pub name: ProjectName,
1324 pub kind: ProjectKind,
1326 pub path: RepoRelativePath,
1328 pub owners: Vec<OwnerHandle>,
1330 pub visibility: Visibility,
1332 pub workspaces: Vec<WorkspaceSpec>,
1334 pub depends_on: Vec<ProjectDependency>,
1336 pub tasks: BTreeMap<TaskName, Vec<TaskCommand>>,
1338 pub iac: Option<IacSpec>,
1340 pub deploy: Option<DeploySpec>,
1342 pub dns: ProjectDnsSpec,
1344 pub cdn: Option<CdnSpec>,
1346 pub ops: ProjectOpsSpec,
1348 pub protos: ProjectProtoSpec,
1350 pub ai: ProjectAiSpec,
1352 pub areas: ProjectAreas,
1354 pub policies: BTreeMap<String, serde_json::Value>,
1356 pub source: RepoRelativePath,
1358}
1359
1360impl ProjectManifest {
1361 pub fn node_id(&self) -> String {
1363 format!("project:{}", self.name)
1364 }
1365
1366 pub fn contains_path(&self, path: &RepoRelativePath) -> bool {
1368 path.starts_with(&self.path)
1369 }
1370}
1371
1372#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1374#[serde(rename_all = "camelCase")]
1375pub struct TemplateManifest {
1376 pub schema: SchemaId,
1378 pub name: String,
1380 pub kind: String,
1382 pub engine: String,
1384 pub inputs: Vec<TemplateInput>,
1386 pub files: Vec<TemplateFile>,
1388 pub post_render_validate: Vec<CommandSpec>,
1390}
1391
1392#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1394#[serde(rename_all = "camelCase", tag = "kind")]
1395pub enum TemplateSource {
1396 Builtin {
1398 name: String,
1400 },
1401 Local {
1403 root: RepoRelativePath,
1405 },
1406}
1407
1408#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1410#[serde(rename_all = "camelCase")]
1411pub struct ResolvedTemplateSource {
1412 pub root: RepoRelativePath,
1414 pub manifest: TemplateManifest,
1416}
1417
1418#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1420#[serde(rename_all = "camelCase")]
1421pub struct TemplateInput {
1422 pub name: String,
1424 pub input_type: String,
1426 pub required: bool,
1428 pub default: Option<serde_json::Value>,
1430}
1431
1432#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1434#[serde(rename_all = "camelCase")]
1435pub struct TemplateFile {
1436 pub source: ProjectRelativePath,
1438 pub target: String,
1440 pub mode: String,
1442 pub when: Option<String>,
1444}
1445
1446#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1448#[serde(rename_all = "kebab-case")]
1449pub enum GraphNodeKind {
1450 Project,
1452 Workspace,
1454 ProtoPackage,
1456 IacTarget,
1458 Template,
1460}
1461
1462#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1464#[serde(rename_all = "camelCase")]
1465pub struct GraphNode {
1466 pub id: String,
1468 pub kind: GraphNodeKind,
1470 pub label: String,
1472 pub project: Option<ProjectName>,
1474 pub workspace: Option<WorkspaceName>,
1476}
1477
1478#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1480#[serde(rename_all = "kebab-case")]
1481pub enum EdgeKind {
1482 DependsOnProject,
1484 ContainsWorkspace,
1486 ConsumesProto,
1488 OwnsProto,
1490 UsesFrameworkFacade,
1492 UsesFrameworkInternal,
1494 UsesFoundationClient,
1496 UsesFoundationInternal,
1498 UsesCoreInfraModule,
1500 UsesCoreInfraInternalModule,
1502 OwnsIac,
1504 RunsTask,
1506}
1507
1508#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1510#[serde(rename_all = "camelCase")]
1511pub struct GraphEdge {
1512 pub from: String,
1514 pub to: String,
1516 pub kind: EdgeKind,
1518 pub evidence: Option<String>,
1520}
1521
1522#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1524#[serde(rename_all = "camelCase")]
1525pub struct RepoGraph {
1526 pub nodes: Vec<GraphNode>,
1528 pub edges: Vec<GraphEdge>,
1530}
1531
1532impl RepoGraph {
1533 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 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#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1550#[serde(rename_all = "camelCase")]
1551pub struct RepoSnapshot {
1552 pub root: RepoRoot,
1554 pub repo_manifest: RepoManifest,
1556 pub projects: Vec<ProjectManifest>,
1558 pub graph: RepoGraph,
1560 pub generated_policy: GeneratedCodePolicy,
1562 pub discovered_at_unix: u64,
1564}
1565
1566impl RepoSnapshot {
1567 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 pub fn project(&self, name: &ProjectName) -> Option<&ProjectManifest> {
1590 self.projects.iter().find(|project| &project.name == name)
1591 }
1592}
1593
1594#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1596#[serde(rename_all = "camelCase")]
1597pub struct DiscoverRequest {
1598 pub repo: Option<PathBuf>,
1600}
1601
1602#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1604#[serde(rename_all = "camelCase")]
1605pub struct GraphValidateRequest {
1606 pub repo: Option<PathBuf>,
1608 pub changed_files: Vec<RepoRelativePath>,
1610 pub mode: ValidationMode,
1612}
1613
1614#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1616#[serde(rename_all = "kebab-case")]
1617pub enum ValidationMode {
1618 #[default]
1620 Structural,
1621 Metadata,
1623 Full,
1625}
1626
1627#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1629#[serde(rename_all = "camelCase")]
1630pub struct GraphPrintRequest {
1631 pub repo: Option<PathBuf>,
1633}
1634
1635#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
1637#[serde(rename_all = "camelCase")]
1638pub struct GraphPrintReport {
1639 pub snapshot: RepoSnapshot,
1641}
1642
1643#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1645#[serde(rename_all = "camelCase")]
1646pub struct ExplainRequest {
1647 pub repo: Option<PathBuf>,
1649 pub selector: String,
1651}
1652
1653#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1655#[serde(rename_all = "camelCase")]
1656pub struct ExplainReport {
1657 pub selector: String,
1659 pub nodes: Vec<GraphNode>,
1661 pub edges: Vec<GraphEdge>,
1663 pub diagnostics: Vec<Diagnostic>,
1665}
1666
1667#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1669#[serde(rename_all = "camelCase")]
1670pub struct BoundaryLintRequest {
1671 pub repo: Option<PathBuf>,
1673 pub changed_files: Vec<RepoRelativePath>,
1675}
1676
1677#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1679#[serde(rename_all = "camelCase")]
1680pub struct BoundaryLintReport {
1681 pub diagnostics: Vec<Diagnostic>,
1683}
1684
1685#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1687#[serde(rename_all = "kebab-case")]
1688pub enum InitProfile {
1689 Startup,
1691 Enterprise,
1693}
1694
1695#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1697#[serde(rename_all = "camelCase")]
1698pub struct InitRequest {
1699 pub repo_root: PathBuf,
1701 pub name: RepoName,
1703 pub profile: InitProfile,
1705 pub layout: RepoLayout,
1707 pub dry_run: bool,
1709}
1710
1711#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1713#[serde(rename_all = "camelCase")]
1714pub struct FileOperation {
1715 pub path: RepoRelativePath,
1717 pub operation: String,
1719 #[serde(skip_serializing_if = "Option::is_none")]
1721 pub content: Option<String>,
1722}
1723
1724#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1726#[serde(rename_all = "camelCase")]
1727pub struct InitPlan {
1728 pub operations: Vec<FileOperation>,
1730 pub warnings: Vec<Diagnostic>,
1732 pub next_steps: Vec<String>,
1734}
1735
1736#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1738#[serde(rename_all = "camelCase")]
1739pub struct NewProjectRequest {
1740 pub repo: Option<PathBuf>,
1742 pub kind: ProjectKind,
1744 pub path: RepoRelativePath,
1746 pub stack: Vec<String>,
1748 pub languages: Vec<String>,
1750 pub clients: Vec<String>,
1752 pub facade: bool,
1754 pub iac: Option<IacProvider>,
1756 pub proto: Option<ProtoPackageName>,
1758 pub owner: Option<OwnerHandle>,
1760 pub dry_run: bool,
1762}
1763
1764#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1766#[serde(rename_all = "camelCase")]
1767pub struct RenderPlan {
1768 pub operations: Vec<FileOperation>,
1770 pub diagnostics: Vec<Diagnostic>,
1772}
1773
1774#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1776#[serde(rename_all = "camelCase")]
1777pub struct TemplateListRequest {
1778 pub repo: Option<PathBuf>,
1780}
1781
1782#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1784#[serde(rename_all = "camelCase")]
1785pub struct TemplateSummary {
1786 pub source: String,
1788 pub name: String,
1790 pub kind: String,
1792}
1793
1794#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1796#[serde(rename_all = "camelCase")]
1797pub struct TemplateListReport {
1798 pub templates: Vec<TemplateSummary>,
1800 pub diagnostics: Vec<Diagnostic>,
1802}
1803
1804#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1806#[serde(rename_all = "camelCase")]
1807pub struct TemplateRenderRequest {
1808 pub repo: Option<PathBuf>,
1810 pub source: TemplateSource,
1812 pub inputs: serde_json::Value,
1814 pub dry_run: bool,
1816}
1817
1818#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1820#[serde(rename_all = "camelCase")]
1821pub struct AffectedRequest {
1822 pub repo: Option<PathBuf>,
1824 pub base: Option<String>,
1826 pub head: Option<String>,
1828 pub changed_files: Vec<RepoRelativePath>,
1830 pub tasks: Vec<TaskName>,
1832}
1833
1834#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1836#[serde(rename_all = "camelCase")]
1837pub struct AffectedReport {
1838 pub directly_affected: Vec<ProjectName>,
1840 pub transitively_affected: Vec<ProjectName>,
1842 pub workspaces: Vec<String>,
1844 pub tasks: Vec<String>,
1846 pub risk_flags: Vec<String>,
1848 pub reasons: Vec<AffectedReason>,
1850 pub suggested_reviewers: Vec<OwnerHandle>,
1852 pub diagnostics: Vec<Diagnostic>,
1854}
1855
1856#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1858#[serde(rename_all = "camelCase")]
1859pub struct AffectedReason {
1860 pub source: String,
1862 pub target: String,
1864 pub reason: String,
1866}
1867
1868#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1870#[serde(rename_all = "camelCase")]
1871pub struct TaskRunRequest {
1872 pub repo: Option<PathBuf>,
1874 pub tasks: Vec<TaskName>,
1876 pub projects: Vec<ProjectName>,
1878 pub workspaces: Vec<String>,
1880 pub affected: bool,
1882 pub changed_files: Vec<RepoRelativePath>,
1884 pub base: Option<String>,
1886 pub head: Option<String>,
1888 pub concurrency: Option<NonZeroU32>,
1890 pub dry_run: bool,
1892}
1893
1894#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1896#[serde(rename_all = "camelCase")]
1897pub struct TaskRunPlan {
1898 pub commands: Vec<ProcessCommand>,
1900 pub concurrency: NonZeroUsize,
1902}
1903
1904#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1906#[serde(rename_all = "camelCase")]
1907pub struct TaskRunReport {
1908 pub commands: Vec<ProcessCommand>,
1910 pub outputs: Vec<TaskCommandOutput>,
1912 pub diagnostics: Vec<Diagnostic>,
1914}
1915
1916#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1918#[serde(rename_all = "camelCase")]
1919pub struct TaskCommandOutput {
1920 pub project: ProjectName,
1922 pub workspace: WorkspaceName,
1924 pub task: TaskName,
1926 pub output: ProcessOutput,
1928}
1929
1930#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1932#[serde(rename_all = "camelCase")]
1933pub struct ProcessCommand {
1934 #[serde(skip_serializing_if = "Option::is_none")]
1936 pub project: Option<ProjectName>,
1937 #[serde(skip_serializing_if = "Option::is_none")]
1939 pub workspace: Option<WorkspaceName>,
1940 #[serde(skip_serializing_if = "Option::is_none")]
1942 pub task: Option<TaskName>,
1943 pub cwd: RepoRelativePath,
1945 #[serde(skip_serializing_if = "Option::is_none")]
1947 pub absolute_cwd: Option<PathBuf>,
1948 pub program: String,
1950 pub args: Vec<String>,
1952 pub env: BTreeMap<String, String>,
1954}
1955
1956#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1958#[serde(rename_all = "camelCase")]
1959pub struct ProcessOutput {
1960 pub status: i32,
1962 pub stdout: String,
1964 pub stderr: String,
1966}
1967
1968#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1970#[serde(rename_all = "camelCase")]
1971pub struct CiMatrixRequest {
1972 pub repo: Option<PathBuf>,
1974 pub tasks: Vec<TaskName>,
1976 pub changed_files: Vec<RepoRelativePath>,
1978 pub base: Option<String>,
1980 pub head: Option<String>,
1982 pub fallback: CiFallback,
1984}
1985
1986#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
1988#[serde(rename_all = "kebab-case")]
1989pub enum CiFallback {
1990 All,
1992 #[default]
1994 None,
1995 Error,
1997}
1998
1999#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2001#[serde(rename_all = "camelCase")]
2002pub struct CiMatrixReport {
2003 pub entries: Vec<serde_json::Value>,
2005 pub github_actions: serde_json::Value,
2007}
2008
2009#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
2011#[serde(rename_all = "kebab-case")]
2012pub enum CiProvider {
2013 #[default]
2015 GitHubActions,
2016}
2017
2018#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2020#[serde(rename_all = "camelCase")]
2021pub struct CiWorkflowRequest {
2022 pub repo: Option<PathBuf>,
2024 pub provider: CiProvider,
2026 pub write: bool,
2028}
2029
2030#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2032#[serde(rename_all = "camelCase")]
2033pub struct CiWorkflowReport {
2034 pub path: RepoRelativePath,
2036 pub content: String,
2038 pub operations: Vec<FileOperation>,
2040 pub diagnostics: Vec<Diagnostic>,
2042}
2043
2044#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2046#[serde(rename_all = "camelCase")]
2047pub struct HygieneCheckRequest {
2048 pub repo: Option<PathBuf>,
2050}
2051
2052#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2054#[serde(rename_all = "camelCase")]
2055pub struct HygieneCleanRequest {
2056 pub repo: Option<PathBuf>,
2058 pub dry_run: bool,
2060}
2061
2062#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2064#[serde(rename_all = "camelCase")]
2065pub struct HygieneReport {
2066 pub diagnostics: Vec<Diagnostic>,
2068 pub operations: Vec<FileOperation>,
2070}
2071
2072#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
2074#[serde(rename_all = "kebab-case")]
2075pub enum DependencyRewriteMode {
2076 #[default]
2078 Auto,
2079 Off,
2081 ReportOnly,
2083}
2084
2085#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
2087#[serde(rename_all = "kebab-case")]
2088pub enum AdoptionCiMode {
2089 #[default]
2091 Update,
2092 Off,
2094 ReportOnly,
2096}
2097
2098#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
2100#[serde(rename_all = "kebab-case")]
2101pub enum AdoptionOutputFormat {
2102 #[default]
2104 Human,
2105 Json,
2107 GitHubActions,
2109}
2110
2111#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2113#[serde(rename_all = "camelCase")]
2114pub struct AdoptionPlanRequest {
2115 pub source: PathBuf,
2117 pub dest: PathBuf,
2119 pub include: Vec<String>,
2121 pub exclude: Vec<String>,
2123 pub map: BTreeMap<String, RepoRelativePath>,
2125 pub kind: BTreeMap<String, ProjectKind>,
2127 pub owner: BTreeMap<String, OwnerHandle>,
2129 pub rewrite_deps: DependencyRewriteMode,
2131 pub ci: AdoptionCiMode,
2133 pub verification: ValidationMode,
2135 pub format: AdoptionOutputFormat,
2137}
2138
2139#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2141#[serde(rename_all = "camelCase")]
2142pub struct AdoptionApplyRequest {
2143 pub plan: PathBuf,
2145 pub refresh: bool,
2147}
2148
2149#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2151#[serde(rename_all = "camelCase")]
2152pub struct AdoptionVerifyRequest {
2153 pub plan: PathBuf,
2155}
2156
2157#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
2159#[serde(rename_all = "camelCase")]
2160pub struct AdoptionPlan {
2161 pub source_root: Utf8PathBuf,
2163 pub dest_root: Utf8PathBuf,
2165 pub sources: Vec<AdoptedSource>,
2167 pub operations: Vec<AdoptionFileOperation>,
2169 pub dependency_rewrites: Vec<DependencyRewrite>,
2171 pub manifest_syntheses: Vec<ProjectManifestSynthesis>,
2173 pub ci_operations: Vec<FileOperation>,
2175 pub verification: VerificationPlan,
2177 pub diagnostics: Vec<Diagnostic>,
2179}
2180
2181#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
2183#[serde(rename_all = "camelCase")]
2184pub struct AdoptedSource {
2185 pub name: String,
2187 pub source_path: Utf8PathBuf,
2189 pub destination_path: RepoRelativePath,
2191 pub inferred_kind: ProjectKind,
2193 pub confidence: f32,
2195 pub reasons: Vec<String>,
2197 pub inventory: SourceInventory,
2199 pub skipped: bool,
2201 pub override_applied: bool,
2203}
2204
2205#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2207#[serde(rename_all = "camelCase")]
2208pub struct SourceInventory {
2209 pub name: String,
2211 pub has_vcs: bool,
2213 pub readme_summary: Option<String>,
2215 pub manifests: Vec<String>,
2217 pub top_level_dirs: Vec<String>,
2219 pub dependency_references: Vec<String>,
2221 pub generated_artifacts: Vec<String>,
2223 pub required_tools: Vec<String>,
2225}
2226
2227#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2229#[serde(rename_all = "camelCase")]
2230pub struct AdoptionFileOperation {
2231 pub id: String,
2233 pub operation: String,
2235 #[serde(skip_serializing_if = "Option::is_none")]
2237 pub source_path: Option<Utf8PathBuf>,
2238 pub destination_path: RepoRelativePath,
2240 #[serde(skip_serializing_if = "Option::is_none")]
2242 pub content: Option<String>,
2243 #[serde(skip_serializing_if = "Option::is_none")]
2245 pub checksum: Option<String>,
2246}
2247
2248#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2250#[serde(rename_all = "camelCase")]
2251pub struct DependencyRewrite {
2252 pub file: RepoRelativePath,
2254 pub package: String,
2256 pub from: String,
2258 pub to: String,
2260 pub surface: DependencySurface,
2262 pub owner_project: ProjectName,
2264}
2265
2266#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2268#[serde(rename_all = "camelCase")]
2269pub struct ProjectManifestSynthesis {
2270 pub source: String,
2272 pub project: ProjectName,
2274 pub manifest_path: RepoRelativePath,
2276 pub content: String,
2278}
2279
2280#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2282#[serde(rename_all = "camelCase")]
2283pub struct VerificationPlan {
2284 pub prerequisites: Vec<ToolPrerequisite>,
2286 pub commands: Vec<ProcessCommand>,
2288}
2289
2290#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2292#[serde(rename_all = "camelCase")]
2293pub struct ToolPrerequisite {
2294 pub tool: String,
2296 pub reason: String,
2298}
2299
2300#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2302#[serde(rename_all = "camelCase")]
2303pub struct PrSummaryRequest {
2304 pub repo: Option<PathBuf>,
2306 pub base: Option<String>,
2308 pub head: Option<String>,
2310 pub changed_files: Vec<RepoRelativePath>,
2312}
2313
2314#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2316#[serde(rename_all = "camelCase")]
2317pub struct PrSummary {
2318 pub markdown: String,
2320 pub impact: serde_json::Value,
2322}
2323
2324#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2326#[serde(rename_all = "camelCase")]
2327pub struct AiContextRequest {
2328 pub repo: Option<PathBuf>,
2330 pub project: ProjectName,
2332 pub audience: String,
2334}
2335
2336#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2338#[serde(rename_all = "camelCase")]
2339pub struct AiContext {
2340 pub payload: serde_json::Value,
2342}
2343
2344#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2346#[serde(rename_all = "camelCase")]
2347pub struct CodegenCheckRequest {
2348 pub repo: Option<PathBuf>,
2350 pub base: Option<String>,
2352 pub head: Option<String>,
2354 pub changed_files: Vec<RepoRelativePath>,
2356}
2357
2358#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2360#[serde(rename_all = "camelCase")]
2361pub struct CodegenCheckReport {
2362 pub diagnostics: Vec<Diagnostic>,
2364}
2365
2366#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2368#[serde(rename_all = "camelCase")]
2369pub struct ProtoFacadeRequest {
2370 pub repo: Option<PathBuf>,
2372 pub operation: ProtoOperation,
2374 #[serde(skip_serializing_if = "Option::is_none")]
2376 pub selector: Option<String>,
2377 pub base: Option<String>,
2379 pub head: Option<String>,
2381 pub changed_files: Vec<RepoRelativePath>,
2383}
2384
2385#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2387#[serde(rename_all = "kebab-case")]
2388pub enum ProtoOperation {
2389 #[default]
2391 Check,
2392 Owners,
2394 Consumers,
2396}
2397
2398#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2400#[serde(rename_all = "camelCase")]
2401pub struct ProtoFacadeReport {
2402 pub owners: Vec<ProjectName>,
2404 pub consumers: Vec<ProjectName>,
2406 pub commands: Vec<ProcessCommand>,
2408 pub diagnostics: Vec<Diagnostic>,
2410}
2411
2412#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2414#[serde(rename_all = "camelCase")]
2415pub struct IacFacadeRequest {
2416 pub repo: Option<PathBuf>,
2418 pub affected: bool,
2420 #[serde(skip_serializing_if = "Option::is_none")]
2422 pub project: Option<ProjectName>,
2423 #[serde(skip_serializing_if = "Option::is_none")]
2425 pub env: Option<String>,
2426 pub core: bool,
2428 pub base: Option<String>,
2430 pub head: Option<String>,
2432 pub changed_files: Vec<RepoRelativePath>,
2434 pub dry_run: bool,
2436}
2437
2438#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2440#[serde(rename_all = "camelCase")]
2441pub struct IacFacadeReport {
2442 pub commands: Vec<ProcessCommand>,
2444 pub risk_flags: Vec<String>,
2446 pub diagnostics: Vec<Diagnostic>,
2448}
2449
2450#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2452#[serde(rename_all = "camelCase")]
2453pub struct OpsPlanRequest {
2454 pub repo: Option<PathBuf>,
2456 pub base: Option<String>,
2458 pub head: Option<String>,
2460 pub changed_files: Vec<RepoRelativePath>,
2462 pub environments: Vec<String>,
2464 pub tasks: Vec<TaskName>,
2466 #[serde(skip_serializing_if = "Option::is_none")]
2468 pub output: Option<PathBuf>,
2469}
2470
2471#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2473#[serde(rename_all = "camelCase")]
2474pub struct OpsPlan {
2475 pub id: String,
2477 #[serde(skip_serializing_if = "Option::is_none")]
2479 pub repo_root: Option<RepoRoot>,
2480 #[serde(skip_serializing_if = "Option::is_none")]
2482 pub base: Option<String>,
2483 #[serde(skip_serializing_if = "Option::is_none")]
2485 pub head: Option<String>,
2486 pub environments: Vec<String>,
2488 pub affected: AffectedReport,
2490 pub task_plan: TaskRunReport,
2492 pub iac: Vec<IacOperation>,
2494 pub dns: Vec<DnsOperation>,
2496 pub cdn: Vec<CdnCheck>,
2498 pub provider_capabilities: Vec<ProviderCapabilityReport>,
2500 pub probes: Vec<ProbeSpec>,
2502 pub manual_reconciliation: Vec<ManualStateRecord>,
2504 pub required_env: Vec<String>,
2506 pub production_gaps: Vec<String>,
2508 pub diagnostics: Vec<Diagnostic>,
2510}
2511
2512#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2514#[serde(rename_all = "camelCase")]
2515pub struct IacOperation {
2516 #[serde(skip_serializing_if = "Option::is_none")]
2518 pub project: Option<ProjectName>,
2519 pub workspace: String,
2521 pub provider: IacProvider,
2523 pub environment: String,
2525 pub stack: String,
2527 pub preview_command: ProcessCommand,
2529 #[serde(skip_serializing_if = "Option::is_none")]
2531 pub apply_command: Option<ProcessCommand>,
2532 pub risk: Vec<String>,
2534}
2535
2536#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2538#[serde(rename_all = "camelCase")]
2539pub struct DnsOperation {
2540 pub zone: String,
2542 pub provider: String,
2544 pub record: String,
2546 pub expected_target: String,
2548 #[serde(skip_serializing_if = "Option::is_none")]
2550 pub expected_proxied: Option<bool>,
2551 pub verification: Vec<ProcessCommand>,
2553}
2554
2555#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2557#[serde(rename_all = "camelCase")]
2558pub struct CdnCheck {
2559 pub provider: String,
2561 pub alias: String,
2563 pub expected_response_headers: Vec<String>,
2565 pub verification: ProcessCommand,
2567}
2568
2569#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2571#[serde(rename_all = "camelCase")]
2572pub struct OpsVerifyRequest {
2573 pub repo: Option<PathBuf>,
2575 pub plan: PathBuf,
2577}
2578
2579#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2581#[serde(rename_all = "camelCase")]
2582pub struct OpsVerifyReport {
2583 pub commands: Vec<ProcessCommand>,
2585 pub skipped_mutating_commands: Vec<ProcessCommand>,
2587 pub diagnostics: Vec<Diagnostic>,
2589}
2590
2591#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2593#[serde(rename_all = "camelCase")]
2594pub struct OpsReconcileRequest {
2595 pub repo: Option<PathBuf>,
2597 pub plan: PathBuf,
2599}
2600
2601#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2603#[serde(rename_all = "camelCase")]
2604pub struct OpsReconcileReport {
2605 pub records: Vec<ManualStateRecord>,
2607 pub cleanup_commands: Vec<ProcessCommand>,
2609 pub diagnostics: Vec<Diagnostic>,
2611}
2612
2613#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2615#[serde(rename_all = "camelCase")]
2616pub struct ProviderCapabilityRequest {
2617 pub repo: Option<PathBuf>,
2619 #[serde(skip_serializing_if = "Option::is_none")]
2621 pub workspace: Option<String>,
2622 pub base: Option<String>,
2624 pub head: Option<String>,
2626 pub changed_files: Vec<RepoRelativePath>,
2628}
2629
2630#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2632#[serde(rename_all = "camelCase")]
2633pub struct ProviderCapabilityReport {
2634 pub workspace: String,
2636 pub package: String,
2638 pub version: String,
2640 pub resource: String,
2642 pub field: String,
2644 pub status: String,
2646 pub advice: String,
2648 pub diagnostics: Vec<Diagnostic>,
2650}
2651
2652#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2654#[serde(rename_all = "camelCase")]
2655pub struct SessionJournal {
2656 pub id: String,
2658 pub name: String,
2660 #[serde(skip_serializing_if = "Option::is_none")]
2662 pub plan_id: Option<String>,
2663 pub entries: Vec<SessionEntry>,
2665}
2666
2667#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2669#[serde(rename_all = "camelCase")]
2670pub struct SessionEntry {
2671 pub kind: String,
2673 pub timestamp: u64,
2675 #[serde(skip_serializing_if = "Option::is_none")]
2677 pub command: Option<String>,
2678 #[serde(skip_serializing_if = "Option::is_none")]
2680 pub exit_status: Option<i32>,
2681 #[serde(skip_serializing_if = "Option::is_none")]
2683 pub message: Option<String>,
2684 #[serde(skip_serializing_if = "Option::is_none")]
2686 pub plan_id: Option<String>,
2687}
2688
2689#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2691#[serde(rename_all = "camelCase")]
2692pub struct OpsJournalRequest {
2693 pub repo: Option<PathBuf>,
2695 pub action: OpsJournalAction,
2697}
2698
2699#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2701#[serde(rename_all = "camelCase", tag = "kind")]
2702pub enum OpsJournalAction {
2703 Start {
2705 name: String,
2707 plan_id: Option<String>,
2709 },
2710 AddCommand {
2712 session: String,
2714 command: String,
2716 exit_status: Option<i32>,
2718 },
2719 AddNote {
2721 session: String,
2723 note_kind: String,
2725 message: String,
2727 },
2728 Summary {
2730 session: String,
2732 },
2733}
2734
2735#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2737#[serde(rename_all = "camelCase")]
2738pub struct OpsJournalReport {
2739 #[serde(skip_serializing_if = "Option::is_none")]
2741 pub path: Option<PathBuf>,
2742 #[serde(skip_serializing_if = "Option::is_none")]
2744 pub journal: Option<SessionJournal>,
2745 #[serde(skip_serializing_if = "Option::is_none")]
2747 pub markdown: Option<String>,
2748 pub diagnostics: Vec<Diagnostic>,
2750}
2751
2752#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2754#[serde(rename_all = "camelCase")]
2755pub struct SkillsFacadeRequest {
2756 pub repo: Option<PathBuf>,
2758 pub sync: bool,
2760 pub dry_run: bool,
2762}
2763
2764#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2766#[serde(rename_all = "camelCase")]
2767pub struct SkillsFacadeReport {
2768 pub diagnostics: Vec<Diagnostic>,
2770}
2771
2772pub 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
2807pub 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}