1use std::collections::BTreeMap;
2
3use serde::Deserialize;
4
5use crate::{
6 EngineInputV2, IncrementalRevisionV0, OmenaBundlerHostResolveModuleRequestV0,
7 OmenaBundlerHostResolveModuleResponseV0, OmenaError, OmenaErrorClassV0, OmenaErrorContextV0,
8 OmenaErrorRecoverabilityV0, OmenaErrorSeverityV0, OmenaQueryBuildVerificationProfileV0,
9 OmenaQueryConsumerBuildOptionsV0, OmenaQueryExplainInputV0,
10 OmenaQuerySourceDiagnosticsForFileV0, OmenaQueryStylePackageManifestV0,
11 OmenaQueryStyleResolutionInputsV0, OmenaQueryStyleSourceInputV0,
12 OmenaQueryTransformStrictPolicyEventV0, OmenaQueryTransformStrictPolicyReasonV0,
13 OmenaQueryTransformStrictPolicySummaryV0, OmenaSdkBuildRequestV0, OmenaSdkBuildResponseV0,
14 OmenaSdkBuildVerificationEventV0, OmenaSdkBuildVerificationProfileV0,
15 OmenaSdkBuildVerificationReasonV0, OmenaSdkBuildVerificationSummaryV0,
16 OmenaSdkDiagnosticsRequestV0, OmenaSdkDiagnosticsResponseV0, OmenaSdkExplainRequestV0,
17 OmenaSdkExplainResponseV0, OmenaSdkQueryRequestV0, OmenaSdkQueryResponseV0,
18 OmenaSdkResponsePartitionV0, OmenaSdkSnapshotRequestV0, OmenaSdkSnapshotResponseV0,
19 OmenaWorkspaceSnapshotIdV0, ParserPositionV0, attach_omena_query_consumer_build_source_map_v3,
20 execute_omena_query_consumer_build_style_source_with_context_and_options,
21 execute_omena_sdk_diagnostics_workflow, explain_omena_query,
22 read_omena_query_cascade_at_position, resolve_omena_bundler_host_module_v0,
23 summarize_omena_query_consumer_check_style_source,
24 summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs,
25 summarize_omena_query_style_document, summarize_omena_query_style_hover_candidates,
26};
27
28#[derive(Debug, Clone)]
29pub struct OmenaSdkWorkspaceV0 {
30 workspace_root: String,
31 style_sources: BTreeMap<String, String>,
32 style_resolution_inputs: OmenaQueryStyleResolutionInputsV0,
33 revision: IncrementalRevisionV0,
34}
35
36impl OmenaSdkWorkspaceV0 {
37 pub fn open(
38 request: OmenaSdkSnapshotRequestV0,
39 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
40 ) -> Result<Self, OmenaError> {
41 Self::open_with_resolution_inputs(
42 request,
43 style_sources,
44 OmenaQueryStyleResolutionInputsV0::default(),
45 )
46 }
47
48 pub fn open_with_resolution_inputs(
49 request: OmenaSdkSnapshotRequestV0,
50 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
51 style_resolution_inputs: OmenaQueryStyleResolutionInputsV0,
52 ) -> Result<Self, OmenaError> {
53 Self::open_at_snapshot_with_resolution_inputs(
54 request,
55 style_sources,
56 OmenaWorkspaceSnapshotIdV0::from_revision(IncrementalRevisionV0 { value: 1 }),
57 style_resolution_inputs,
58 )
59 }
60
61 pub fn open_at_snapshot(
62 request: OmenaSdkSnapshotRequestV0,
63 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
64 snapshot_id: OmenaWorkspaceSnapshotIdV0,
65 ) -> Result<Self, OmenaError> {
66 Self::open_at_snapshot_with_resolution_inputs(
67 request,
68 style_sources,
69 snapshot_id,
70 OmenaQueryStyleResolutionInputsV0::default(),
71 )
72 }
73
74 pub fn open_at_snapshot_with_resolution_inputs(
75 request: OmenaSdkSnapshotRequestV0,
76 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
77 snapshot_id: OmenaWorkspaceSnapshotIdV0,
78 style_resolution_inputs: OmenaQueryStyleResolutionInputsV0,
79 ) -> Result<Self, OmenaError> {
80 if request.workspace_root.trim().is_empty() {
81 return Err(sdk_error(
82 OmenaErrorClassV0::Input,
83 "workspace root must not be empty",
84 "workspace.empty-root",
85 OmenaErrorRecoverabilityV0::UserAction,
86 ));
87 }
88 let mut sources = BTreeMap::new();
89 for source in style_sources {
90 let style_path = normalize_style_path(source.style_path.as_str());
91 if sources
92 .insert(style_path.clone(), source.style_source)
93 .is_some()
94 {
95 return Err(sdk_error(
96 OmenaErrorClassV0::Input,
97 format!("workspace contains duplicate style path {style_path:?}"),
98 "workspace.duplicate-style-path",
99 OmenaErrorRecoverabilityV0::UserAction,
100 ));
101 }
102 }
103 Ok(Self {
104 workspace_root: request.workspace_root,
105 style_sources: sources,
106 style_resolution_inputs,
107 revision: snapshot_id.revision(),
108 })
109 }
110
111 pub fn snapshot_id(&self) -> OmenaWorkspaceSnapshotIdV0 {
112 OmenaWorkspaceSnapshotIdV0::from_revision(self.revision)
113 }
114
115 pub fn snapshot(&self) -> OmenaSdkSnapshotResponseV0 {
116 OmenaSdkSnapshotResponseV0 {
117 snapshot_id: self.snapshot_id(),
118 partition: OmenaSdkResponsePartitionV0::Public,
119 workspace_root: self.workspace_root.clone(),
120 }
121 }
122
123 pub fn replace_style_sources(
124 &mut self,
125 style_sources: impl IntoIterator<Item = OmenaQueryStyleSourceInputV0>,
126 ) -> Result<OmenaSdkSnapshotResponseV0, OmenaError> {
127 let mut replacement = BTreeMap::new();
128 for source in style_sources {
129 let style_path = normalize_style_path(source.style_path.as_str());
130 if replacement
131 .insert(style_path.clone(), source.style_source)
132 .is_some()
133 {
134 return Err(sdk_error(
135 OmenaErrorClassV0::Input,
136 format!("workspace contains duplicate style path {style_path:?}"),
137 "workspace.duplicate-style-path",
138 OmenaErrorRecoverabilityV0::UserAction,
139 ));
140 }
141 }
142 if replacement != self.style_sources {
143 self.style_sources = replacement;
144 self.revision.value = self.revision.value.saturating_add(1);
145 }
146 Ok(self.snapshot())
147 }
148
149 pub fn replace_style_resolution_inputs(
150 &mut self,
151 style_resolution_inputs: OmenaQueryStyleResolutionInputsV0,
152 ) -> OmenaSdkSnapshotResponseV0 {
153 if style_resolution_inputs != self.style_resolution_inputs {
154 self.style_resolution_inputs = style_resolution_inputs;
155 self.revision.value = self.revision.value.saturating_add(1);
156 }
157 self.snapshot()
158 }
159
160 pub fn execute_query(
161 &self,
162 request: OmenaSdkQueryRequestV0,
163 ) -> Result<OmenaSdkQueryResponseV0, OmenaError> {
164 self.ensure_snapshot(request.snapshot_id, "query")?;
165 let input = query_input(request.input.as_ref())?;
166 let (style_path, style_source) = self.style_source(input.style_path.as_str())?;
167 let payload = match request.query_kind.as_str() {
168 "styleSummary" => summarize_omena_query_style_document(style_path, style_source)
169 .map(|summary| serde_json::to_value(summary).map_err(serialize_error))
170 .transpose()?
171 .ok_or_else(|| {
172 sdk_error(
173 OmenaErrorClassV0::Analysis,
174 format!("style summary is unavailable for {style_path:?}"),
175 "query.style-summary-unavailable",
176 OmenaErrorRecoverabilityV0::Retry,
177 )
178 })?,
179 "hoverCandidates" => serde_json::to_value(
180 summarize_omena_query_style_hover_candidates(style_path, style_source).ok_or_else(
181 || {
182 sdk_error(
183 OmenaErrorClassV0::Analysis,
184 format!("hover candidates are unavailable for {style_path:?}"),
185 "query.hover-candidates-unavailable",
186 OmenaErrorRecoverabilityV0::Retry,
187 )
188 },
189 )?,
190 )
191 .map_err(serialize_error)?,
192 _ => {
193 return Err(sdk_error(
194 OmenaErrorClassV0::Unsupported,
195 format!("unsupported SDK query kind {:?}", request.query_kind),
196 "query.unsupported-kind",
197 OmenaErrorRecoverabilityV0::UserAction,
198 ));
199 }
200 };
201 Ok(OmenaSdkQueryResponseV0 {
202 snapshot_id: self.snapshot_id(),
203 partition: OmenaSdkResponsePartitionV0::Public,
204 payload,
205 })
206 }
207
208 pub fn execute_diagnostics(
209 &self,
210 mut request: OmenaSdkDiagnosticsRequestV0,
211 ) -> Result<OmenaSdkDiagnosticsResponseV0, OmenaError> {
212 self.ensure_snapshot(request.snapshot_id, "diagnostics")?;
213 let (style_path, style_source) = self.style_source(request.style_path.as_str())?;
214 if request.style_source != style_source {
215 return Err(sdk_error(
216 OmenaErrorClassV0::Workspace,
217 format!("diagnostics source does not match snapshot for {style_path:?}"),
218 "workspace.style-source-mismatch",
219 OmenaErrorRecoverabilityV0::Retry,
220 ));
221 }
222 request.style_path = style_path.to_string();
223 execute_omena_sdk_diagnostics_workflow(request, self.snapshot_id())
224 }
225
226 pub fn execute_consumer_check(
227 &self,
228 snapshot_id: OmenaWorkspaceSnapshotIdV0,
229 style_path: &str,
230 ) -> Result<serde_json::Value, OmenaError> {
231 self.ensure_snapshot(snapshot_id, "check")?;
232 let (style_path, style_source) = self.style_source(style_path)?;
233 serde_json::to_value(summarize_omena_query_consumer_check_style_source(
234 style_path,
235 style_source,
236 ))
237 .map_err(serialize_error)
238 }
239
240 pub fn execute_source_diagnostics(
241 &self,
242 snapshot_id: OmenaWorkspaceSnapshotIdV0,
243 source_path: &str,
244 source: &str,
245 package_manifests: &[OmenaQueryStylePackageManifestV0],
246 ) -> Result<OmenaQuerySourceDiagnosticsForFileV0, OmenaError> {
247 self.ensure_snapshot(snapshot_id, "source diagnostics")?;
248 let style_sources = self.style_source_inputs();
249 Ok(
250 summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs(
251 source_path,
252 source,
253 style_sources.as_slice(),
254 package_manifests,
255 &self.style_resolution_inputs,
256 ),
257 )
258 }
259
260 pub fn execute_bundler_resolve(
261 &self,
262 snapshot_id: OmenaWorkspaceSnapshotIdV0,
263 style_path: String,
264 package_manifests: Vec<OmenaQueryStylePackageManifestV0>,
265 ) -> Result<OmenaBundlerHostResolveModuleResponseV0, OmenaError> {
266 self.ensure_snapshot(snapshot_id, "bundler resolve")?;
267 Ok(resolve_omena_bundler_host_module_v0(
268 OmenaBundlerHostResolveModuleRequestV0 {
269 snapshot_id: self.snapshot_id(),
270 workspace_root: self.workspace_root.clone(),
271 style_path,
272 style_sources: self.style_source_inputs(),
273 package_manifests,
274 },
275 ))
276 }
277
278 pub fn execute_build(
279 &self,
280 mut request: OmenaSdkBuildRequestV0,
281 ) -> Result<OmenaSdkBuildResponseV0, OmenaError> {
282 self.ensure_snapshot(request.snapshot_id, "build")?;
283 let (style_path, style_source) = self.style_source(request.style_path.as_str())?;
284 if request.style_source != style_source {
285 return Err(sdk_error(
286 OmenaErrorClassV0::Workspace,
287 format!("build source does not match snapshot for {style_path:?}"),
288 "workspace.style-source-mismatch",
289 OmenaErrorRecoverabilityV0::Retry,
290 ));
291 }
292 request.style_path = style_path.to_string();
293 let build_options = OmenaQueryConsumerBuildOptionsV0 {
294 verification_profile: match request.verification_profile {
295 Some(OmenaSdkBuildVerificationProfileV0::Strict) => {
296 OmenaQueryBuildVerificationProfileV0::Strict
297 }
298 Some(OmenaSdkBuildVerificationProfileV0::Descriptive) | None => {
299 OmenaQueryBuildVerificationProfileV0::Descriptive
300 }
301 },
302 ..OmenaQueryConsumerBuildOptionsV0::default()
303 };
304 let default_context = crate::OmenaQueryTransformExecutionContextV0::default();
305 let context = request.context.as_ref().unwrap_or(&default_context);
306 let mut summary = execute_omena_query_consumer_build_style_source_with_context_and_options(
307 style_path,
308 style_source,
309 request.pass_ids.as_slice(),
310 context,
311 &build_options,
312 );
313 attach_omena_query_consumer_build_source_map_v3(&mut summary, style_source);
314 let verification = sdk_build_verification_summary(&summary.execution.strict_policy);
315 Ok(OmenaSdkBuildResponseV0 {
316 snapshot_id: self.snapshot_id(),
317 partition: OmenaSdkResponsePartitionV0::Public,
318 verification,
319 summary: serde_json::to_value(summary).map_err(serialize_error)?,
320 })
321 }
322
323 pub fn execute_explain(
324 &self,
325 request: OmenaSdkExplainRequestV0,
326 ) -> Result<OmenaSdkExplainResponseV0, OmenaError> {
327 self.ensure_snapshot(request.snapshot_id, "explain")?;
328 let (style_path, style_source) = self.style_source(request.style_path.as_str())?;
329 let position = parser_position(request.position.line, request.position.character)?;
330 let empty_input = EngineInputV2 {
331 version: "2".to_string(),
332 sources: Vec::new(),
333 styles: Vec::new(),
334 type_facts: Vec::new(),
335 };
336 let report = match read_omena_query_cascade_at_position(
337 style_path,
338 style_source,
339 &empty_input,
340 position,
341 ) {
342 Some(cascade) => {
343 explain_omena_query(OmenaQueryExplainInputV0::Cascade { result: &cascade })
344 }
345 None => {
346 let candidate_count =
347 summarize_omena_query_style_hover_candidates(style_path, style_source)
348 .map_or(0, |candidates| candidates.candidates.len());
349 explain_omena_query(OmenaQueryExplainInputV0::HoverTrace {
350 document_uri: style_path,
351 position: Some(position),
352 reason_code: "style-position",
353 matched: candidate_count > 0,
354 candidate_count,
355 definition_count: 0,
356 })
357 }
358 };
359 let source_identity = serde_json::json!({
360 "originalSource": style_path,
361 "line": position.line,
362 "character": position.character,
363 });
364 Ok(OmenaSdkExplainResponseV0 {
365 snapshot_id: self.snapshot_id(),
366 partition: OmenaSdkResponsePartitionV0::Public,
367 report: serde_json::json!({
368 "explanation": report,
369 "sourceIdentity": source_identity,
370 }),
371 })
372 }
373
374 fn ensure_snapshot(
375 &self,
376 requested: OmenaWorkspaceSnapshotIdV0,
377 operation: &str,
378 ) -> Result<(), OmenaError> {
379 if requested == self.snapshot_id() {
380 return Ok(());
381 }
382 Err(sdk_error(
383 OmenaErrorClassV0::Workspace,
384 format!("{operation} request does not match the current workspace snapshot"),
385 "workspace.snapshot-mismatch",
386 OmenaErrorRecoverabilityV0::Retry,
387 ))
388 }
389
390 fn style_source(&self, style_path: &str) -> Result<(&str, &str), OmenaError> {
391 let style_path = normalize_style_path(style_path);
392 self.style_sources
393 .get_key_value(style_path.as_str())
394 .map(|(path, source)| (path.as_str(), source.as_str()))
395 .ok_or_else(|| {
396 sdk_error(
397 OmenaErrorClassV0::Resolution,
398 format!("style path {style_path:?} is not present in the workspace snapshot"),
399 "workspace.style-path-not-found",
400 OmenaErrorRecoverabilityV0::UserAction,
401 )
402 })
403 }
404
405 fn style_source_inputs(&self) -> Vec<OmenaQueryStyleSourceInputV0> {
406 self.style_sources
407 .iter()
408 .map(|(style_path, style_source)| OmenaQueryStyleSourceInputV0 {
409 style_path: style_path.clone(),
410 style_source: style_source.clone(),
411 })
412 .collect()
413 }
414}
415
416fn sdk_build_verification_summary(
417 summary: &OmenaQueryTransformStrictPolicySummaryV0,
418) -> OmenaSdkBuildVerificationSummaryV0 {
419 OmenaSdkBuildVerificationSummaryV0 {
420 profile_id: summary.profile_id.clone(),
421 refused_count: summary.refused_count as u64,
422 rolled_back_count: summary.rolled_back_count as u64,
423 refusal_reasons: summary
424 .refusal_reasons
425 .iter()
426 .map(sdk_build_verification_event)
427 .collect(),
428 rollback_reasons: summary
429 .rollback_reasons
430 .iter()
431 .map(sdk_build_verification_event)
432 .collect(),
433 }
434}
435
436fn sdk_build_verification_event(
437 event: &OmenaQueryTransformStrictPolicyEventV0,
438) -> OmenaSdkBuildVerificationEventV0 {
439 OmenaSdkBuildVerificationEventV0 {
440 pass_id: event.pass_id.clone(),
441 reasons: event
442 .reasons
443 .iter()
444 .map(sdk_build_verification_reason)
445 .collect(),
446 }
447}
448
449fn sdk_build_verification_reason(
450 reason: &OmenaQueryTransformStrictPolicyReasonV0,
451) -> OmenaSdkBuildVerificationReasonV0 {
452 match reason {
453 OmenaQueryTransformStrictPolicyReasonV0::RequiredAxisUnavailable { .. } => {
454 OmenaSdkBuildVerificationReasonV0::RequiredAxisUnavailable
455 }
456 OmenaQueryTransformStrictPolicyReasonV0::CascadeEnvironmentUnavailable => {
457 OmenaSdkBuildVerificationReasonV0::CascadeEnvironmentUnavailable
458 }
459 OmenaQueryTransformStrictPolicyReasonV0::WinnerChanged { .. } => {
460 OmenaSdkBuildVerificationReasonV0::WinnerChanged
461 }
462 OmenaQueryTransformStrictPolicyReasonV0::ObservationUnavailable { .. } => {
463 OmenaSdkBuildVerificationReasonV0::ObservationUnavailable
464 }
465 OmenaQueryTransformStrictPolicyReasonV0::UnknownPass => {
466 OmenaSdkBuildVerificationReasonV0::UnknownPass
467 }
468 OmenaQueryTransformStrictPolicyReasonV0::ClosedWorldEvidenceUnavailable => {
469 OmenaSdkBuildVerificationReasonV0::ClosedWorldEvidenceUnavailable
470 }
471 OmenaQueryTransformStrictPolicyReasonV0::DecisionCoverageIncomplete => {
472 OmenaSdkBuildVerificationReasonV0::DecisionCoverageIncomplete
473 }
474 OmenaQueryTransformStrictPolicyReasonV0::ClosedWorldEvidenceIncomplete { .. }
475 | OmenaQueryTransformStrictPolicyReasonV0::LivenessNotClosed { .. }
476 | OmenaQueryTransformStrictPolicyReasonV0::EvidenceUnavailable
477 | OmenaQueryTransformStrictPolicyReasonV0::OwnershipNotSeparable { .. } => {
478 OmenaSdkBuildVerificationReasonV0::ClosedWorldEvidenceUnavailable
481 }
482 }
483}
484
485#[derive(Debug, Deserialize)]
486#[serde(rename_all = "camelCase")]
487struct OmenaSdkStyleQueryInputV0 {
488 style_path: String,
489}
490
491fn query_input(input: Option<&serde_json::Value>) -> Result<OmenaSdkStyleQueryInputV0, OmenaError> {
492 serde_json::from_value(input.cloned().unwrap_or(serde_json::Value::Null)).map_err(|error| {
493 sdk_error(
494 OmenaErrorClassV0::Input,
495 format!("SDK query input is invalid: {error}"),
496 "query.invalid-input",
497 OmenaErrorRecoverabilityV0::UserAction,
498 )
499 })
500}
501
502fn parser_position(line: i32, character: i32) -> Result<ParserPositionV0, OmenaError> {
503 let line = usize::try_from(line).map_err(|_| {
504 sdk_error(
505 OmenaErrorClassV0::Input,
506 "explain line must be non-negative",
507 "explain.invalid-position",
508 OmenaErrorRecoverabilityV0::UserAction,
509 )
510 })?;
511 let character = usize::try_from(character).map_err(|_| {
512 sdk_error(
513 OmenaErrorClassV0::Input,
514 "explain character must be non-negative",
515 "explain.invalid-position",
516 OmenaErrorRecoverabilityV0::UserAction,
517 )
518 })?;
519 Ok(ParserPositionV0 { line, character })
520}
521
522fn normalize_style_path(style_path: &str) -> String {
523 if style_path.trim().is_empty() {
524 "style.css".to_string()
525 } else {
526 style_path.to_string()
527 }
528}
529
530fn serialize_error(error: serde_json::Error) -> OmenaError {
531 sdk_error(
532 OmenaErrorClassV0::Internal,
533 format!("failed to serialize SDK workflow response: {error}"),
534 "sdk.response-serialization",
535 OmenaErrorRecoverabilityV0::Retry,
536 )
537}
538
539fn sdk_error(
540 class: OmenaErrorClassV0,
541 message: impl Into<String>,
542 code: &str,
543 recoverability: OmenaErrorRecoverabilityV0,
544) -> OmenaError {
545 OmenaError::new(
546 class,
547 message,
548 OmenaErrorContextV0 {
549 code: code.to_string(),
550 severity: OmenaErrorSeverityV0::Error,
551 recoverability,
552 evidence: Vec::new(),
553 },
554 )
555}