1#![allow(clippy::missing_errors_doc)]
4
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8use crate::platform::{SnapshotUnit, WorkspaceSnapshot};
9use crate::{
10 validate_production_chunk_graph, validate_production_runtime_artifact,
11 ApplicationSemanticModel, ComponentDiagnosticSeverity, OptimizationReportV1,
12 ProductionChunkGraph, ProductionChunkKind, ProductionRuntimeArtifactV1, RuntimeCostReportV1,
13 SemanticEntityKind, SemanticReferenceKind, SourceProvenance,
14};
15
16pub const BUILD_TRACE_TOOLING_SCHEMA_V1: &str = "presolve.build-trace";
17pub const COMPILE_COST_TOOLING_SCHEMA_V1: &str = "presolve.compile-cost-report";
18pub const ARTIFACT_GRAPH_TOOLING_SCHEMA_V1: &str = "presolve.artifact-graph";
19pub const QUERY_SNAPSHOT_TOOLING_SCHEMA_V1: &str = "presolve.query-snapshot";
20
21#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23pub struct ToolingTraceIdentityV1 {
24 pub name: String,
25 pub value: String,
26}
27
28#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum ToolingTraceStageKindV1 {
31 L3Snapshot,
32 L5IncrementalPlan,
33 L6Cache,
34 L7Workspace,
35 L8Watch,
36 L4Publication,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum ToolingTraceOutcomeV1 {
42 Succeeded,
43 Failed,
44 Skipped,
45}
46
47#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase", deny_unknown_fields)]
49pub struct ToolingBuildTraceStageV1 {
50 pub ordinal: u32,
51 pub kind: ToolingTraceStageKindV1,
52 pub outcome: ToolingTraceOutcomeV1,
53 pub identities: Vec<ToolingTraceIdentityV1>,
54}
55
56#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58pub struct ToolingBuildTraceV1 {
59 pub schema: String,
60 pub version: u32,
61 pub trace_id: String,
62 pub workspace_id: String,
63 pub compiler_contract: String,
64 pub snapshot_id: Option<String>,
65 pub outcome: ToolingTraceOutcomeV1,
66 pub stages: Vec<ToolingBuildTraceStageV1>,
67}
68
69#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase", deny_unknown_fields)]
71pub struct ToolingCompileCostReportV1 {
72 pub schema: String,
73 pub version: u32,
74 pub report_id: String,
75 pub build_id: String,
76 pub optimization_policy: String,
77 pub optimization_report_id: String,
78 pub runtime_cost_report_id: String,
79 pub optimization_report: OptimizationReportV1,
80 pub runtime_cost_report: RuntimeCostReportV1,
81}
82
83#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase", deny_unknown_fields)]
85pub struct ToolingArtifactGraphChunkV1 {
86 pub chunk_id: String,
87 pub kind: String,
88 pub module_filename: String,
89 pub activation_roots: Vec<String>,
90 pub root_kind: Option<String>,
91 pub program_fingerprints: Vec<String>,
92 pub registration_only: bool,
93}
94
95#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase", deny_unknown_fields)]
97pub struct ToolingArtifactGraphDependencyV1 {
98 pub dependent_chunk_id: String,
99 pub dependency_chunk_id: String,
100}
101
102#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase", deny_unknown_fields)]
104pub struct ToolingArtifactGraphActivationV1 {
105 pub activation_root_id: String,
106 pub root_chunk_id: String,
107 pub shared_chunk_ids: Vec<String>,
108}
109
110#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase", deny_unknown_fields)]
112pub struct ToolingArtifactGraphV1 {
113 pub schema: String,
114 pub version: u32,
115 pub graph_id: String,
116 pub build_id: String,
117 pub runtime_protocol_version: u32,
118 pub optimization_policy: String,
119 pub artifact_checksum: String,
120 pub chunks: Vec<ToolingArtifactGraphChunkV1>,
121 pub dependencies: Vec<ToolingArtifactGraphDependencyV1>,
122 pub activations: Vec<ToolingArtifactGraphActivationV1>,
123}
124
125#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase", deny_unknown_fields)]
127pub struct ToolingQuerySnapshotSourceUnitV1 {
128 pub source_unit_id: String,
129 pub source_revision_id: String,
130 pub source_length: u64,
131}
132
133#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase", deny_unknown_fields)]
135pub struct ToolingQueryRangeV1 {
136 pub source_unit_id: String,
137 pub start: u64,
138 pub end: u64,
139}
140
141#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum ToolingQuerySemanticKindV1 {
144 Component,
145 StateField,
146 Method,
147 Context,
148 Provider,
149 Consumer,
150 Form,
151 FormField,
152 FormFieldBinding,
153 ValidationRule,
154 Slot,
155 ComponentInvocation,
156 ComponentInstance,
157 BlockedComponentInstance,
158 SlotContentFragment,
159 SlotOutlet,
160 Computed,
161 Effect,
162 Parameter,
163 LocalVariable,
164 Action,
165 EventHandler,
166 Template,
167 TemplateEntity,
168}
169
170#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum ToolingQueryReferenceKindV1 {
173 ActionState,
174 ComputedState,
175 ComputedComputed,
176 ComputedResource,
177 EffectState,
178 EffectComputed,
179 ProvidesContext,
180 ConsumesContext,
181 ResolvesToProvider,
182 EventMethod,
183 TemplateState,
184 TemplateComputed,
185 TemplateLocal,
186 FieldBindingField,
187 FieldBindingForm,
188 ValidationRuleField,
189}
190
191#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
192#[serde(rename_all = "snake_case")]
193pub enum ToolingQueryDiagnosticSeverityV1 {
194 Error,
195}
196
197#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase", deny_unknown_fields)]
199pub struct ToolingQuerySemanticRecordV1 {
200 pub query_semantic_id: String,
201 pub kind: ToolingQuerySemanticKindV1,
202 pub range: ToolingQueryRangeV1,
203}
204
205#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase", deny_unknown_fields)]
207pub struct ToolingQueryReferenceV1 {
208 pub kind: ToolingQueryReferenceKindV1,
209 pub source_query_semantic_id: String,
210 pub target_query_semantic_id: String,
211 pub range: ToolingQueryRangeV1,
212}
213
214#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase", deny_unknown_fields)]
216pub struct ToolingQueryDiagnosticSecondaryV1 {
217 pub range: ToolingQueryRangeV1,
218 pub message: String,
219}
220
221#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
222#[serde(rename_all = "camelCase", deny_unknown_fields)]
223pub struct ToolingQueryDiagnosticV1 {
224 pub code: String,
225 pub severity: ToolingQueryDiagnosticSeverityV1,
226 pub message: String,
227 pub primary_range: Option<ToolingQueryRangeV1>,
228 pub secondary: Vec<ToolingQueryDiagnosticSecondaryV1>,
229}
230
231#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase", deny_unknown_fields)]
233pub struct ToolingQuerySnapshotV1 {
234 pub schema: String,
235 pub version: u32,
236 pub query_snapshot_id: String,
237 pub workspace_id: String,
238 pub snapshot_id: String,
239 pub source_units: Vec<ToolingQuerySnapshotSourceUnitV1>,
240 pub semantic_records: Vec<ToolingQuerySemanticRecordV1>,
241 pub references: Vec<ToolingQueryReferenceV1>,
242 pub diagnostics: Vec<ToolingQueryDiagnosticV1>,
243}
244
245#[derive(Clone, Debug, Eq, PartialEq)]
246pub enum ToolingProductValidationErrorV1 {
247 InvalidTraceProvenance,
248 InvalidCostProvenance,
249 InvalidSourceReport,
250 InvalidArtifactGraphProvenance,
251 ArtifactGraphTopologyDisagreement,
252 InvalidQuerySnapshotProvenance,
253 InvalidQuerySnapshotBinding,
254 Noncanonical,
255}
256
257pub fn build_tooling_build_trace_v1(
258 workspace_id: String,
259 compiler_contract: String,
260 snapshot_id: Option<String>,
261 outcome: ToolingTraceOutcomeV1,
262 stages: Vec<ToolingBuildTraceStageV1>,
263) -> Result<ToolingBuildTraceV1, ToolingProductValidationErrorV1> {
264 let mut product = ToolingBuildTraceV1 {
265 schema: BUILD_TRACE_TOOLING_SCHEMA_V1.into(),
266 version: 1,
267 trace_id: String::new(),
268 workspace_id,
269 compiler_contract,
270 snapshot_id,
271 outcome,
272 stages,
273 };
274 validate_trace(&product)?;
275 product.trace_id = identity_without_field(&product, "traceId");
276 Ok(product)
277}
278
279pub fn build_tooling_compile_cost_report_v1(
280 optimization_report: OptimizationReportV1,
281 runtime_cost_report: RuntimeCostReportV1,
282) -> Result<ToolingCompileCostReportV1, ToolingProductValidationErrorV1> {
283 if optimization_report.build_id != runtime_cost_report.build_id {
284 return Err(ToolingProductValidationErrorV1::InvalidCostProvenance);
285 }
286 if optimization_report.schema_version != 1
287 || runtime_cost_report.schema_version != 1
288 || optimization_report.validation_status != "valid"
289 {
290 return Err(ToolingProductValidationErrorV1::InvalidSourceReport);
291 }
292 let mut product = ToolingCompileCostReportV1 {
293 schema: COMPILE_COST_TOOLING_SCHEMA_V1.into(),
294 version: 1,
295 report_id: String::new(),
296 build_id: optimization_report.build_id.to_string(),
297 optimization_policy: optimization_report.optimization_policy.to_string(),
298 optimization_report_id: sha256_json(&optimization_report),
299 runtime_cost_report_id: sha256_json(&runtime_cost_report),
300 optimization_report,
301 runtime_cost_report,
302 };
303 product.report_id = identity_without_field(&product, "reportId");
304 Ok(product)
305}
306
307pub fn build_tooling_artifact_graph_v1(
308 graph: &ProductionChunkGraph,
309 artifact: &ProductionRuntimeArtifactV1,
310) -> Result<ToolingArtifactGraphV1, ToolingProductValidationErrorV1> {
311 validate_production_chunk_graph(graph)
312 .map_err(|_| ToolingProductValidationErrorV1::ArtifactGraphTopologyDisagreement)?;
313 if !validate_production_runtime_artifact(artifact, &artifact.build_id).is_empty()
314 || artifact.entry.eager_chunk_id != graph.eager_chunk_id
315 {
316 return Err(ToolingProductValidationErrorV1::InvalidArtifactGraphProvenance);
317 }
318 let mut product = ToolingArtifactGraphV1 {
319 schema: ARTIFACT_GRAPH_TOOLING_SCHEMA_V1.into(),
320 version: 1,
321 graph_id: String::new(),
322 build_id: artifact.build_id.to_string(),
323 runtime_protocol_version: artifact.runtime_protocol_version,
324 optimization_policy: artifact.optimization_policy.to_string(),
325 artifact_checksum: artifact.integrity.artifact_checksum.clone(),
326 chunks: graph
327 .chunks
328 .iter()
329 .map(|chunk| ToolingArtifactGraphChunkV1 {
330 chunk_id: chunk.id.to_string(),
331 kind: chunk_kind(chunk.kind).into(),
332 module_filename: chunk.provisional_module_filename.clone(),
333 activation_roots: sorted(chunk.activation_roots.clone()),
334 root_kind: chunk.root_kind.clone(),
335 program_fingerprints: sorted(
336 chunk.programs.iter().map(ToString::to_string).collect(),
337 ),
338 registration_only: chunk.registration_only,
339 })
340 .collect(),
341 dependencies: graph
342 .dependencies
343 .iter()
344 .map(|edge| ToolingArtifactGraphDependencyV1 {
345 dependent_chunk_id: edge.dependent_chunk_id.to_string(),
346 dependency_chunk_id: edge.dependency_chunk_id.to_string(),
347 })
348 .collect(),
349 activations: graph
350 .activation_plans
351 .iter()
352 .map(|plan| ToolingArtifactGraphActivationV1 {
353 activation_root_id: plan.activation_root_id.clone(),
354 root_chunk_id: plan.root_chunk_id.to_string(),
355 shared_chunk_ids: sorted(
356 plan.shared_chunk_ids
357 .iter()
358 .map(ToString::to_string)
359 .collect(),
360 ),
361 })
362 .collect(),
363 };
364 product.chunks.sort_by(|a, b| a.chunk_id.cmp(&b.chunk_id));
365 product.dependencies.sort();
366 product.dependencies.dedup();
367 product
368 .activations
369 .sort_by(|a, b| a.activation_root_id.cmp(&b.activation_root_id));
370 product.graph_id = identity_without_field(&product, "graphId");
371 Ok(product)
372}
373
374pub(crate) fn build_tooling_query_snapshot_v1(
375 snapshot: &WorkspaceSnapshot,
376 model: &ApplicationSemanticModel,
377) -> Result<ToolingQuerySnapshotV1, ToolingProductValidationErrorV1> {
378 snapshot
379 .validate()
380 .map_err(|_| ToolingProductValidationErrorV1::InvalidQuerySnapshotBinding)?;
381 let source_lengths = snapshot
382 .units
383 .iter()
384 .map(|unit| (unit.path.as_str(), unit))
385 .collect::<std::collections::BTreeMap<_, _>>();
386 let make_range =
387 |provenance: &SourceProvenance| query_range_from_provenance(provenance, &source_lengths);
388 let mut query_ids = std::collections::BTreeMap::new();
389 let mut semantic_records = Vec::new();
390 for semantic_id in model.ownership.keys() {
391 let Some(provenance) = model.provenance(semantic_id) else {
392 continue;
393 };
394 let entity = model
395 .entity(semantic_id)
396 .ok_or(ToolingProductValidationErrorV1::InvalidQuerySnapshotProvenance)?;
397 let query_semantic_id = query_semantic_id(semantic_id.as_str());
398 query_ids.insert(semantic_id.clone(), query_semantic_id.clone());
399 semantic_records.push(ToolingQuerySemanticRecordV1 {
400 query_semantic_id,
401 kind: query_semantic_kind(entity.kind()),
402 range: make_range(provenance)?,
403 });
404 }
405 semantic_records.sort_by_key(query_semantic_record_key);
406
407 let mut references = Vec::new();
408 for reference in &model.references {
409 let (Some(source_query_semantic_id), Some(target_query_semantic_id)) = (
410 query_ids.get(&reference.source),
411 query_ids.get(&reference.target),
412 ) else {
413 continue;
414 };
415 references.push(ToolingQueryReferenceV1 {
416 kind: query_reference_kind(reference.kind),
417 source_query_semantic_id: source_query_semantic_id.clone(),
418 target_query_semantic_id: target_query_semantic_id.clone(),
419 range: make_range(&reference.provenance)?,
420 });
421 }
422 references.sort_by_key(query_reference_key);
423 references.dedup();
424
425 let mut diagnostics = model
426 .diagnostics
427 .iter()
428 .map(|diagnostic| {
429 let mut secondary = diagnostic
430 .secondary_labels
431 .iter()
432 .map(|label| {
433 Ok(ToolingQueryDiagnosticSecondaryV1 {
434 range: make_range(&label.provenance)?,
435 message: label.message.clone(),
436 })
437 })
438 .collect::<Result<Vec<_>, ToolingProductValidationErrorV1>>()?;
439 secondary.sort_by_key(query_diagnostic_secondary_key);
440 secondary.dedup();
441 Ok(ToolingQueryDiagnosticV1 {
442 code: diagnostic.code.clone(),
443 severity: query_diagnostic_severity(diagnostic.severity),
444 message: diagnostic.message.clone(),
445 primary_range: diagnostic.provenance.as_ref().map(make_range).transpose()?,
446 secondary,
447 })
448 })
449 .collect::<Result<Vec<_>, ToolingProductValidationErrorV1>>()?;
450 diagnostics.sort_by_key(query_diagnostic_key);
451
452 let mut product = ToolingQuerySnapshotV1 {
453 schema: QUERY_SNAPSHOT_TOOLING_SCHEMA_V1.into(),
454 version: 1,
455 query_snapshot_id: String::new(),
456 workspace_id: snapshot.workspace_id.as_str().into(),
457 snapshot_id: snapshot.snapshot_id.as_str().into(),
458 source_units: snapshot
459 .units
460 .iter()
461 .map(|unit| ToolingQuerySnapshotSourceUnitV1 {
462 source_unit_id: unit.source_unit_id.as_str().into(),
463 source_revision_id: unit.source_revision_id.as_str().into(),
464 source_length: unit.source_length,
465 })
466 .collect(),
467 semantic_records,
468 references,
469 diagnostics,
470 };
471 product.source_units.sort();
472 validate_query_snapshot(&product)?;
473 product.query_snapshot_id = identity_without_field(&product, "querySnapshotId");
474 Ok(product)
475}
476
477#[must_use]
478pub fn tooling_build_trace_json_v1(value: &ToolingBuildTraceV1) -> String {
479 canonical_json(value)
480}
481#[must_use]
482pub fn tooling_compile_cost_report_json_v1(value: &ToolingCompileCostReportV1) -> String {
483 canonical_json(value)
484}
485#[must_use]
486pub fn tooling_artifact_graph_json_v1(value: &ToolingArtifactGraphV1) -> String {
487 canonical_json(value)
488}
489#[must_use]
490pub fn tooling_query_snapshot_json_v1(value: &ToolingQuerySnapshotV1) -> String {
491 canonical_json(value)
492}
493
494pub fn decode_tooling_build_trace_v1(
495 bytes: &[u8],
496) -> Result<ToolingBuildTraceV1, ToolingProductValidationErrorV1> {
497 let value: ToolingBuildTraceV1 =
498 serde_json::from_slice(bytes).map_err(|_| ToolingProductValidationErrorV1::Noncanonical)?;
499 validate_trace(&value)?;
500 (tooling_build_trace_json_v1(&value).as_bytes() == bytes
501 && value.trace_id == identity_without_field(&value, "traceId"))
502 .then_some(value)
503 .ok_or(ToolingProductValidationErrorV1::Noncanonical)
504}
505pub fn decode_tooling_compile_cost_report_v1(
506 bytes: &[u8],
507) -> Result<ToolingCompileCostReportV1, ToolingProductValidationErrorV1> {
508 let value: ToolingCompileCostReportV1 =
509 serde_json::from_slice(bytes).map_err(|_| ToolingProductValidationErrorV1::Noncanonical)?;
510 let rebuilt = build_tooling_compile_cost_report_v1(
511 value.optimization_report.clone(),
512 value.runtime_cost_report.clone(),
513 )?;
514 (rebuilt == value && tooling_compile_cost_report_json_v1(&value).as_bytes() == bytes)
515 .then_some(value)
516 .ok_or(ToolingProductValidationErrorV1::Noncanonical)
517}
518pub fn decode_tooling_artifact_graph_v1(
519 bytes: &[u8],
520) -> Result<ToolingArtifactGraphV1, ToolingProductValidationErrorV1> {
521 let value: ToolingArtifactGraphV1 =
522 serde_json::from_slice(bytes).map_err(|_| ToolingProductValidationErrorV1::Noncanonical)?;
523 if value.schema != ARTIFACT_GRAPH_TOOLING_SCHEMA_V1
524 || value.version != 1
525 || value.graph_id != identity_without_field(&value, "graphId")
526 || tooling_artifact_graph_json_v1(&value).as_bytes() != bytes
527 {
528 return Err(ToolingProductValidationErrorV1::Noncanonical);
529 }
530 let canonical = value
531 .chunks
532 .windows(2)
533 .all(|p| p[0].chunk_id < p[1].chunk_id)
534 && value.dependencies.windows(2).all(|p| p[0] < p[1])
535 && value
536 .activations
537 .windows(2)
538 .all(|p| p[0].activation_root_id < p[1].activation_root_id);
539 canonical
540 .then_some(value)
541 .ok_or(ToolingProductValidationErrorV1::ArtifactGraphTopologyDisagreement)
542}
543pub fn decode_tooling_query_snapshot_v1(
544 bytes: &[u8],
545) -> Result<ToolingQuerySnapshotV1, ToolingProductValidationErrorV1> {
546 let value: ToolingQuerySnapshotV1 =
547 serde_json::from_slice(bytes).map_err(|_| ToolingProductValidationErrorV1::Noncanonical)?;
548 validate_query_snapshot(&value)?;
549 (value.query_snapshot_id == identity_without_field(&value, "querySnapshotId")
550 && tooling_query_snapshot_json_v1(&value).as_bytes() == bytes)
551 .then_some(value)
552 .ok_or(ToolingProductValidationErrorV1::Noncanonical)
553}
554
555fn validate_trace(value: &ToolingBuildTraceV1) -> Result<(), ToolingProductValidationErrorV1> {
556 if value.schema != BUILD_TRACE_TOOLING_SCHEMA_V1
557 || value.version != 1
558 || value.workspace_id.is_empty()
559 || value.compiler_contract.is_empty()
560 || value
561 .stages
562 .windows(2)
563 .any(|p| p[0].ordinal >= p[1].ordinal)
564 || value.stages.iter().any(|stage| {
565 stage.identities.windows(2).any(|p| p[0].name >= p[1].name)
566 || stage
567 .identities
568 .iter()
569 .any(|id| id.value.is_empty() || forbidden(&id.value))
570 })
571 {
572 return Err(ToolingProductValidationErrorV1::InvalidTraceProvenance);
573 }
574 Ok(())
575}
576fn validate_query_snapshot(
577 value: &ToolingQuerySnapshotV1,
578) -> Result<(), ToolingProductValidationErrorV1> {
579 if value.schema != QUERY_SNAPSHOT_TOOLING_SCHEMA_V1
580 || value.version != 1
581 || value.workspace_id.is_empty()
582 || value.snapshot_id.is_empty()
583 || value.source_units.is_empty()
584 || value
585 .source_units
586 .windows(2)
587 .any(|pair| pair[0].source_unit_id >= pair[1].source_unit_id)
588 || value
589 .source_units
590 .iter()
591 .any(|unit| unit.source_unit_id.is_empty() || unit.source_revision_id.is_empty())
592 {
593 return Err(ToolingProductValidationErrorV1::InvalidQuerySnapshotBinding);
594 }
595 let lengths = value
596 .source_units
597 .iter()
598 .map(|unit| (unit.source_unit_id.as_str(), unit.source_length))
599 .collect::<std::collections::BTreeMap<_, _>>();
600 let valid_range = |range: &ToolingQueryRangeV1| {
601 lengths
602 .get(range.source_unit_id.as_str())
603 .is_some_and(|length| range.start <= range.end && range.end <= *length)
604 };
605 if value
606 .semantic_records
607 .iter()
608 .any(|record| record.query_semantic_id.is_empty() || !valid_range(&record.range))
609 || value
610 .semantic_records
611 .windows(2)
612 .any(|pair| query_semantic_record_key(&pair[0]) >= query_semantic_record_key(&pair[1]))
613 {
614 return Err(ToolingProductValidationErrorV1::InvalidQuerySnapshotProvenance);
615 }
616 let query_semantic_ids = value
617 .semantic_records
618 .iter()
619 .map(|record| record.query_semantic_id.as_str())
620 .collect::<std::collections::BTreeSet<_>>();
621 if query_semantic_ids.len() != value.semantic_records.len()
622 || value.references.iter().any(|reference| {
623 !valid_range(&reference.range)
624 || !query_semantic_ids.contains(reference.source_query_semantic_id.as_str())
625 || !query_semantic_ids.contains(reference.target_query_semantic_id.as_str())
626 })
627 || value
628 .references
629 .windows(2)
630 .any(|pair| query_reference_key(&pair[0]) >= query_reference_key(&pair[1]))
631 || value.diagnostics.iter().any(|diagnostic| {
632 diagnostic.code.is_empty()
633 || diagnostic.message.is_empty()
634 || diagnostic
635 .primary_range
636 .as_ref()
637 .is_some_and(|range| !valid_range(range))
638 || diagnostic
639 .secondary
640 .iter()
641 .any(|secondary| secondary.message.is_empty() || !valid_range(&secondary.range))
642 || diagnostic.secondary.windows(2).any(|pair| {
643 query_diagnostic_secondary_key(&pair[0])
644 >= query_diagnostic_secondary_key(&pair[1])
645 })
646 })
647 || value
648 .diagnostics
649 .windows(2)
650 .any(|pair| query_diagnostic_key(&pair[0]) > query_diagnostic_key(&pair[1]))
651 {
652 return Err(ToolingProductValidationErrorV1::InvalidQuerySnapshotProvenance);
653 }
654 Ok(())
655}
656fn query_range_from_provenance(
657 provenance: &SourceProvenance,
658 source_units: &std::collections::BTreeMap<&str, &SnapshotUnit>,
659) -> Result<ToolingQueryRangeV1, ToolingProductValidationErrorV1> {
660 let path = provenance.path.to_string_lossy();
661 let source_unit = source_units
662 .get(path.as_ref())
663 .ok_or(ToolingProductValidationErrorV1::InvalidQuerySnapshotProvenance)?;
664 let start = u64::try_from(provenance.span.start)
665 .map_err(|_| ToolingProductValidationErrorV1::InvalidQuerySnapshotProvenance)?;
666 let end = u64::try_from(provenance.span.end)
667 .map_err(|_| ToolingProductValidationErrorV1::InvalidQuerySnapshotProvenance)?;
668 (start <= end && end <= source_unit.source_length)
669 .then_some(ToolingQueryRangeV1 {
670 source_unit_id: source_unit.source_unit_id.as_str().into(),
671 start,
672 end,
673 })
674 .ok_or(ToolingProductValidationErrorV1::InvalidQuerySnapshotProvenance)
675}
676fn query_semantic_id(semantic_id: &str) -> String {
677 let mut bytes = b"query-semantic-v1\0".to_vec();
678 bytes.extend_from_slice(semantic_id.as_bytes());
679 format!("query-semantic:sha256:{:x}", Sha256::digest(bytes))
680}
681fn query_semantic_kind(kind: SemanticEntityKind) -> ToolingQuerySemanticKindV1 {
682 match kind {
683 SemanticEntityKind::Component => ToolingQuerySemanticKindV1::Component,
684 SemanticEntityKind::StateField => ToolingQuerySemanticKindV1::StateField,
685 SemanticEntityKind::Method => ToolingQuerySemanticKindV1::Method,
686 SemanticEntityKind::Context => ToolingQuerySemanticKindV1::Context,
687 SemanticEntityKind::Provider => ToolingQuerySemanticKindV1::Provider,
688 SemanticEntityKind::Consumer => ToolingQuerySemanticKindV1::Consumer,
689 SemanticEntityKind::Form => ToolingQuerySemanticKindV1::Form,
690 SemanticEntityKind::FormField => ToolingQuerySemanticKindV1::FormField,
691 SemanticEntityKind::FormFieldBinding => ToolingQuerySemanticKindV1::FormFieldBinding,
692 SemanticEntityKind::ValidationRule => ToolingQuerySemanticKindV1::ValidationRule,
693 SemanticEntityKind::Slot => ToolingQuerySemanticKindV1::Slot,
694 SemanticEntityKind::ComponentInvocation => ToolingQuerySemanticKindV1::ComponentInvocation,
695 SemanticEntityKind::ComponentInstance => ToolingQuerySemanticKindV1::ComponentInstance,
696 SemanticEntityKind::BlockedComponentInstance => {
697 ToolingQuerySemanticKindV1::BlockedComponentInstance
698 }
699 SemanticEntityKind::SlotContentFragment => ToolingQuerySemanticKindV1::SlotContentFragment,
700 SemanticEntityKind::SlotOutlet => ToolingQuerySemanticKindV1::SlotOutlet,
701 SemanticEntityKind::Computed => ToolingQuerySemanticKindV1::Computed,
702 SemanticEntityKind::Effect => ToolingQuerySemanticKindV1::Effect,
703 SemanticEntityKind::Parameter => ToolingQuerySemanticKindV1::Parameter,
704 SemanticEntityKind::LocalVariable => ToolingQuerySemanticKindV1::LocalVariable,
705 SemanticEntityKind::Action => ToolingQuerySemanticKindV1::Action,
706 SemanticEntityKind::EventHandler => ToolingQuerySemanticKindV1::EventHandler,
707 SemanticEntityKind::Template => ToolingQuerySemanticKindV1::Template,
708 SemanticEntityKind::TemplateEntity => ToolingQuerySemanticKindV1::TemplateEntity,
709 }
710}
711fn query_reference_kind(kind: SemanticReferenceKind) -> ToolingQueryReferenceKindV1 {
712 match kind {
713 SemanticReferenceKind::ActionState => ToolingQueryReferenceKindV1::ActionState,
714 SemanticReferenceKind::ComputedState => ToolingQueryReferenceKindV1::ComputedState,
715 SemanticReferenceKind::ComputedComputed => ToolingQueryReferenceKindV1::ComputedComputed,
716 SemanticReferenceKind::ComputedResource => ToolingQueryReferenceKindV1::ComputedResource,
717 SemanticReferenceKind::EffectState => ToolingQueryReferenceKindV1::EffectState,
718 SemanticReferenceKind::EffectComputed => ToolingQueryReferenceKindV1::EffectComputed,
719 SemanticReferenceKind::ProvidesContext => ToolingQueryReferenceKindV1::ProvidesContext,
720 SemanticReferenceKind::ConsumesContext => ToolingQueryReferenceKindV1::ConsumesContext,
721 SemanticReferenceKind::ResolvesToProvider => {
722 ToolingQueryReferenceKindV1::ResolvesToProvider
723 }
724 SemanticReferenceKind::EventMethod => ToolingQueryReferenceKindV1::EventMethod,
725 SemanticReferenceKind::TemplateState => ToolingQueryReferenceKindV1::TemplateState,
726 SemanticReferenceKind::TemplateComputed => ToolingQueryReferenceKindV1::TemplateComputed,
727 SemanticReferenceKind::TemplateLocal => ToolingQueryReferenceKindV1::TemplateLocal,
728 SemanticReferenceKind::FieldBindingField => ToolingQueryReferenceKindV1::FieldBindingField,
729 SemanticReferenceKind::FieldBindingForm => ToolingQueryReferenceKindV1::FieldBindingForm,
730 SemanticReferenceKind::ValidationRuleField => {
731 ToolingQueryReferenceKindV1::ValidationRuleField
732 }
733 }
734}
735fn query_diagnostic_severity(
736 severity: ComponentDiagnosticSeverity,
737) -> ToolingQueryDiagnosticSeverityV1 {
738 match severity {
739 ComponentDiagnosticSeverity::Error => ToolingQueryDiagnosticSeverityV1::Error,
740 }
741}
742fn query_semantic_record_key(record: &ToolingQuerySemanticRecordV1) -> (String, u64, u64, String) {
743 (
744 record.range.source_unit_id.clone(),
745 record.range.start,
746 record.range.end,
747 record.query_semantic_id.clone(),
748 )
749}
750fn query_reference_key(
751 reference: &ToolingQueryReferenceV1,
752) -> (String, u64, u64, String, String, String) {
753 (
754 reference.range.source_unit_id.clone(),
755 reference.range.start,
756 reference.range.end,
757 reference.source_query_semantic_id.clone(),
758 reference.target_query_semantic_id.clone(),
759 format!("{:?}", reference.kind),
760 )
761}
762fn query_diagnostic_secondary_key(
763 secondary: &ToolingQueryDiagnosticSecondaryV1,
764) -> (String, u64, u64, String) {
765 (
766 secondary.range.source_unit_id.clone(),
767 secondary.range.start,
768 secondary.range.end,
769 secondary.message.clone(),
770 )
771}
772fn query_diagnostic_key(
773 diagnostic: &ToolingQueryDiagnosticV1,
774) -> (String, u64, u64, String, String) {
775 let range = diagnostic.primary_range.as_ref();
776 (
777 range.map_or_else(String::new, |range| range.source_unit_id.clone()),
778 range.map_or(0, |range| range.start),
779 range.map_or(0, |range| range.end),
780 diagnostic.code.clone(),
781 diagnostic.message.clone(),
782 )
783}
784fn forbidden(value: &str) -> bool {
785 ["/", "\\", "timestamp", "duration", "millisecond"]
786 .iter()
787 .any(|needle| value.contains(needle))
788}
789fn sorted(mut values: Vec<String>) -> Vec<String> {
790 values.sort();
791 values.dedup();
792 values
793}
794fn chunk_kind(kind: ProductionChunkKind) -> &'static str {
795 match kind {
796 ProductionChunkKind::Eager => "eager",
797 ProductionChunkKind::Root => "root",
798 ProductionChunkKind::Shared => "shared",
799 }
800}
801fn canonical_json<T: Serialize>(value: &T) -> String {
802 serde_json::to_string(value).expect("tooling product serializes") + "\n"
803}
804fn sha256_json<T: Serialize>(value: &T) -> String {
805 format!(
806 "{:x}",
807 Sha256::digest(serde_json::to_vec(value).expect("tooling product serializes"))
808 )
809}
810fn identity_without_field<T: Serialize>(value: &T, field: &str) -> String {
811 let mut object = serde_json::to_value(value).expect("tooling product serializes");
812 object
813 .as_object_mut()
814 .expect("tooling product is object")
815 .remove(field);
816 format!(
817 "{:x}",
818 Sha256::digest(serde_json::to_vec(&object).expect("tooling product serializes"))
819 )
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825 use crate::platform::{
826 derive_workspace_id_v1, CacheLimits, CancellationToken, CompilationOutcome,
827 CompileWorkspaceRequest, CompilerSessionState, RequestedCompilationMode, WorkspaceInput,
828 WorkspaceSource,
829 };
830 use crate::{
831 build_production_reports, build_production_runtime_artifact,
832 extract_production_chunk_graph, ExecutableProgramFingerprint, ProductionReportInputs,
833 ProductionRootChunkInput, ResumeBoundaryId, ResumeBuildId, ResumeManifest,
834 SharedChunkCandidatePlan,
835 };
836 use std::str::FromStr;
837
838 fn phase_k_products() -> (
839 ProductionChunkGraph,
840 ProductionRuntimeArtifactV1,
841 OptimizationReportV1,
842 RuntimeCostReportV1,
843 ) {
844 let manifest = ResumeManifest {
845 schema_version: 6,
846 build_id: ResumeBuildId::zero_sentinel(),
847 snapshot_schema_version: 1,
848 runtime_protocol_version: 1,
849 application_root_boundary_id: ResumeBoundaryId::from_str("resume-boundary:root")
850 .expect("boundary"),
851 boundaries: Vec::new(),
852 slot_schemas: Vec::new(),
853 capture_programs: Vec::new(),
854 restore_programs: Vec::new(),
855 chunks: Vec::new(),
856 activations: Vec::new(),
857 anchors: Vec::new(),
858 events: Vec::new(),
859 phase_i_component_resume_records: Vec::new(),
860 phase_i_form_resume_records: Vec::new(),
861 };
862 let graph = extract_production_chunk_graph(
863 &SharedChunkCandidatePlan {
864 candidates: Vec::new(),
865 rejections: Vec::new(),
866 },
867 &[ProductionRootChunkInput {
868 activation_root_id: "root".into(),
869 root_kind: "interaction".into(),
870 programs: vec![ExecutableProgramFingerprint::for_canonical_opcode_stream(
871 b"a",
872 )],
873 }],
874 )
875 .expect("graph")
876 .0;
877 let artifact = build_production_runtime_artifact(&manifest, &graph).expect("artifact");
878 let (optimization, cost) = build_production_reports(
879 &artifact,
880 &graph,
881 &ProductionReportInputs {
882 dead_products_removed: 1,
883 constants_pooled: 2,
884 programs_deduplicated: 3,
885 shared_candidates_rejected: 0,
886 binding_writes_coalesced: 4,
887 development_bytes: 100,
888 production_bytes: 80,
889 cold_init_operation_count: 0,
890 resume_restore_operation_count: 0,
891 max_action_batch_operation_count: 0,
892 max_scheduler_batch_width: 0,
893 max_dom_patch_count_per_action: 0,
894 retained_slot_count: 0,
895 },
896 );
897 (graph, artifact, optimization, cost)
898 }
899
900 #[test]
901 fn l11f_trace_is_canonical_source_free_and_strict() {
902 let trace = build_tooling_build_trace_v1(
903 "workspace:fixture".into(),
904 "compiler-contract:v1".into(),
905 Some("snapshot:fixture".into()),
906 ToolingTraceOutcomeV1::Succeeded,
907 vec![ToolingBuildTraceStageV1 {
908 ordinal: 0,
909 kind: ToolingTraceStageKindV1::L3Snapshot,
910 outcome: ToolingTraceOutcomeV1::Succeeded,
911 identities: vec![ToolingTraceIdentityV1 {
912 name: "snapshot_id".into(),
913 value: "snapshot:fixture".into(),
914 }],
915 }],
916 )
917 .unwrap();
918 let bytes = tooling_build_trace_json_v1(&trace);
919 assert_eq!(
920 decode_tooling_build_trace_v1(bytes.as_bytes()).unwrap(),
921 trace
922 );
923 assert!(!bytes.contains("timestamp"));
924 assert!(decode_tooling_build_trace_v1(bytes.trim_end().as_bytes()).is_err());
925 }
926
927 #[test]
928 fn l11f_cost_and_artifact_graph_are_canonical_source_free_products() {
929 let (graph, artifact, optimization, cost) = phase_k_products();
930 let report = build_tooling_compile_cost_report_v1(optimization, cost).unwrap();
931 let report_bytes = tooling_compile_cost_report_json_v1(&report);
932 assert_eq!(
933 decode_tooling_compile_cost_report_v1(report_bytes.as_bytes()).unwrap(),
934 report
935 );
936 assert!(!report_bytes.contains("timestamp"));
937
938 let artifact_graph = build_tooling_artifact_graph_v1(&graph, &artifact).unwrap();
939 let graph_bytes = tooling_artifact_graph_json_v1(&artifact_graph);
940 assert_eq!(
941 decode_tooling_artifact_graph_v1(graph_bytes.as_bytes()).unwrap(),
942 artifact_graph
943 );
944 assert!(!graph_bytes.contains("production/"));
945 }
946
947 #[test]
948 fn l12c_query_snapshot_is_compiler_produced_source_free_and_strict() {
949 let source = r#"@component("x-query-fixture")
950class QueryFixture extends Component {
951 value = state(1)
952 render() { return <main>{this.value}</main>; }
953}
954"#;
955 let workspace = WorkspaceInput::new(vec![WorkspaceSource {
956 path: "src/QueryFixture.tsx".into(),
957 source: source.into(),
958 language: None,
959 }]);
960 let workspace_id = derive_workspace_id_v1(&workspace.configuration).unwrap();
961 let mut session = CompilerSessionState::new(
962 workspace_id,
963 workspace.compiler_contract.clone(),
964 CacheLimits::default(),
965 );
966 let CompilationOutcome::Committed(result) =
967 session.compile_workspace(CompileWorkspaceRequest {
968 workspace,
969 mode: RequestedCompilationMode::Full,
970 cancellation: CancellationToken::new(),
971 })
972 else {
973 panic!("query snapshot fixture compilation must commit");
974 };
975 let snapshot = result.query_snapshot.as_ref().clone();
976 let bytes = tooling_query_snapshot_json_v1(&snapshot);
977 assert_eq!(
978 decode_tooling_query_snapshot_v1(bytes.as_bytes()).unwrap(),
979 snapshot
980 );
981 assert_eq!(
982 bytes,
983 include_str!("../fixtures/tooling/query-snapshot-v1.json")
984 );
985 assert_eq!(snapshot.workspace_id, result.snapshot.workspace_id.as_str());
986 assert_eq!(snapshot.snapshot_id, result.snapshot.snapshot_id.as_str());
987 assert!(!bytes.contains("QueryFixture.tsx"), "{bytes}");
988 assert!(!bytes.contains("x-query-fixture"));
989 assert!(decode_tooling_query_snapshot_v1(bytes.trim_end().as_bytes()).is_err());
990 assert!(decode_tooling_query_snapshot_v1(
991 bytes
992 .replacen("query-semantic:", "query-semantiX:", 1)
993 .as_bytes()
994 )
995 .is_err());
996 }
997
998 #[test]
999 fn l12c_query_snapshot_is_input_enumeration_independent() {
1000 let alpha = WorkspaceSource {
1001 path: "src/Alpha.tsx".into(),
1002 source: "@component(\"x-alpha\") class Alpha extends Component { render() { return <main />; } }\n".into(),
1003 language: None,
1004 };
1005 let beta = WorkspaceSource {
1006 path: "src/Beta.tsx".into(),
1007 source: "@component(\"x-beta\") class Beta extends Component { render() { return <aside />; } }\n".into(),
1008 language: None,
1009 };
1010 let compile = |sources| {
1011 let workspace = WorkspaceInput::new(sources);
1012 let workspace_id = derive_workspace_id_v1(&workspace.configuration).unwrap();
1013 let mut session = CompilerSessionState::new(
1014 workspace_id,
1015 workspace.compiler_contract.clone(),
1016 CacheLimits::default(),
1017 );
1018 let CompilationOutcome::Committed(result) =
1019 session.compile_workspace(CompileWorkspaceRequest {
1020 workspace,
1021 mode: RequestedCompilationMode::Full,
1022 cancellation: CancellationToken::new(),
1023 })
1024 else {
1025 panic!("query enumeration fixture compilation must commit");
1026 };
1027 tooling_query_snapshot_json_v1(result.query_snapshot.as_ref())
1028 };
1029 assert_eq!(
1030 compile(vec![alpha.clone(), beta.clone()]),
1031 compile(vec![beta, alpha])
1032 );
1033 }
1034}