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::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}