Skip to main content

minco_project_view/
model.rs

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}