Skip to main content

presolve_compiler/
tooling_products.rs

1//! L11-F and L12-C canonical, source-free tooling products.
2
3#![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::ActionEndpoint => ToolingQuerySemanticKindV1::Action,
687        SemanticEntityKind::Context => ToolingQuerySemanticKindV1::Context,
688        SemanticEntityKind::Provider => ToolingQuerySemanticKindV1::Provider,
689        SemanticEntityKind::Consumer => ToolingQuerySemanticKindV1::Consumer,
690        SemanticEntityKind::Form => ToolingQuerySemanticKindV1::Form,
691        SemanticEntityKind::FormField => ToolingQuerySemanticKindV1::FormField,
692        SemanticEntityKind::FormFieldBinding => ToolingQuerySemanticKindV1::FormFieldBinding,
693        SemanticEntityKind::ValidationRule => ToolingQuerySemanticKindV1::ValidationRule,
694        SemanticEntityKind::Slot => ToolingQuerySemanticKindV1::Slot,
695        SemanticEntityKind::ComponentInvocation => ToolingQuerySemanticKindV1::ComponentInvocation,
696        SemanticEntityKind::ComponentInstance => ToolingQuerySemanticKindV1::ComponentInstance,
697        SemanticEntityKind::BlockedComponentInstance => {
698            ToolingQuerySemanticKindV1::BlockedComponentInstance
699        }
700        SemanticEntityKind::SlotContentFragment => ToolingQuerySemanticKindV1::SlotContentFragment,
701        SemanticEntityKind::SlotOutlet => ToolingQuerySemanticKindV1::SlotOutlet,
702        SemanticEntityKind::Computed => ToolingQuerySemanticKindV1::Computed,
703        SemanticEntityKind::Effect => ToolingQuerySemanticKindV1::Effect,
704        SemanticEntityKind::Parameter => ToolingQuerySemanticKindV1::Parameter,
705        SemanticEntityKind::LocalVariable => ToolingQuerySemanticKindV1::LocalVariable,
706        SemanticEntityKind::Action => ToolingQuerySemanticKindV1::Action,
707        SemanticEntityKind::EventHandler => ToolingQuerySemanticKindV1::EventHandler,
708        SemanticEntityKind::Template => ToolingQuerySemanticKindV1::Template,
709        SemanticEntityKind::TemplateEntity => ToolingQuerySemanticKindV1::TemplateEntity,
710    }
711}
712fn query_reference_kind(kind: SemanticReferenceKind) -> ToolingQueryReferenceKindV1 {
713    match kind {
714        SemanticReferenceKind::ActionState => ToolingQueryReferenceKindV1::ActionState,
715        SemanticReferenceKind::ComputedState => ToolingQueryReferenceKindV1::ComputedState,
716        SemanticReferenceKind::ComputedComputed => ToolingQueryReferenceKindV1::ComputedComputed,
717        SemanticReferenceKind::ComputedResource => ToolingQueryReferenceKindV1::ComputedResource,
718        SemanticReferenceKind::EffectState => ToolingQueryReferenceKindV1::EffectState,
719        SemanticReferenceKind::EffectComputed => ToolingQueryReferenceKindV1::EffectComputed,
720        SemanticReferenceKind::ProvidesContext => ToolingQueryReferenceKindV1::ProvidesContext,
721        SemanticReferenceKind::ConsumesContext => ToolingQueryReferenceKindV1::ConsumesContext,
722        SemanticReferenceKind::ResolvesToProvider => {
723            ToolingQueryReferenceKindV1::ResolvesToProvider
724        }
725        SemanticReferenceKind::EventMethod => ToolingQueryReferenceKindV1::EventMethod,
726        SemanticReferenceKind::TemplateState => ToolingQueryReferenceKindV1::TemplateState,
727        SemanticReferenceKind::TemplateComputed => ToolingQueryReferenceKindV1::TemplateComputed,
728        SemanticReferenceKind::TemplateLocal => ToolingQueryReferenceKindV1::TemplateLocal,
729        SemanticReferenceKind::FieldBindingField => ToolingQueryReferenceKindV1::FieldBindingField,
730        SemanticReferenceKind::FieldBindingForm => ToolingQueryReferenceKindV1::FieldBindingForm,
731        SemanticReferenceKind::ValidationRuleField => {
732            ToolingQueryReferenceKindV1::ValidationRuleField
733        }
734    }
735}
736fn query_diagnostic_severity(
737    severity: ComponentDiagnosticSeverity,
738) -> ToolingQueryDiagnosticSeverityV1 {
739    match severity {
740        ComponentDiagnosticSeverity::Error => ToolingQueryDiagnosticSeverityV1::Error,
741    }
742}
743fn query_semantic_record_key(record: &ToolingQuerySemanticRecordV1) -> (String, u64, u64, String) {
744    (
745        record.range.source_unit_id.clone(),
746        record.range.start,
747        record.range.end,
748        record.query_semantic_id.clone(),
749    )
750}
751fn query_reference_key(
752    reference: &ToolingQueryReferenceV1,
753) -> (String, u64, u64, String, String, String) {
754    (
755        reference.range.source_unit_id.clone(),
756        reference.range.start,
757        reference.range.end,
758        reference.source_query_semantic_id.clone(),
759        reference.target_query_semantic_id.clone(),
760        format!("{:?}", reference.kind),
761    )
762}
763fn query_diagnostic_secondary_key(
764    secondary: &ToolingQueryDiagnosticSecondaryV1,
765) -> (String, u64, u64, String) {
766    (
767        secondary.range.source_unit_id.clone(),
768        secondary.range.start,
769        secondary.range.end,
770        secondary.message.clone(),
771    )
772}
773fn query_diagnostic_key(
774    diagnostic: &ToolingQueryDiagnosticV1,
775) -> (String, u64, u64, String, String) {
776    let range = diagnostic.primary_range.as_ref();
777    (
778        range.map_or_else(String::new, |range| range.source_unit_id.clone()),
779        range.map_or(0, |range| range.start),
780        range.map_or(0, |range| range.end),
781        diagnostic.code.clone(),
782        diagnostic.message.clone(),
783    )
784}
785fn forbidden(value: &str) -> bool {
786    ["/", "\\", "timestamp", "duration", "millisecond"]
787        .iter()
788        .any(|needle| value.contains(needle))
789}
790fn sorted(mut values: Vec<String>) -> Vec<String> {
791    values.sort();
792    values.dedup();
793    values
794}
795fn chunk_kind(kind: ProductionChunkKind) -> &'static str {
796    match kind {
797        ProductionChunkKind::Eager => "eager",
798        ProductionChunkKind::Root => "root",
799        ProductionChunkKind::Shared => "shared",
800    }
801}
802fn canonical_json<T: Serialize>(value: &T) -> String {
803    serde_json::to_string(value).expect("tooling product serializes") + "\n"
804}
805fn sha256_json<T: Serialize>(value: &T) -> String {
806    format!(
807        "{:x}",
808        Sha256::digest(serde_json::to_vec(value).expect("tooling product serializes"))
809    )
810}
811fn identity_without_field<T: Serialize>(value: &T, field: &str) -> String {
812    let mut object = serde_json::to_value(value).expect("tooling product serializes");
813    object
814        .as_object_mut()
815        .expect("tooling product is object")
816        .remove(field);
817    format!(
818        "{:x}",
819        Sha256::digest(serde_json::to_vec(&object).expect("tooling product serializes"))
820    )
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826    use crate::platform::{
827        derive_workspace_id_v1, CacheLimits, CancellationToken, CompilationOutcome,
828        CompileWorkspaceRequest, CompilerSessionState, RequestedCompilationMode, WorkspaceInput,
829        WorkspaceSource,
830    };
831    use crate::{
832        build_production_reports, build_production_runtime_artifact,
833        extract_production_chunk_graph, ExecutableProgramFingerprint, ProductionReportInputs,
834        ProductionRootChunkInput, ResumeBoundaryId, ResumeBuildId, ResumeManifest,
835        SharedChunkCandidatePlan,
836    };
837    use std::str::FromStr;
838
839    fn phase_k_products() -> (
840        ProductionChunkGraph,
841        ProductionRuntimeArtifactV1,
842        OptimizationReportV1,
843        RuntimeCostReportV1,
844    ) {
845        let manifest = ResumeManifest {
846            schema_version: 6,
847            build_id: ResumeBuildId::zero_sentinel(),
848            snapshot_schema_version: 1,
849            runtime_protocol_version: 1,
850            application_root_boundary_id: ResumeBoundaryId::from_str("resume-boundary:root")
851                .expect("boundary"),
852            boundaries: Vec::new(),
853            slot_schemas: Vec::new(),
854            capture_programs: Vec::new(),
855            restore_programs: Vec::new(),
856            chunks: Vec::new(),
857            activations: Vec::new(),
858            anchors: Vec::new(),
859            events: Vec::new(),
860            phase_i_component_resume_records: Vec::new(),
861            phase_i_form_resume_records: Vec::new(),
862        };
863        let graph = extract_production_chunk_graph(
864            &SharedChunkCandidatePlan {
865                candidates: Vec::new(),
866                rejections: Vec::new(),
867            },
868            &[ProductionRootChunkInput {
869                activation_root_id: "root".into(),
870                root_kind: "interaction".into(),
871                programs: vec![ExecutableProgramFingerprint::for_canonical_opcode_stream(
872                    b"a",
873                )],
874            }],
875        )
876        .expect("graph")
877        .0;
878        let artifact = build_production_runtime_artifact(&manifest, &graph).expect("artifact");
879        let (optimization, cost) = build_production_reports(
880            &artifact,
881            &graph,
882            &ProductionReportInputs {
883                dead_products_removed: 1,
884                constants_pooled: 2,
885                programs_deduplicated: 3,
886                shared_candidates_rejected: 0,
887                binding_writes_coalesced: 4,
888                development_bytes: 100,
889                production_bytes: 80,
890                cold_init_operation_count: 0,
891                resume_restore_operation_count: 0,
892                max_action_batch_operation_count: 0,
893                max_scheduler_batch_width: 0,
894                max_dom_patch_count_per_action: 0,
895                retained_slot_count: 0,
896            },
897        );
898        (graph, artifact, optimization, cost)
899    }
900
901    #[test]
902    fn l11f_trace_is_canonical_source_free_and_strict() {
903        let trace = build_tooling_build_trace_v1(
904            "workspace:fixture".into(),
905            "compiler-contract:v1".into(),
906            Some("snapshot:fixture".into()),
907            ToolingTraceOutcomeV1::Succeeded,
908            vec![ToolingBuildTraceStageV1 {
909                ordinal: 0,
910                kind: ToolingTraceStageKindV1::L3Snapshot,
911                outcome: ToolingTraceOutcomeV1::Succeeded,
912                identities: vec![ToolingTraceIdentityV1 {
913                    name: "snapshot_id".into(),
914                    value: "snapshot:fixture".into(),
915                }],
916            }],
917        )
918        .unwrap();
919        let bytes = tooling_build_trace_json_v1(&trace);
920        assert_eq!(
921            decode_tooling_build_trace_v1(bytes.as_bytes()).unwrap(),
922            trace
923        );
924        assert!(!bytes.contains("timestamp"));
925        assert!(decode_tooling_build_trace_v1(bytes.trim_end().as_bytes()).is_err());
926    }
927
928    #[test]
929    fn l11f_cost_and_artifact_graph_are_canonical_source_free_products() {
930        let (graph, artifact, optimization, cost) = phase_k_products();
931        let report = build_tooling_compile_cost_report_v1(optimization, cost).unwrap();
932        let report_bytes = tooling_compile_cost_report_json_v1(&report);
933        assert_eq!(
934            decode_tooling_compile_cost_report_v1(report_bytes.as_bytes()).unwrap(),
935            report
936        );
937        assert!(!report_bytes.contains("timestamp"));
938
939        let artifact_graph = build_tooling_artifact_graph_v1(&graph, &artifact).unwrap();
940        let graph_bytes = tooling_artifact_graph_json_v1(&artifact_graph);
941        assert_eq!(
942            decode_tooling_artifact_graph_v1(graph_bytes.as_bytes()).unwrap(),
943            artifact_graph
944        );
945        assert!(!graph_bytes.contains("production/"));
946    }
947
948    #[test]
949    fn l12c_query_snapshot_is_compiler_produced_source_free_and_strict() {
950        let source = r#"@component("x-query-fixture")
951class QueryFixture extends Component {
952  value = state(1)
953  render() { return <main>{this.value}</main>; }
954}
955"#;
956        let workspace = WorkspaceInput::new(vec![WorkspaceSource {
957            path: "src/QueryFixture.tsx".into(),
958            source: source.into(),
959            language: None,
960        }]);
961        let workspace_id = derive_workspace_id_v1(&workspace.configuration).unwrap();
962        let mut session = CompilerSessionState::new(
963            workspace_id,
964            workspace.compiler_contract.clone(),
965            CacheLimits::default(),
966        );
967        let CompilationOutcome::Committed(result) =
968            session.compile_workspace(CompileWorkspaceRequest {
969                workspace,
970                mode: RequestedCompilationMode::Full,
971                cancellation: CancellationToken::new(),
972            })
973        else {
974            panic!("query snapshot fixture compilation must commit");
975        };
976        let snapshot = result.query_snapshot.as_ref().clone();
977        let bytes = tooling_query_snapshot_json_v1(&snapshot);
978        assert_eq!(
979            decode_tooling_query_snapshot_v1(bytes.as_bytes()).unwrap(),
980            snapshot
981        );
982        assert_eq!(
983            bytes,
984            include_str!("../fixtures/tooling/query-snapshot-v1.json")
985        );
986        assert_eq!(snapshot.workspace_id, result.snapshot.workspace_id.as_str());
987        assert_eq!(snapshot.snapshot_id, result.snapshot.snapshot_id.as_str());
988        assert!(!bytes.contains("QueryFixture.tsx"), "{bytes}");
989        assert!(!bytes.contains("x-query-fixture"));
990        assert!(decode_tooling_query_snapshot_v1(bytes.trim_end().as_bytes()).is_err());
991        assert!(decode_tooling_query_snapshot_v1(
992            bytes
993                .replacen("query-semantic:", "query-semantiX:", 1)
994                .as_bytes()
995        )
996        .is_err());
997    }
998
999    #[test]
1000    fn l12c_query_snapshot_is_input_enumeration_independent() {
1001        let alpha = WorkspaceSource {
1002            path: "src/Alpha.tsx".into(),
1003            source: "@component(\"x-alpha\") class Alpha extends Component { render() { return <main />; } }\n".into(),
1004            language: None,
1005        };
1006        let beta = WorkspaceSource {
1007            path: "src/Beta.tsx".into(),
1008            source: "@component(\"x-beta\") class Beta extends Component { render() { return <aside />; } }\n".into(),
1009            language: None,
1010        };
1011        let compile = |sources| {
1012            let workspace = WorkspaceInput::new(sources);
1013            let workspace_id = derive_workspace_id_v1(&workspace.configuration).unwrap();
1014            let mut session = CompilerSessionState::new(
1015                workspace_id,
1016                workspace.compiler_contract.clone(),
1017                CacheLimits::default(),
1018            );
1019            let CompilationOutcome::Committed(result) =
1020                session.compile_workspace(CompileWorkspaceRequest {
1021                    workspace,
1022                    mode: RequestedCompilationMode::Full,
1023                    cancellation: CancellationToken::new(),
1024                })
1025            else {
1026                panic!("query enumeration fixture compilation must commit");
1027            };
1028            tooling_query_snapshot_json_v1(result.query_snapshot.as_ref())
1029        };
1030        assert_eq!(
1031            compile(vec![alpha.clone(), beta.clone()]),
1032            compile(vec![beta, alpha])
1033        );
1034    }
1035}