1use minco_db::{MigrationCatalog, SeedCatalog};
2use minco_plan::{DatabaseCostEstimate, DeploymentPlan, PlanDiagnostic, RuntimeCostEstimate};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::{collections::BTreeMap, path::PathBuf};
6use thiserror::Error;
7
8pub const PROJECT_VIEW_SCHEMA_VERSION: u32 = 1;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ViewLimits {
12 pub max_files: usize,
13 pub max_file_bytes: usize,
14 pub max_total_input_bytes: usize,
15 pub max_text_bytes: usize,
16 pub max_nodes: usize,
17 pub max_edges: usize,
18 pub max_response_bytes: usize,
19}
20
21impl Default for ViewLimits {
22 fn default() -> Self {
23 Self {
24 max_files: 1_024,
25 max_file_bytes: 2 * 1_024 * 1_024,
26 max_total_input_bytes: 16 * 1_024 * 1_024,
27 max_text_bytes: 16 * 1_024,
28 max_nodes: 4_096,
29 max_edges: 8_192,
30 max_response_bytes: 2 * 1_024 * 1_024,
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct ProjectIdentity {
37 pub name: String,
38 pub source_digest: String,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum SourceKind {
44 Manifest,
45 Contract,
46 Deployment,
47 Roadmap,
48 Task,
49 PluginCatalog,
50 QualityContract,
51 GeneratedBinding,
52 Migration,
53 Seed,
54 Verification,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct SourceProvenance {
59 pub kind: SourceKind,
60 pub path: PathBuf,
61 pub sha256: String,
62 pub bytes: usize,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum NodeKind {
68 Project,
69 Architecture,
70 Resource,
71 Operation,
72 Milestone,
73 Task,
74 Feature,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum SemanticStatus {
80 NotStarted,
81 Active,
82 Blocked,
83 Complete,
84 Unknown,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ProjectNode {
89 pub id: String,
90 pub kind: NodeKind,
91 pub label: String,
92 pub description: Option<String>,
93 pub raw_status: Option<String>,
94 pub semantic_status: Option<SemanticStatus>,
95 pub source: PathBuf,
96 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
97 pub properties: BTreeMap<String, Value>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum EdgeKind {
103 Contains,
104 DependsOn,
105 BelongsTo,
106 Implements,
107 Exposes,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct ProjectEdge {
112 pub from: String,
113 pub to: String,
114 pub kind: EdgeKind,
115 pub source: PathBuf,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct StatusMapping {
120 pub vocabulary: String,
121 pub raw: String,
122 pub semantic: SemanticStatus,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum EvidenceLane {
128 Source,
129 LocalVerification,
130 HostedVerification,
131 Deployment,
132 Runtime,
133 Review,
134}
135
136impl EvidenceLane {
137 pub const ALL: [Self; 6] = [
138 Self::Source,
139 Self::LocalVerification,
140 Self::HostedVerification,
141 Self::Deployment,
142 Self::Runtime,
143 Self::Review,
144 ];
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct EvidenceFreshness {
149 pub basis: String,
150 pub observed_at: Option<String>,
151 pub limitation: Option<String>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct EvidenceItem {
156 pub subject: String,
157 pub state: String,
158 pub source: String,
159 pub exact_subject: Option<String>,
160 pub freshness: EvidenceFreshness,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct ConfigurationFieldView {
165 pub key: String,
166 pub kind: String,
167 pub required: bool,
168 pub secret: bool,
169 pub description: String,
170 pub value: ConfigurationValue,
171 pub source: PathBuf,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(tag = "state", content = "value", rename_all = "snake_case")]
176pub enum ConfigurationValue {
177 Declared(Value),
178 Redacted,
179 Absent,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct DeploymentProjection {
184 pub plan: DeploymentPlan,
185 pub diagnostics: Vec<PlanDiagnostic>,
186}
187
188#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct CostProjection {
190 pub database: DatabaseCostEstimate,
191 pub runtime: RuntimeCostEstimate,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct TaskReadiness {
196 pub id: String,
197 pub raw_status: String,
198 pub dependencies_complete: bool,
199 pub ready: bool,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct FeedbackContext {
204 pub feature_declared: bool,
205 pub enabled: bool,
206 pub operation_ids: Vec<String>,
207 pub limitation: String,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "snake_case")]
212pub enum DiagnosticSeverity {
213 Information,
214 Warning,
215 Error,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct ProjectDiagnostic {
220 pub code: String,
221 pub severity: DiagnosticSeverity,
222 pub message: String,
223 pub source: Option<PathBuf>,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct InputUsage {
228 pub files: usize,
229 pub bytes: usize,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct DerivedSummary {
234 pub derived: bool,
235 pub node_count: usize,
236 pub edge_count: usize,
237 pub denominator: usize,
238 pub task_status_counts: BTreeMap<String, usize>,
239 pub ready_task_ids: Vec<String>,
240 pub evidence_item_counts: BTreeMap<EvidenceLane, usize>,
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct ProjectView {
245 pub schema_version: u32,
246 pub project: ProjectIdentity,
247 pub limits: ViewLimits,
248 pub input_usage: InputUsage,
249 pub provenance: Vec<SourceProvenance>,
250 pub nodes: Vec<ProjectNode>,
251 pub edges: Vec<ProjectEdge>,
252 pub status_mappings: Vec<StatusMapping>,
253 pub evidence: BTreeMap<EvidenceLane, Vec<EvidenceItem>>,
254 pub configuration: Vec<ConfigurationFieldView>,
255 pub migrations: MigrationCatalog,
256 pub seeds: SeedCatalog,
257 pub deployment: DeploymentProjection,
258 pub costs: CostProjection,
259 pub task_readiness: Vec<TaskReadiness>,
260 pub feedback: FeedbackContext,
261 pub summary: DerivedSummary,
262 pub diagnostics: Vec<ProjectDiagnostic>,
263}
264
265impl ProjectView {
266 #[must_use]
267 pub fn operation(&self, operation_id: &str) -> Option<&ProjectNode> {
268 self.nodes.iter().find(|node| {
269 node.kind == NodeKind::Operation
270 && node.properties.get("operation_id").and_then(Value::as_str) == Some(operation_id)
271 })
272 }
273
274 #[must_use]
275 pub fn task(&self, task_id: &str) -> Option<&TaskReadiness> {
276 self.task_readiness.iter().find(|task| task.id == task_id)
277 }
278}
279
280#[derive(Debug, Error)]
281pub enum ProjectViewError {
282 #[error("project root must be an explicit canonical absolute directory: {0}")]
283 NonCanonicalRoot(PathBuf),
284 #[error("declared project path is not a normalized relative path: {0}")]
285 InvalidDeclaredPath(PathBuf),
286 #[error("declared project path crosses a symbolic link: {0}")]
287 SymbolicLink(PathBuf),
288 #[error("declared project path is missing or has the wrong type: {0}")]
289 InvalidPathType(PathBuf),
290 #[error("project view input exceeds {limit_name}={limit} at {path}")]
291 LimitExceeded {
292 limit_name: &'static str,
293 limit: usize,
294 path: PathBuf,
295 },
296 #[error("project view source I/O failed at {path}: {source}")]
297 Io {
298 path: PathBuf,
299 #[source]
300 source: std::io::Error,
301 },
302 #[error("project view source is invalid at {path}: {message}")]
303 InvalidSource { path: PathBuf, message: String },
304}