Skip to main content

presolve_compiler/
platform.rs

1//! Deterministic, process-local compiler platform products (Phase L3).
2//!
3//! This module deliberately orchestrates the existing parser and application
4//! semantic model.  It neither defines a second parser nor reproduces semantic
5//! ownership, artifact, or runtime derivation.
6
7#![allow(
8    clippy::case_sensitive_file_extension_comparisons,
9    clippy::cast_possible_truncation,
10    clippy::missing_errors_doc,
11    clippy::needless_pass_by_value,
12    clippy::semicolon_if_nothing_returned,
13    clippy::too_many_lines
14)]
15
16use std::collections::{BTreeMap, BTreeSet};
17use std::fmt;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::sync::Arc;
20
21use sha2::{Digest as _, Sha256};
22
23use crate::{
24    build_application_semantic_model_for_unit, tooling_products::build_tooling_query_snapshot_v1,
25    tooling_products::ToolingQuerySnapshotV1, CompilationUnit,
26};
27
28pub const WORKSPACE_GRAPH_SCHEMA_VERSION: u32 = 1;
29pub const WORKSPACE_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
30pub const COMPILER_SESSION_SCHEMA_VERSION: u32 = 1;
31pub const INCREMENTAL_PLAN_SCHEMA_VERSION: u32 = 1;
32pub const PRODUCT_CACHE_INSPECTION_SCHEMA_VERSION: u32 = 1;
33/// The public canonical workspace-configuration input schema authorized by L4.
34pub const WORKSPACE_CONFIGURATION_SCHEMA_VERSION: u32 = 1;
35/// L5's session-local incremental planning product discriminator.
36pub const INCREMENTAL_COMPILATION_PLAN_V1_SCHEMA: &str = "presolve.incremental-compilation-plan.v1";
37/// L5's service execution-report product discriminator.
38pub const INCREMENTAL_EXECUTION_REPORT_V1_SCHEMA: &str = "presolve.incremental-execution-report.v1";
39
40static NEXT_SESSION_ID: AtomicU64 = AtomicU64::new(1);
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub struct Digest(String);
44
45impl Digest {
46    #[must_use]
47    pub fn sha256(bytes: impl AsRef<[u8]>) -> Self {
48        Self(format!("sha256:{:x}", Sha256::digest(bytes.as_ref())))
49    }
50
51    #[must_use]
52    pub fn as_str(&self) -> &str {
53        &self.0
54    }
55}
56
57impl fmt::Display for Digest {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        self.0.fmt(f)
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
64pub struct ContractVersion(String);
65impl ContractVersion {
66    #[must_use]
67    pub fn new(value: impl Into<String>) -> Self {
68        Self(value.into())
69    }
70    #[must_use]
71    pub fn as_str(&self) -> &str {
72        &self.0
73    }
74}
75impl fmt::Display for ContractVersion {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        self.0.fmt(f)
78    }
79}
80
81macro_rules! typed_id {
82    ($name:ident) => {
83        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84        pub struct $name(String);
85        impl $name {
86            #[must_use]
87            pub fn as_str(&self) -> &str {
88                &self.0
89            }
90        }
91        impl fmt::Display for $name {
92            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93                self.0.fmt(f)
94            }
95        }
96    };
97}
98typed_id!(WorkspaceId);
99typed_id!(SourceUnitId);
100typed_id!(SourceRevisionId);
101typed_id!(WorkspaceSnapshotId);
102typed_id!(IncrementalPlanId);
103typed_id!(ProductKey);
104typed_id!(WorkspaceProductKey);
105typed_id!(CompilerSessionId);
106typed_id!(CompilationAttemptId);
107
108fn hashed_id(prefix: &str, domain: &str, fields: &[&[u8]]) -> String {
109    let mut bytes = Vec::new();
110    bytes.extend_from_slice(domain.as_bytes());
111    for field in fields {
112        bytes.push(0);
113        bytes.extend_from_slice(field);
114    }
115    format!("{prefix}:{}", Digest::sha256(bytes))
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub struct WorkspacePath(String);
120
121impl WorkspacePath {
122    pub fn new(path: impl AsRef<str>) -> Result<Self, PlatformFailure> {
123        let raw = path.as_ref();
124        if raw.is_empty()
125            || raw.contains('\0')
126            || raw.starts_with('/')
127            || raw.starts_with('\\')
128            || is_windows_absolute(raw)
129        {
130            return Err(PlatformFailure::path(
131                "workspace path must be non-empty and relative",
132                raw,
133            ));
134        }
135        let mut parts = Vec::new();
136        let separated = raw.replace('\\', "/");
137        for part in separated.split('/') {
138            match part {
139                "" | "." => {}
140                ".." => {
141                    if parts.pop().is_none() {
142                        return Err(PlatformFailure::path(
143                            "workspace path escapes its root",
144                            raw,
145                        ));
146                    }
147                }
148                value => parts.push(value),
149            }
150        }
151        if parts.is_empty() {
152            return Err(PlatformFailure::path(
153                "workspace path must name a file",
154                raw,
155            ));
156        }
157        Ok(Self(parts.join("/")))
158    }
159    #[must_use]
160    pub fn as_str(&self) -> &str {
161        &self.0
162    }
163}
164impl fmt::Display for WorkspacePath {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        self.0.fmt(f)
167    }
168}
169fn is_windows_absolute(path: &str) -> bool {
170    path.len() > 2 && path.as_bytes()[1] == b':' && path.as_bytes()[2] == b'\\'
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
174pub enum SourceLanguage {
175    TypeScript,
176    TypeScriptJsx,
177    JavaScript,
178    JavaScriptJsx,
179}
180impl SourceLanguage {
181    #[must_use]
182    pub const fn as_str(self) -> &'static str {
183        match self {
184            Self::TypeScript => "typescript",
185            Self::TypeScriptJsx => "typescript_jsx",
186            Self::JavaScript => "javascript",
187            Self::JavaScriptJsx => "javascript_jsx",
188        }
189    }
190    fn from_path(path: &WorkspacePath) -> Result<Self, PlatformFailure> {
191        if path.0.ends_with(".tsx") {
192            Ok(Self::TypeScriptJsx)
193        } else if path.0.ends_with(".ts") {
194            Ok(Self::TypeScript)
195        } else if path.0.ends_with(".jsx") {
196            Ok(Self::JavaScriptJsx)
197        } else if path.0.ends_with(".js") {
198            Ok(Self::JavaScript)
199        } else {
200            Err(PlatformFailure::new(
201                PlatformFailureCode::UnsupportedSourceLanguage,
202                "only .ts, .tsx, .js, and .jsx workspace inputs are supported",
203            ))
204        }
205    }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
209pub enum ProductKind {
210    Parse,
211    SourceSemantic,
212    ComponentGraph,
213    ApplicationSemanticModel,
214    LoweredProgram,
215    HtmlArtifact,
216    RuntimeArtifact,
217    TemplateManifest,
218    ResumeManifest,
219    InspectionArtifact,
220    DiagnosticSet,
221}
222impl ProductKind {
223    #[must_use]
224    pub const fn as_str(self) -> &'static str {
225        match self {
226            Self::Parse => "parse",
227            Self::SourceSemantic => "source_semantic",
228            Self::ComponentGraph => "component_graph",
229            Self::ApplicationSemanticModel => "application_semantic_model",
230            Self::LoweredProgram => "lowered_program",
231            Self::HtmlArtifact => "html_artifact",
232            Self::RuntimeArtifact => "runtime_artifact",
233            Self::TemplateManifest => "template_manifest",
234            Self::ResumeManifest => "resume_manifest",
235            Self::InspectionArtifact => "inspection_artifact",
236            Self::DiagnosticSet => "diagnostic_set",
237        }
238    }
239}
240#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
241pub enum ApplicationProductKind {
242    ApplicationSemanticModel,
243    ApplicationDiagnostics,
244    BuildManifest,
245}
246impl ApplicationProductKind {
247    #[must_use]
248    pub const fn as_str(self) -> &'static str {
249        match self {
250            Self::ApplicationSemanticModel => "application_semantic_model",
251            Self::ApplicationDiagnostics => "application_diagnostics",
252            Self::BuildManifest => "build_manifest",
253        }
254    }
255}
256#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
257pub enum WorkspaceDependencyKind {
258    Compile,
259    Semantic,
260    Artifact,
261}
262impl WorkspaceDependencyKind {
263    #[must_use]
264    pub const fn as_str(self) -> &'static str {
265        match self {
266            Self::Compile => "compile",
267            Self::Semantic => "semantic",
268            Self::Artifact => "artifact",
269        }
270    }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct WorkspaceConfiguration {
275    pub source_roots: Vec<WorkspacePath>,
276    pub feature_flags: Vec<String>,
277    pub target_profile: String,
278    pub platform_options: Vec<(String, String)>,
279}
280impl Default for WorkspaceConfiguration {
281    fn default() -> Self {
282        Self {
283            source_roots: vec![WorkspacePath("src".into())],
284            feature_flags: Vec::new(),
285            target_profile: "default".into(),
286            platform_options: Vec::new(),
287        }
288    }
289}
290impl WorkspaceConfiguration {
291    fn canonical(&self) -> String {
292        let roots = self
293            .source_roots
294            .iter()
295            .map(ToString::to_string)
296            .collect::<Vec<_>>();
297        let mut flags = self.feature_flags.clone();
298        flags.sort();
299        flags.dedup();
300        let mut options = self.platform_options.clone();
301        options.sort();
302        options.dedup();
303        format!(
304            "roots={}\\nflags={}\\ntarget={}\\noptions={}",
305            roots.join(","),
306            flags.join(","),
307            self.target_profile,
308            options
309                .into_iter()
310                .map(|(k, v)| format!("{k}={v}"))
311                .collect::<Vec<_>>()
312                .join(",")
313        )
314    }
315}
316
317/// Validation error for the public L3 platform input surface.
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct PlatformValidationError {
320    pub code: &'static str,
321    pub message: String,
322}
323
324/// Deterministic canonical-decode failure. `pointer` is a JSON pointer.
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct PlatformDecodeError {
327    pub code: &'static str,
328    pub pointer: String,
329    pub message: String,
330}
331
332/// Public L3 v1 configuration validation authorized by the L4 service contract.
333pub fn validate_workspace_configuration_v1(
334    configuration: &WorkspaceConfiguration,
335) -> Result<(), PlatformValidationError> {
336    if configuration.target_profile.is_empty() || configuration.target_profile.contains('\0') {
337        return Err(PlatformValidationError {
338            code: "invalid_workspace_configuration",
339            message: "target profile must be non-empty".into(),
340        });
341    }
342    if configuration.source_roots.is_empty() {
343        return Err(PlatformValidationError {
344            code: "invalid_workspace_configuration",
345            message: "at least one source root is required".into(),
346        });
347    }
348    let mut roots = BTreeSet::new();
349    for root in &configuration.source_roots {
350        WorkspacePath::new(root.as_str()).map_err(|error| PlatformValidationError {
351            code: "invalid_workspace_configuration",
352            message: error.message,
353        })?;
354        if !roots.insert(root.clone()) {
355            return Err(PlatformValidationError {
356                code: "invalid_workspace_configuration",
357                message: "source roots must be unique".into(),
358            });
359        }
360    }
361    Ok(())
362}
363
364/// Returns exact canonical JSON for the L3 v1 workspace configuration.
365pub fn canonical_workspace_configuration_json_v1(
366    configuration: &WorkspaceConfiguration,
367) -> Result<Vec<u8>, PlatformSerializationError> {
368    validate_workspace_configuration_v1(configuration).map_err(|error| {
369        PlatformSerializationError {
370            message: error.message,
371        }
372    })?;
373    let flags = configuration
374        .feature_flags
375        .iter()
376        .map(|value| quote(value))
377        .collect::<Vec<_>>()
378        .join(",");
379    let roots = configuration
380        .source_roots
381        .iter()
382        .map(|value| quote(value.as_str()))
383        .collect::<Vec<_>>()
384        .join(",");
385    let options = configuration
386        .platform_options
387        .iter()
388        .map(|(key, value)| format!("{{\"key\":{},\"value\":{}}}", quote(key), quote(value)))
389        .collect::<Vec<_>>()
390        .join(",");
391    Ok(json_document(format!("{{\"schema_version\":{},\"source_roots\":[{}],\"compiler_flags\":[{}],\"target_profile\":{},\"platform_options\":[{}]}}", WORKSPACE_CONFIGURATION_SCHEMA_VERSION, roots, flags, quote(&configuration.target_profile), options)))
392}
393
394/// Returns the configuration fingerprint used by L3 workspace products.
395pub fn workspace_configuration_fingerprint_v1(
396    configuration: &WorkspaceConfiguration,
397) -> Result<Digest, PlatformValidationError> {
398    validate_workspace_configuration_v1(configuration)?;
399    Ok(Digest::sha256(configuration.canonical()))
400}
401
402/// Derives the L3 workspace identity from a fully validated configuration.
403pub fn derive_workspace_id_v1(
404    configuration: &WorkspaceConfiguration,
405) -> Result<WorkspaceId, PlatformValidationError> {
406    validate_workspace_configuration_v1(configuration)?;
407    Ok(workspace_id(configuration))
408}
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct WorkspaceSource {
411    pub path: String,
412    pub source: String,
413    pub language: Option<SourceLanguage>,
414}
415#[derive(Debug, Clone, PartialEq, Eq)]
416pub struct WorkspaceInput {
417    pub configuration: WorkspaceConfiguration,
418    pub sources: Vec<WorkspaceSource>,
419    pub compiler_contract: ContractVersion,
420}
421impl WorkspaceInput {
422    #[must_use]
423    pub fn new(sources: Vec<WorkspaceSource>) -> Self {
424        Self {
425            configuration: WorkspaceConfiguration::default(),
426            sources,
427            compiler_contract: ContractVersion::new(format!(
428                "presolve-compiler:{}",
429                env!("CARGO_PKG_VERSION")
430            )),
431        }
432    }
433}
434
435#[derive(Debug, Clone, PartialEq, Eq)]
436struct NormalizedInput {
437    path: WorkspacePath,
438    source: String,
439    language: SourceLanguage,
440    source_unit_id: SourceUnitId,
441    source_revision_id: SourceRevisionId,
442    source_digest: Digest,
443}
444fn workspace_id(configuration: &WorkspaceConfiguration) -> WorkspaceId {
445    WorkspaceId(hashed_id(
446        "workspace",
447        "workspace-config-v1",
448        &[configuration.canonical().as_bytes()],
449    ))
450}
451fn source_unit_id(workspace: &WorkspaceId, path: &WorkspacePath) -> SourceUnitId {
452    SourceUnitId(hashed_id(
453        "source",
454        "source-unit-v1",
455        &[workspace.as_str().as_bytes(), path.as_str().as_bytes()],
456    ))
457}
458fn source_revision_id(
459    unit: &SourceUnitId,
460    source: &str,
461    language: SourceLanguage,
462) -> SourceRevisionId {
463    SourceRevisionId(hashed_id(
464        "revision",
465        "source-revision-v1",
466        &[
467            unit.as_str().as_bytes(),
468            source.len().to_string().as_bytes(),
469            source.as_bytes(),
470            language.as_str().as_bytes(),
471        ],
472    ))
473}
474fn normalize(
475    input: &WorkspaceInput,
476) -> Result<(WorkspaceId, Digest, Vec<NormalizedInput>), PlatformFailure> {
477    let workspace = workspace_id(&input.configuration);
478    let configuration_fingerprint = Digest::sha256(input.configuration.canonical());
479    let mut seen = BTreeSet::new();
480    let mut sources = Vec::new();
481    for source in &input.sources {
482        let path = WorkspacePath::new(&source.path)?;
483        if !seen.insert(path.clone()) {
484            return Err(PlatformFailure {
485                code: PlatformFailureCode::DuplicateWorkspacePath,
486                message: format!("duplicate normalized workspace path: {path}"),
487                workspace_path: Some(path),
488                source_unit_id: None,
489                product_key: None,
490            });
491        }
492        let language = match source.language {
493            Some(language) => language,
494            None => SourceLanguage::from_path(&path)?,
495        };
496        let unit = source_unit_id(&workspace, &path);
497        let revision = source_revision_id(&unit, &source.source, language);
498        sources.push(NormalizedInput {
499            path,
500            source: source.source.clone(),
501            language,
502            source_unit_id: unit,
503            source_revision_id: revision,
504            source_digest: Digest::sha256(source.source.as_bytes()),
505        });
506    }
507    sources.sort_by(|a, b| a.path.cmp(&b.path));
508    Ok((workspace, configuration_fingerprint, sources))
509}
510
511#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct SnapshotUnit {
513    pub source_unit_id: SourceUnitId,
514    pub path: WorkspacePath,
515    pub source_revision_id: SourceRevisionId,
516    pub source_digest: Digest,
517    pub source_length: u64,
518    pub language: SourceLanguage,
519}
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct WorkspaceSnapshot {
522    pub schema_version: u32,
523    pub workspace_id: WorkspaceId,
524    pub snapshot_id: WorkspaceSnapshotId,
525    pub compiler_contract: ContractVersion,
526    pub configuration_fingerprint: Digest,
527    pub units: Vec<SnapshotUnit>,
528}
529impl WorkspaceSnapshot {
530    pub fn from_input(input: &WorkspaceInput) -> Result<Self, PlatformFailure> {
531        let (workspace, fingerprint, inputs) = normalize(input)?;
532        Ok(Self::from_normalized(
533            workspace,
534            fingerprint,
535            input.compiler_contract.clone(),
536            &inputs,
537        ))
538    }
539    fn from_normalized(
540        workspace_id: WorkspaceId,
541        configuration_fingerprint: Digest,
542        compiler_contract: ContractVersion,
543        inputs: &[NormalizedInput],
544    ) -> Self {
545        let units = inputs
546            .iter()
547            .map(|input| SnapshotUnit {
548                source_unit_id: input.source_unit_id.clone(),
549                path: input.path.clone(),
550                source_revision_id: input.source_revision_id.clone(),
551                source_digest: input.source_digest.clone(),
552                source_length: input.source.len() as u64,
553                language: input.language,
554            })
555            .collect::<Vec<_>>();
556        let identity = canonical_snapshot_identity(&workspace_id, &compiler_contract, &units);
557        Self {
558            schema_version: WORKSPACE_SNAPSHOT_SCHEMA_VERSION,
559            workspace_id,
560            snapshot_id: WorkspaceSnapshotId(hashed_id(
561                "snapshot",
562                "workspace-snapshot-v1",
563                &[identity.as_bytes()],
564            )),
565            compiler_contract,
566            configuration_fingerprint,
567            units,
568        }
569    }
570    pub fn validate(&self) -> Result<(), PlatformFailure> {
571        if self.schema_version != WORKSPACE_SNAPSHOT_SCHEMA_VERSION {
572            return Err(PlatformFailure::new(
573                PlatformFailureCode::UnsupportedSchemaVersion,
574                "unsupported workspace snapshot schema version",
575            ));
576        }
577        let mut previous = None;
578        for unit in &self.units {
579            if previous
580                .as_ref()
581                .is_some_and(|value: &WorkspacePath| value >= &unit.path)
582            {
583                return Err(PlatformFailure::new(
584                    PlatformFailureCode::InvalidWorkspaceSnapshot,
585                    "snapshot units are not uniquely ordered by path",
586                ));
587            }
588            if source_unit_id(&self.workspace_id, &unit.path) != unit.source_unit_id {
589                return Err(PlatformFailure::new(
590                    PlatformFailureCode::InvalidWorkspaceSnapshot,
591                    "snapshot source-unit identity mismatch",
592                ));
593            }
594            previous = Some(unit.path.clone());
595        }
596        let expected = WorkspaceSnapshotId(hashed_id(
597            "snapshot",
598            "workspace-snapshot-v1",
599            &[canonical_snapshot_identity(
600                &self.workspace_id,
601                &self.compiler_contract,
602                &self.units,
603            )
604            .as_bytes()],
605        ));
606        if expected != self.snapshot_id {
607            return Err(PlatformFailure::new(
608                PlatformFailureCode::InvalidWorkspaceSnapshot,
609                "snapshot identity mismatch",
610            ));
611        }
612        Ok(())
613    }
614    pub fn to_canonical_json(&self) -> Result<Vec<u8>, PlatformSerializationError> {
615        self.validate()
616            .map_err(PlatformSerializationError::validation)?;
617        Ok(json_document(snapshot_json(self)))
618    }
619}
620
621/// Decodes one exact canonical L3 v1 workspace snapshot document without
622/// loading source, invoking the parser, or mutating a compiler session.
623pub fn decode_workspace_snapshot_json_v1(
624    bytes: &[u8],
625) -> Result<WorkspaceSnapshot, PlatformDecodeError> {
626    let value = decode_json(bytes)?;
627    let object = object_fields(
628        value,
629        &[
630            "schema_version",
631            "workspace_id",
632            "snapshot_id",
633            "compiler_contract",
634            "configuration_fingerprint",
635            "units",
636        ],
637        "",
638    )?;
639    let schema = number(object.get("schema_version"), "/schema_version")?;
640    if schema != u64::from(WORKSPACE_SNAPSHOT_SCHEMA_VERSION) {
641        return Err(decode_error(
642            "unsupported_schema_version",
643            "/schema_version",
644            "unsupported workspace snapshot schema version",
645        ));
646    }
647    let units = array(object.get("units"), "/units")?
648        .iter()
649        .enumerate()
650        .map(|(index, value)| {
651            let item = object_fields(
652                value.clone(),
653                &[
654                    "source_unit_id",
655                    "path",
656                    "source_revision_id",
657                    "source_digest",
658                    "source_length",
659                    "language",
660                ],
661                &format!("/units/{index}"),
662            )?;
663            Ok(SnapshotUnit {
664                source_unit_id: SourceUnitId(
665                    string(
666                        item.get("source_unit_id"),
667                        &format!("/units/{index}/source_unit_id"),
668                    )?
669                    .into(),
670                ),
671                path: WorkspacePath::new(string(
672                    item.get("path"),
673                    &format!("/units/{index}/path"),
674                )?)
675                .map_err(|_| {
676                    decode_error(
677                        "invalid_workspace_path",
678                        &format!("/units/{index}/path"),
679                        "invalid workspace path",
680                    )
681                })?,
682                source_revision_id: SourceRevisionId(
683                    string(
684                        item.get("source_revision_id"),
685                        &format!("/units/{index}/source_revision_id"),
686                    )?
687                    .into(),
688                ),
689                source_digest: Digest(
690                    string(
691                        item.get("source_digest"),
692                        &format!("/units/{index}/source_digest"),
693                    )?
694                    .into(),
695                ),
696                source_length: number(
697                    item.get("source_length"),
698                    &format!("/units/{index}/source_length"),
699                )?,
700                language: decode_language(string(
701                    item.get("language"),
702                    &format!("/units/{index}/language"),
703                )?)?,
704            })
705        })
706        .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
707    let snapshot = WorkspaceSnapshot {
708        schema_version: WORKSPACE_SNAPSHOT_SCHEMA_VERSION,
709        workspace_id: WorkspaceId(string(object.get("workspace_id"), "/workspace_id")?.into()),
710        snapshot_id: WorkspaceSnapshotId(string(object.get("snapshot_id"), "/snapshot_id")?.into()),
711        compiler_contract: ContractVersion::new(string(
712            object.get("compiler_contract"),
713            "/compiler_contract",
714        )?),
715        configuration_fingerprint: Digest(
716            string(
717                object.get("configuration_fingerprint"),
718                "/configuration_fingerprint",
719            )?
720            .into(),
721        ),
722        units,
723    };
724    snapshot
725        .validate()
726        .map_err(|error| decode_error("invalid_workspace_snapshot", "", error.message))?;
727    if snapshot
728        .to_canonical_json()
729        .map_err(|error| decode_error("invalid_workspace_snapshot", "", error.message))?
730        != bytes
731    {
732        return Err(decode_error(
733            "noncanonical_json",
734            "",
735            "workspace snapshot is not canonical",
736        ));
737    }
738    Ok(snapshot)
739}
740
741/// Decodes one exact canonical L3 v1 workspace graph document without source
742/// loading or compiler execution.
743pub fn decode_workspace_graph_json_v1(bytes: &[u8]) -> Result<WorkspaceGraph, PlatformDecodeError> {
744    let value = decode_json(bytes)?;
745    let object = object_fields(
746        value,
747        &[
748            "schema_version",
749            "workspace_id",
750            "snapshot_id",
751            "compiler_contract",
752            "units",
753            "dependency_edges",
754            "application_products",
755        ],
756        "",
757    )?;
758    if number(object.get("schema_version"), "/schema_version")?
759        != u64::from(WORKSPACE_GRAPH_SCHEMA_VERSION)
760    {
761        return Err(decode_error(
762            "unsupported_schema_version",
763            "/schema_version",
764            "unsupported workspace graph schema version",
765        ));
766    }
767    let units = array(object.get("units"), "/units")?
768        .iter()
769        .enumerate()
770        .map(|(index, value)| {
771            let item = object_fields(
772                value.clone(),
773                &[
774                    "source_unit_id",
775                    "path",
776                    "source_revision_id",
777                    "source_digest",
778                    "source_length",
779                    "language",
780                    "products",
781                ],
782                &format!("/units/{index}"),
783            )?;
784            let products = array(item.get("products"), &format!("/units/{index}/products"))?
785                .iter()
786                .enumerate()
787                .map(|(product_index, value)| {
788                    let p = object_fields(
789                        value.clone(),
790                        &[
791                            "product_kind",
792                            "product_key",
793                            "producer_contract",
794                            "content_digest",
795                        ],
796                        &format!("/units/{index}/products/{product_index}"),
797                    )?;
798                    Ok(UnitProductRef {
799                        product_kind: decode_product_kind(string(p.get("product_kind"), "")?)?,
800                        product_key: ProductKey(string(p.get("product_key"), "")?.into()),
801                        producer_contract: ContractVersion::new(string(
802                            p.get("producer_contract"),
803                            "",
804                        )?),
805                        content_digest: Digest(string(p.get("content_digest"), "")?.into()),
806                    })
807                })
808                .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
809            Ok(WorkspaceUnit {
810                source_unit_id: SourceUnitId(string(item.get("source_unit_id"), "")?.into()),
811                path: WorkspacePath::new(string(item.get("path"), "")?).map_err(|_| {
812                    decode_error("invalid_workspace_path", "", "invalid workspace path")
813                })?,
814                source_revision_id: SourceRevisionId(
815                    string(item.get("source_revision_id"), "")?.into(),
816                ),
817                source_digest: Digest(string(item.get("source_digest"), "")?.into()),
818                source_length: number(item.get("source_length"), "")?,
819                language: decode_language(string(item.get("language"), "")?)?,
820                products,
821            })
822        })
823        .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
824    let edges = array(object.get("dependency_edges"), "/dependency_edges")?
825        .iter()
826        .map(|value| {
827            let edge = object_fields(
828                value.clone(),
829                &["source", "kind", "target"],
830                "/dependency_edges",
831            )?;
832            Ok(WorkspaceDependencyEdge {
833                source: SourceUnitId(string(edge.get("source"), "")?.into()),
834                kind: decode_dependency_kind(string(edge.get("kind"), "")?)?,
835                target: SourceUnitId(string(edge.get("target"), "")?.into()),
836            })
837        })
838        .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
839    let applications = array(object.get("application_products"), "/application_products")?
840        .iter()
841        .map(|value| {
842            let product = object_fields(
843                value.clone(),
844                &[
845                    "product_kind",
846                    "product_key",
847                    "producer_contract",
848                    "content_digest",
849                ],
850                "/application_products",
851            )?;
852            Ok(ApplicationProductRef {
853                product_kind: decode_application_product_kind(string(
854                    product.get("product_kind"),
855                    "",
856                )?)?,
857                product_key: WorkspaceProductKey(string(product.get("product_key"), "")?.into()),
858                producer_contract: ContractVersion::new(string(
859                    product.get("producer_contract"),
860                    "",
861                )?),
862                content_digest: Digest(string(product.get("content_digest"), "")?.into()),
863            })
864        })
865        .collect::<Result<Vec<_>, PlatformDecodeError>>()?;
866    let graph = WorkspaceGraph {
867        schema_version: WORKSPACE_GRAPH_SCHEMA_VERSION,
868        workspace_id: WorkspaceId(string(object.get("workspace_id"), "/workspace_id")?.into()),
869        snapshot_id: WorkspaceSnapshotId(string(object.get("snapshot_id"), "/snapshot_id")?.into()),
870        compiler_contract: ContractVersion::new(string(
871            object.get("compiler_contract"),
872            "/compiler_contract",
873        )?),
874        units,
875        dependency_edges: edges,
876        application_products: applications,
877    };
878    graph
879        .validate()
880        .map_err(|error| decode_error("invalid_workspace_graph", "", error.message))?;
881    if graph
882        .to_canonical_json()
883        .map_err(|error| decode_error("invalid_workspace_graph", "", error.message))?
884        != bytes
885    {
886        return Err(decode_error(
887            "noncanonical_json",
888            "",
889            "workspace graph is not canonical",
890        ));
891    }
892    Ok(graph)
893}
894fn decode_product_kind(value: &str) -> Result<ProductKind, PlatformDecodeError> {
895    match value {
896        "parse" => Ok(ProductKind::Parse),
897        "source_semantic" => Ok(ProductKind::SourceSemantic),
898        "component_graph" => Ok(ProductKind::ComponentGraph),
899        "application_semantic_model" => Ok(ProductKind::ApplicationSemanticModel),
900        "lowered_program" => Ok(ProductKind::LoweredProgram),
901        "html_artifact" => Ok(ProductKind::HtmlArtifact),
902        "runtime_artifact" => Ok(ProductKind::RuntimeArtifact),
903        "template_manifest" => Ok(ProductKind::TemplateManifest),
904        "resume_manifest" => Ok(ProductKind::ResumeManifest),
905        "inspection_artifact" => Ok(ProductKind::InspectionArtifact),
906        "diagnostic_set" => Ok(ProductKind::DiagnosticSet),
907        _ => Err(decode_error("invalid_enum", "", "unsupported product kind")),
908    }
909}
910fn decode_application_product_kind(
911    value: &str,
912) -> Result<ApplicationProductKind, PlatformDecodeError> {
913    match value {
914        "application_semantic_model" => Ok(ApplicationProductKind::ApplicationSemanticModel),
915        "application_diagnostics" => Ok(ApplicationProductKind::ApplicationDiagnostics),
916        "build_manifest" => Ok(ApplicationProductKind::BuildManifest),
917        _ => Err(decode_error(
918            "invalid_enum",
919            "",
920            "unsupported application product kind",
921        )),
922    }
923}
924fn decode_dependency_kind(value: &str) -> Result<WorkspaceDependencyKind, PlatformDecodeError> {
925    match value {
926        "compile" => Ok(WorkspaceDependencyKind::Compile),
927        "semantic" => Ok(WorkspaceDependencyKind::Semantic),
928        "artifact" => Ok(WorkspaceDependencyKind::Artifact),
929        _ => Err(decode_error(
930            "invalid_enum",
931            "",
932            "unsupported dependency kind",
933        )),
934    }
935}
936
937fn decode_json(bytes: &[u8]) -> Result<serde_json::Value, PlatformDecodeError> {
938    if !bytes.ends_with(b"\n") || bytes.ends_with(b"\n\n") {
939        return Err(decode_error(
940            "noncanonical_json",
941            "",
942            "document must end with exactly one newline",
943        ));
944    }
945    serde_json::from_slice(bytes)
946        .map_err(|_| decode_error("invalid_json", "", "invalid canonical JSON"))
947}
948fn object_fields(
949    value: serde_json::Value,
950    expected: &[&str],
951    pointer: &str,
952) -> Result<serde_json::Map<String, serde_json::Value>, PlatformDecodeError> {
953    let serde_json::Value::Object(object) = value else {
954        return Err(decode_error("invalid_type", pointer, "expected object"));
955    };
956    if object.len() != expected.len() || expected.iter().any(|field| !object.contains_key(*field)) {
957        return Err(decode_error(
958            "invalid_schema",
959            pointer,
960            "missing or unknown object field",
961        ));
962    }
963    Ok(object)
964}
965fn string<'a>(
966    value: Option<&'a serde_json::Value>,
967    pointer: &str,
968) -> Result<&'a str, PlatformDecodeError> {
969    value
970        .and_then(serde_json::Value::as_str)
971        .ok_or_else(|| decode_error("invalid_type", pointer, "expected string"))
972}
973fn number(value: Option<&serde_json::Value>, pointer: &str) -> Result<u64, PlatformDecodeError> {
974    value
975        .and_then(serde_json::Value::as_u64)
976        .ok_or_else(|| decode_error("invalid_type", pointer, "expected unsigned integer"))
977}
978fn array<'a>(
979    value: Option<&'a serde_json::Value>,
980    pointer: &str,
981) -> Result<&'a Vec<serde_json::Value>, PlatformDecodeError> {
982    value
983        .and_then(serde_json::Value::as_array)
984        .ok_or_else(|| decode_error("invalid_type", pointer, "expected array"))
985}
986fn decode_language(value: &str) -> Result<SourceLanguage, PlatformDecodeError> {
987    match value {
988        "typescript" => Ok(SourceLanguage::TypeScript),
989        "typescript_jsx" => Ok(SourceLanguage::TypeScriptJsx),
990        "javascript" => Ok(SourceLanguage::JavaScript),
991        "javascript_jsx" => Ok(SourceLanguage::JavaScriptJsx),
992        _ => Err(decode_error(
993            "invalid_enum",
994            "/language",
995            "unsupported source language",
996        )),
997    }
998}
999fn decode_error(
1000    code: &'static str,
1001    pointer: &str,
1002    message: impl Into<String>,
1003) -> PlatformDecodeError {
1004    PlatformDecodeError {
1005        code,
1006        pointer: pointer.into(),
1007        message: message.into(),
1008    }
1009}
1010fn canonical_snapshot_identity(
1011    workspace: &WorkspaceId,
1012    contract: &ContractVersion,
1013    units: &[SnapshotUnit],
1014) -> String {
1015    let units = units
1016        .iter()
1017        .map(|unit| {
1018            format!(
1019                "{{\"source_unit_id\":{},\"source_revision_id\":{}}}",
1020                quote(unit.source_unit_id.as_str()),
1021                quote(unit.source_revision_id.as_str())
1022            )
1023        })
1024        .collect::<Vec<_>>()
1025        .join(",");
1026    format!(
1027        "{{\"workspace_id\":{},\"compiler_contract\":{},\"units\":[{units}]}}",
1028        quote(workspace.as_str()),
1029        quote(contract.as_str())
1030    )
1031}
1032
1033#[derive(Debug, Clone, PartialEq, Eq)]
1034pub struct UnitProductRef {
1035    pub product_kind: ProductKind,
1036    pub product_key: ProductKey,
1037    pub producer_contract: ContractVersion,
1038    pub content_digest: Digest,
1039}
1040#[derive(Debug, Clone, PartialEq, Eq)]
1041pub struct WorkspaceUnit {
1042    pub source_unit_id: SourceUnitId,
1043    pub path: WorkspacePath,
1044    pub source_revision_id: SourceRevisionId,
1045    pub source_digest: Digest,
1046    pub source_length: u64,
1047    pub language: SourceLanguage,
1048    pub products: Vec<UnitProductRef>,
1049}
1050#[derive(Debug, Clone, PartialEq, Eq)]
1051pub struct WorkspaceDependencyEdge {
1052    pub source: SourceUnitId,
1053    pub kind: WorkspaceDependencyKind,
1054    pub target: SourceUnitId,
1055}
1056#[derive(Debug, Clone, PartialEq, Eq)]
1057pub struct ApplicationProductRef {
1058    pub product_kind: ApplicationProductKind,
1059    pub product_key: WorkspaceProductKey,
1060    pub producer_contract: ContractVersion,
1061    pub content_digest: Digest,
1062}
1063#[derive(Debug, Clone, PartialEq, Eq)]
1064pub struct WorkspaceGraph {
1065    pub schema_version: u32,
1066    pub workspace_id: WorkspaceId,
1067    pub snapshot_id: WorkspaceSnapshotId,
1068    pub compiler_contract: ContractVersion,
1069    pub units: Vec<WorkspaceUnit>,
1070    pub dependency_edges: Vec<WorkspaceDependencyEdge>,
1071    pub application_products: Vec<ApplicationProductRef>,
1072}
1073impl WorkspaceGraph {
1074    #[must_use]
1075    pub fn unit(&self, id: &SourceUnitId) -> Option<&WorkspaceUnit> {
1076        self.units.iter().find(|unit| &unit.source_unit_id == id)
1077    }
1078    #[must_use]
1079    pub fn unit_by_path(&self, path: &WorkspacePath) -> Option<&WorkspaceUnit> {
1080        self.units.iter().find(|unit| &unit.path == path)
1081    }
1082    pub fn direct_dependencies<'a>(
1083        &'a self,
1084        id: &'a SourceUnitId,
1085    ) -> impl Iterator<Item = &'a WorkspaceDependencyEdge> + 'a {
1086        self.dependency_edges
1087            .iter()
1088            .filter(move |edge| &edge.source == id)
1089    }
1090    pub fn direct_dependents<'a>(
1091        &'a self,
1092        id: &'a SourceUnitId,
1093    ) -> impl Iterator<Item = &'a WorkspaceDependencyEdge> + 'a {
1094        self.dependency_edges
1095            .iter()
1096            .filter(move |edge| &edge.target == id)
1097    }
1098    pub fn validate(&self) -> Result<(), PlatformFailure> {
1099        if self.schema_version != WORKSPACE_GRAPH_SCHEMA_VERSION {
1100            return Err(PlatformFailure::new(
1101                PlatformFailureCode::UnsupportedSchemaVersion,
1102                "unsupported workspace graph schema version",
1103            ));
1104        }
1105        let mut paths = BTreeSet::new();
1106        let mut ids = BTreeSet::new();
1107        let mut prior = None;
1108        for unit in &self.units {
1109            if !paths.insert(unit.path.clone())
1110                || !ids.insert(unit.source_unit_id.clone())
1111                || prior
1112                    .as_ref()
1113                    .is_some_and(|path: &WorkspacePath| path >= &unit.path)
1114            {
1115                return Err(PlatformFailure::new(
1116                    PlatformFailureCode::InvalidWorkspaceGraph,
1117                    "workspace graph units must be unique and ordered",
1118                ));
1119            }
1120            if source_unit_id(&self.workspace_id, &unit.path) != unit.source_unit_id {
1121                return Err(PlatformFailure::new(
1122                    PlatformFailureCode::InvalidWorkspaceGraph,
1123                    "workspace graph source-unit identity mismatch",
1124                ));
1125            }
1126            let mut kinds = BTreeSet::new();
1127            for product in &unit.products {
1128                if !kinds.insert(product.product_kind)
1129                    || !product.product_key.as_str().starts_with("product:sha256:")
1130                {
1131                    return Err(PlatformFailure::new(
1132                        PlatformFailureCode::InvalidWorkspaceGraph,
1133                        "workspace graph product key mismatch",
1134                    ));
1135                }
1136            }
1137            prior = Some(unit.path.clone());
1138        }
1139        let mut edges = BTreeSet::new();
1140        let mut previous = None;
1141        for edge in &self.dependency_edges {
1142            if !ids.contains(&edge.source)
1143                || !ids.contains(&edge.target)
1144                || !edges.insert((edge.source.clone(), edge.kind, edge.target.clone()))
1145                || previous.as_ref().is_some_and(
1146                    |value: &(SourceUnitId, WorkspaceDependencyKind, SourceUnitId)| {
1147                        value >= &(edge.source.clone(), edge.kind, edge.target.clone())
1148                    },
1149                )
1150            {
1151                return Err(PlatformFailure::new(
1152                    PlatformFailureCode::InvalidWorkspaceGraph,
1153                    "workspace graph edges must be valid, unique, and ordered",
1154                ));
1155            }
1156            previous = Some((edge.source.clone(), edge.kind, edge.target.clone()));
1157        }
1158        Ok(())
1159    }
1160    pub fn to_canonical_json(&self) -> Result<Vec<u8>, PlatformSerializationError> {
1161        self.validate()
1162            .map_err(PlatformSerializationError::validation)?;
1163        Ok(json_document(graph_json(self)))
1164    }
1165}
1166fn product_key(
1167    unit: &SourceUnitId,
1168    revision: &SourceRevisionId,
1169    kind: ProductKind,
1170    contract: &ContractVersion,
1171    configuration: &Digest,
1172) -> ProductKey {
1173    ProductKey(hashed_id(
1174        "product",
1175        "product-key-v1",
1176        &[
1177            unit.as_str().as_bytes(),
1178            revision.as_str().as_bytes(),
1179            kind.as_str().as_bytes(),
1180            contract.as_str().as_bytes(),
1181            configuration.as_str().as_bytes(),
1182        ],
1183    ))
1184}
1185fn workspace_product_key(
1186    workspace: &WorkspaceId,
1187    snapshot: &WorkspaceSnapshotId,
1188    kind: ApplicationProductKind,
1189    contract: &ContractVersion,
1190    configuration: &Digest,
1191) -> WorkspaceProductKey {
1192    WorkspaceProductKey(hashed_id(
1193        "workspace-product",
1194        "workspace-product-key-v1",
1195        &[
1196            workspace.as_str().as_bytes(),
1197            snapshot.as_str().as_bytes(),
1198            kind.as_str().as_bytes(),
1199            contract.as_str().as_bytes(),
1200            configuration.as_str().as_bytes(),
1201        ],
1202    ))
1203}
1204
1205#[derive(Debug, Clone, PartialEq, Eq)]
1206pub struct WorkspaceChangeSet {
1207    pub baseline_snapshot_id: WorkspaceSnapshotId,
1208    pub candidate_snapshot_id: WorkspaceSnapshotId,
1209    pub configuration_changed: bool,
1210    pub added: Vec<SourceUnitId>,
1211    pub removed: Vec<SourceUnitId>,
1212    pub modified: Vec<SourceUnitId>,
1213    pub unchanged: Vec<SourceUnitId>,
1214}
1215#[must_use]
1216pub fn compare_snapshots(
1217    baseline: &WorkspaceSnapshot,
1218    candidate: &WorkspaceSnapshot,
1219) -> WorkspaceChangeSet {
1220    let old = baseline
1221        .units
1222        .iter()
1223        .map(|unit| (unit.source_unit_id.clone(), unit))
1224        .collect::<BTreeMap<_, _>>();
1225    let new = candidate
1226        .units
1227        .iter()
1228        .map(|unit| (unit.source_unit_id.clone(), unit))
1229        .collect::<BTreeMap<_, _>>();
1230    let mut added = Vec::new();
1231    let mut removed = Vec::new();
1232    let mut modified = Vec::new();
1233    let mut unchanged = Vec::new();
1234    for unit in &candidate.units {
1235        match old.get(&unit.source_unit_id) {
1236            None => added.push(unit.source_unit_id.clone()),
1237            Some(previous) if previous.source_revision_id != unit.source_revision_id => {
1238                modified.push(unit.source_unit_id.clone())
1239            }
1240            Some(_) => unchanged.push(unit.source_unit_id.clone()),
1241        }
1242    }
1243    for unit in &baseline.units {
1244        if !new.contains_key(&unit.source_unit_id) {
1245            removed.push(unit.source_unit_id.clone());
1246        }
1247    }
1248    WorkspaceChangeSet {
1249        baseline_snapshot_id: baseline.snapshot_id.clone(),
1250        candidate_snapshot_id: candidate.snapshot_id.clone(),
1251        configuration_changed: baseline.workspace_id != candidate.workspace_id
1252            || baseline.configuration_fingerprint != candidate.configuration_fingerprint
1253            || baseline.compiler_contract != candidate.compiler_contract,
1254        added,
1255        removed,
1256        modified,
1257        unchanged,
1258    }
1259}
1260
1261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1262pub enum IncrementalMode {
1263    NoOp,
1264    Incremental,
1265    Full,
1266}
1267impl IncrementalMode {
1268    #[must_use]
1269    pub const fn as_str(self) -> &'static str {
1270        match self {
1271            Self::NoOp => "no_op",
1272            Self::Incremental => "incremental",
1273            Self::Full => "full",
1274        }
1275    }
1276}
1277#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1278pub enum InvalidationReason {
1279    Added,
1280    Modified,
1281    RemovedDependency,
1282    CompileDependent,
1283    SemanticDependent,
1284    ArtifactDependent,
1285    ConfigurationChanged,
1286    CompilerContractChanged,
1287}
1288impl InvalidationReason {
1289    #[must_use]
1290    pub const fn as_str(self) -> &'static str {
1291        match self {
1292            Self::Added => "added",
1293            Self::Modified => "modified",
1294            Self::RemovedDependency => "removed_dependency",
1295            Self::CompileDependent => "compile_dependent",
1296            Self::SemanticDependent => "semantic_dependent",
1297            Self::ArtifactDependent => "artifact_dependent",
1298            Self::ConfigurationChanged => "configuration_changed",
1299            Self::CompilerContractChanged => "compiler_contract_changed",
1300        }
1301    }
1302}
1303#[derive(Debug, Clone, PartialEq, Eq)]
1304pub struct InvalidatedUnit {
1305    pub source_unit_id: SourceUnitId,
1306    pub reason: InvalidationReason,
1307    pub caused_by: Vec<SourceUnitId>,
1308}
1309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1310pub enum IncrementalStageKind {
1311    Remove,
1312    Parse,
1313    Analyze,
1314    AssembleApplicationModel,
1315    Lower,
1316    Emit,
1317    Validate,
1318    Commit,
1319}
1320impl IncrementalStageKind {
1321    const fn as_str(self) -> &'static str {
1322        match self {
1323            Self::Remove => "remove",
1324            Self::Parse => "parse",
1325            Self::Analyze => "analyze",
1326            Self::AssembleApplicationModel => "assemble_application_model",
1327            Self::Lower => "lower",
1328            Self::Emit => "emit",
1329            Self::Validate => "validate",
1330            Self::Commit => "commit",
1331        }
1332    }
1333}
1334#[derive(Debug, Clone, PartialEq, Eq)]
1335pub struct IncrementalStage {
1336    pub stage_index: u32,
1337    pub kind: IncrementalStageKind,
1338    pub units: Vec<SourceUnitId>,
1339}
1340#[derive(Debug, Clone, PartialEq, Eq)]
1341pub struct ExpectedCommit {
1342    pub snapshot_id: WorkspaceSnapshotId,
1343    pub workspace_graph_schema_version: u32,
1344}
1345#[derive(Debug, Clone, PartialEq, Eq)]
1346pub struct IncrementalPlan {
1347    pub schema_version: u32,
1348    pub plan_id: IncrementalPlanId,
1349    pub baseline_snapshot_id: WorkspaceSnapshotId,
1350    pub candidate_snapshot_id: WorkspaceSnapshotId,
1351    pub mode: IncrementalMode,
1352    pub changes: WorkspaceChangeSet,
1353    pub invalidated_units: Vec<InvalidatedUnit>,
1354    pub stages: Vec<IncrementalStage>,
1355    pub retained_products: Vec<ProductKey>,
1356    pub discarded_products: Vec<ProductKey>,
1357    pub expected_commit: ExpectedCommit,
1358}
1359impl IncrementalPlan {
1360    pub fn to_canonical_json(&self) -> Result<Vec<u8>, PlatformSerializationError> {
1361        if self.schema_version != INCREMENTAL_PLAN_SCHEMA_VERSION {
1362            return Err(PlatformSerializationError {
1363                message: "unsupported incremental plan schema version".into(),
1364            });
1365        }
1366        Ok(json_document(incremental_plan_json(self)))
1367    }
1368}
1369
1370/// The only L5 execution modes. They describe orchestration, never language
1371/// semantics or generated output.
1372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1373pub enum IncrementalCompilationModeV1 {
1374    Cold,
1375    NoChange,
1376    Incremental,
1377    CleanFallback,
1378}
1379impl IncrementalCompilationModeV1 {
1380    #[must_use]
1381    pub const fn as_str(self) -> &'static str {
1382        match self {
1383            Self::Cold => "cold",
1384            Self::NoChange => "no_change",
1385            Self::Incremental => "incremental",
1386            Self::CleanFallback => "clean_fallback",
1387        }
1388    }
1389}
1390
1391/// Canonical order is declaration order, which is also the L5 contract order.
1392#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1393pub enum IncrementalInputChangeKindV1 {
1394    ConfigurationChanged,
1395    SourceDeleted,
1396    SourceAdded,
1397    SourceContentChanged,
1398}
1399impl IncrementalInputChangeKindV1 {
1400    #[must_use]
1401    pub const fn as_str(self) -> &'static str {
1402        match self {
1403            Self::ConfigurationChanged => "configuration_changed",
1404            Self::SourceDeleted => "source_deleted",
1405            Self::SourceAdded => "source_added",
1406            Self::SourceContentChanged => "source_content_changed",
1407        }
1408    }
1409}
1410
1411#[derive(Debug, Clone, PartialEq, Eq)]
1412pub struct IncrementalInputChangeV1 {
1413    pub kind: IncrementalInputChangeKindV1,
1414    /// `configuration` or a canonical L3 source-unit identity.
1415    pub identity: String,
1416}
1417
1418/// Stable, non-diagnostic L5 fallback facts in the exact contract order.
1419#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1420pub enum IncrementalFallbackReasonV1 {
1421    NoBaseline,
1422    ServiceRestart,
1423    ConfigurationChanged,
1424    CompilerVersionMismatch,
1425    BuildProtocolMismatch,
1426    ProductSchemaMismatch,
1427    MalformedBaselineGraph,
1428    IncompleteDependencyAuthority,
1429    UnresolvedCanonicalIdentity,
1430    SourceUniverseMembershipUnmodeled,
1431    ReuseProductRejected,
1432    IncrementalInvariantFailure,
1433}
1434impl IncrementalFallbackReasonV1 {
1435    #[must_use]
1436    pub const fn code(self) -> &'static str {
1437        match self {
1438            Self::NoBaseline => "L5F000_NO_BASELINE",
1439            Self::ServiceRestart => "L5F001_SERVICE_RESTART",
1440            Self::ConfigurationChanged => "L5F002_CONFIGURATION_CHANGED",
1441            Self::CompilerVersionMismatch => "L5F003_COMPILER_VERSION_MISMATCH",
1442            Self::BuildProtocolMismatch => "L5F004_BUILD_PROTOCOL_MISMATCH",
1443            Self::ProductSchemaMismatch => "L5F005_PRODUCT_SCHEMA_MISMATCH",
1444            Self::MalformedBaselineGraph => "L5F006_MALFORMED_BASELINE_GRAPH",
1445            Self::IncompleteDependencyAuthority => "L5F007_INCOMPLETE_DEPENDENCY_AUTHORITY",
1446            Self::UnresolvedCanonicalIdentity => "L5F008_UNRESOLVED_CANONICAL_IDENTITY",
1447            Self::SourceUniverseMembershipUnmodeled => {
1448                "L5F009_SOURCE_UNIVERSE_MEMBERSHIP_UNMODELED"
1449            }
1450            Self::ReuseProductRejected => "L5F010_REUSE_PRODUCT_REJECTED",
1451            Self::IncrementalInvariantFailure => "L5F011_INCREMENTAL_INVARIANT_FAILURE",
1452        }
1453    }
1454}
1455
1456/// Immutable, canonical L5 planning product. It carries no source text.
1457#[derive(Debug, Clone, PartialEq, Eq)]
1458pub struct IncrementalCompilationPlanV1 {
1459    pub schema: &'static str,
1460    pub baseline_publication_identity: Option<String>,
1461    pub candidate_configuration_fingerprint: Digest,
1462    pub candidate_source_universe_fingerprint: Digest,
1463    pub input_changes: Vec<IncrementalInputChangeV1>,
1464    pub directly_invalidated: Vec<SourceUnitId>,
1465    pub invalidation_closure: Vec<SourceUnitId>,
1466    pub reusable_product_identities: Vec<ProductKey>,
1467    pub recompute_work_units: Vec<SourceUnitId>,
1468    pub fallback_reasons: Vec<IncrementalFallbackReasonV1>,
1469    pub mode: IncrementalCompilationModeV1,
1470    pub plan_fingerprint: Digest,
1471}
1472impl IncrementalCompilationPlanV1 {
1473    pub fn to_canonical_json(&self) -> Result<Vec<u8>, PlatformSerializationError> {
1474        if self.schema != INCREMENTAL_COMPILATION_PLAN_V1_SCHEMA {
1475            return Err(PlatformSerializationError {
1476                message: "unsupported L5 incremental compilation plan schema".into(),
1477            });
1478        }
1479        let expected = Digest::sha256(l5_plan_bytes_without_fingerprint(self));
1480        if expected != self.plan_fingerprint {
1481            return Err(PlatformSerializationError {
1482                message: "L5 incremental compilation plan fingerprint mismatch".into(),
1483            });
1484        }
1485        Ok(json_document(l5_plan_json(self)))
1486    }
1487}
1488
1489/// An L3 parse product explicitly authorized for L5 session-local reuse.
1490/// `ParsedFile` is a normalized parser result, not authored source text.
1491#[derive(Debug, Clone)]
1492pub struct CanonicalReusableProductV1 {
1493    pub source_unit_id: SourceUnitId,
1494    pub source_revision_id: SourceRevisionId,
1495    pub product_key: ProductKey,
1496    parsed: Arc<presolve_parser::ParsedFile>,
1497}
1498impl CanonicalReusableProductV1 {
1499    fn parsed(&self) -> &presolve_parser::ParsedFile {
1500        self.parsed.as_ref()
1501    }
1502}
1503
1504#[derive(Debug, Clone)]
1505pub struct IncrementalCompileWorkspaceRequestV1 {
1506    pub workspace: WorkspaceInput,
1507    pub cancellation: CancellationToken,
1508    pub plan: IncrementalCompilationPlanV1,
1509    pub reusable_products: Vec<CanonicalReusableProductV1>,
1510}
1511
1512#[derive(Debug, Clone)]
1513pub struct IncrementalCompilationOutcomeV1 {
1514    pub outcome: CompilationOutcome,
1515    pub reused_product_identities: Vec<ProductKey>,
1516    pub recomputed_work_units: Vec<SourceUnitId>,
1517    pub rejected_reuse_product_identities: Vec<ProductKey>,
1518}
1519#[must_use]
1520pub fn plan_incremental(
1521    baseline: Option<(&WorkspaceSnapshot, &WorkspaceGraph)>,
1522    candidate: &WorkspaceSnapshot,
1523    forced_full: bool,
1524) -> IncrementalPlan {
1525    let empty_id = WorkspaceSnapshotId(
1526        "snapshot:sha256:0000000000000000000000000000000000000000000000000000000000000000".into(),
1527    );
1528    let Some((before, graph)) = baseline else {
1529        return build_full_plan(
1530            empty_id.clone(),
1531            candidate,
1532            WorkspaceChangeSet {
1533                baseline_snapshot_id: empty_id.clone(),
1534                candidate_snapshot_id: candidate.snapshot_id.clone(),
1535                configuration_changed: true,
1536                added: candidate
1537                    .units
1538                    .iter()
1539                    .map(|u| u.source_unit_id.clone())
1540                    .collect(),
1541                removed: Vec::new(),
1542                modified: Vec::new(),
1543                unchanged: Vec::new(),
1544            },
1545            candidate,
1546        );
1547    };
1548    let changes = compare_snapshots(before, candidate);
1549    if forced_full || changes.configuration_changed || graph.validate().is_err() {
1550        return build_full_plan(before.snapshot_id.clone(), candidate, changes, candidate);
1551    }
1552    if before.snapshot_id == candidate.snapshot_id {
1553        return build_plan(
1554            before.snapshot_id.clone(),
1555            candidate,
1556            changes,
1557            IncrementalMode::NoOp,
1558            Vec::new(),
1559            Vec::new(),
1560            Vec::new(),
1561        );
1562    }
1563    let candidate_paths = candidate
1564        .units
1565        .iter()
1566        .map(|u| (u.source_unit_id.clone(), u.path.clone()))
1567        .collect::<BTreeMap<_, _>>();
1568    let mut causes = BTreeMap::<SourceUnitId, BTreeSet<SourceUnitId>>::new();
1569    let mut reasons = BTreeMap::<SourceUnitId, InvalidationReason>::new();
1570    for id in &changes.added {
1571        causes.entry(id.clone()).or_default().insert(id.clone());
1572        reasons.insert(id.clone(), InvalidationReason::Added);
1573    }
1574    for id in &changes.modified {
1575        causes.entry(id.clone()).or_default().insert(id.clone());
1576        reasons.insert(id.clone(), InvalidationReason::Modified);
1577    }
1578    let mut frontier = changes.modified.clone();
1579    frontier.extend(changes.removed.clone());
1580    while let Some(changed) = frontier.pop() {
1581        for edge in graph.direct_dependents(&changed) {
1582            if !candidate_paths.contains_key(&edge.source) {
1583                continue;
1584            }
1585            let reason = match edge.kind {
1586                WorkspaceDependencyKind::Compile => InvalidationReason::CompileDependent,
1587                WorkspaceDependencyKind::Semantic => InvalidationReason::SemanticDependent,
1588                WorkspaceDependencyKind::Artifact => InvalidationReason::ArtifactDependent,
1589            };
1590            let added = causes
1591                .entry(edge.source.clone())
1592                .or_default()
1593                .insert(changed.clone());
1594            reasons.entry(edge.source.clone()).or_insert(reason);
1595            if added {
1596                frontier.push(edge.source.clone());
1597            }
1598        }
1599    }
1600    let mut invalidated = causes
1601        .into_iter()
1602        .map(|(id, causes)| InvalidatedUnit {
1603            reason: reasons[&id],
1604            source_unit_id: id,
1605            caused_by: causes.into_iter().collect(),
1606        })
1607        .collect::<Vec<_>>();
1608    invalidated.sort_by_key(|item| candidate_paths.get(&item.source_unit_id).cloned());
1609    let impacted = invalidated
1610        .iter()
1611        .map(|item| item.source_unit_id.clone())
1612        .collect::<Vec<_>>();
1613    let discarded = graph
1614        .units
1615        .iter()
1616        .filter(|unit| {
1617            impacted.contains(&unit.source_unit_id)
1618                || changes.removed.contains(&unit.source_unit_id)
1619        })
1620        .flat_map(|unit| {
1621            unit.products
1622                .iter()
1623                .map(|product| product.product_key.clone())
1624        })
1625        .collect::<Vec<_>>();
1626    build_plan(
1627        before.snapshot_id.clone(),
1628        candidate,
1629        changes,
1630        IncrementalMode::Incremental,
1631        invalidated,
1632        impacted,
1633        discarded,
1634    )
1635}
1636
1637/// Fingerprints the complete normalized source universe using only canonical
1638/// L3 snapshot facts. The source revision already binds identity, language,
1639/// and exact authored bytes; no text is retained by this product.
1640#[must_use]
1641pub fn source_universe_fingerprint_v1(snapshot: &WorkspaceSnapshot) -> Digest {
1642    let mut bytes = Vec::new();
1643    for unit in &snapshot.units {
1644        for value in [
1645            unit.source_unit_id.as_str(),
1646            unit.language.as_str(),
1647            unit.source_revision_id.as_str(),
1648        ] {
1649            bytes.extend_from_slice(&(value.len() as u64).to_be_bytes());
1650            bytes.extend_from_slice(value.as_bytes());
1651        }
1652    }
1653    Digest::sha256(bytes)
1654}
1655
1656/// Builds an L5 plan solely from canonical L3 snapshots, graph edges, and
1657/// product identities. Source membership lacks product-granular authority in
1658/// L3 v1, so additions and deletions deliberately clean-fallback.
1659#[must_use]
1660pub fn plan_incremental_compilation_v1(
1661    baseline: Option<(&WorkspaceSnapshot, &WorkspaceGraph)>,
1662    candidate: &WorkspaceSnapshot,
1663) -> IncrementalCompilationPlanV1 {
1664    let mut changes = Vec::new();
1665    let mut direct = Vec::new();
1666    let mut closure = Vec::new();
1667    let mut reusable = Vec::new();
1668    let mut work = Vec::new();
1669    let mut fallback = Vec::new();
1670    let mut mode = IncrementalCompilationModeV1::Cold;
1671    let baseline_publication_identity =
1672        baseline.map(|(snapshot, _)| snapshot.snapshot_id.to_string());
1673
1674    if let Some((before, graph)) = baseline {
1675        let old = before
1676            .units
1677            .iter()
1678            .map(|unit| (unit.source_unit_id.clone(), unit))
1679            .collect::<BTreeMap<_, _>>();
1680        let current = candidate
1681            .units
1682            .iter()
1683            .map(|unit| (unit.source_unit_id.clone(), unit))
1684            .collect::<BTreeMap<_, _>>();
1685        if before.configuration_fingerprint != candidate.configuration_fingerprint {
1686            changes.push(IncrementalInputChangeV1 {
1687                kind: IncrementalInputChangeKindV1::ConfigurationChanged,
1688                identity: "configuration".into(),
1689            });
1690            fallback.push(IncrementalFallbackReasonV1::ConfigurationChanged);
1691        }
1692        for unit in &before.units {
1693            if !current.contains_key(&unit.source_unit_id) {
1694                changes.push(IncrementalInputChangeV1 {
1695                    kind: IncrementalInputChangeKindV1::SourceDeleted,
1696                    identity: unit.source_unit_id.to_string(),
1697                });
1698            }
1699        }
1700        for unit in &candidate.units {
1701            match old.get(&unit.source_unit_id) {
1702                None => changes.push(IncrementalInputChangeV1 {
1703                    kind: IncrementalInputChangeKindV1::SourceAdded,
1704                    identity: unit.source_unit_id.to_string(),
1705                }),
1706                Some(previous) if previous.source_revision_id != unit.source_revision_id => {
1707                    changes.push(IncrementalInputChangeV1 {
1708                        kind: IncrementalInputChangeKindV1::SourceContentChanged,
1709                        identity: unit.source_unit_id.to_string(),
1710                    });
1711                    direct.push(unit.source_unit_id.clone());
1712                }
1713                Some(_) => {}
1714            }
1715        }
1716        changes.sort_by(|left, right| {
1717            left.kind
1718                .cmp(&right.kind)
1719                .then_with(|| left.identity.as_bytes().cmp(right.identity.as_bytes()))
1720        });
1721        if graph.validate().is_err() || graph.snapshot_id != before.snapshot_id {
1722            fallback.push(IncrementalFallbackReasonV1::MalformedBaselineGraph);
1723        }
1724        if changes.iter().any(|change| {
1725            matches!(
1726                change.kind,
1727                IncrementalInputChangeKindV1::SourceAdded
1728                    | IncrementalInputChangeKindV1::SourceDeleted
1729            )
1730        }) {
1731            fallback.push(IncrementalFallbackReasonV1::SourceUniverseMembershipUnmodeled);
1732        }
1733        if before.compiler_contract != candidate.compiler_contract {
1734            fallback.push(IncrementalFallbackReasonV1::CompilerVersionMismatch);
1735        }
1736        if fallback.is_empty() {
1737            let mut invalidated = direct.iter().cloned().collect::<BTreeSet<_>>();
1738            let mut frontier = direct.clone();
1739            while let Some(current_id) = frontier.pop() {
1740                for edge in graph.direct_dependents(&current_id) {
1741                    if invalidated.insert(edge.source.clone()) {
1742                        frontier.push(edge.source.clone());
1743                    }
1744                }
1745            }
1746            closure = invalidated.into_iter().collect();
1747            work.clone_from(&closure);
1748            let candidate_by_id = candidate
1749                .units
1750                .iter()
1751                .map(|unit| (unit.source_unit_id.clone(), unit))
1752                .collect::<BTreeMap<_, _>>();
1753            for unit in &graph.units {
1754                let Some(candidate_unit) = candidate_by_id.get(&unit.source_unit_id) else {
1755                    continue;
1756                };
1757                if closure.contains(&unit.source_unit_id)
1758                    || candidate_unit.source_revision_id != unit.source_revision_id
1759                {
1760                    continue;
1761                }
1762                for product in &unit.products {
1763                    if product.product_kind == ProductKind::Parse {
1764                        reusable.push(product.product_key.clone());
1765                    }
1766                }
1767            }
1768            mode = if changes.is_empty() {
1769                IncrementalCompilationModeV1::NoChange
1770            } else if reusable.is_empty() {
1771                // There is no separately reusable L3 product. A clean compile
1772                // is safer than pretending the plan has incremental benefit.
1773                fallback.push(IncrementalFallbackReasonV1::IncompleteDependencyAuthority);
1774                IncrementalCompilationModeV1::CleanFallback
1775            } else {
1776                IncrementalCompilationModeV1::Incremental
1777            };
1778        }
1779        if !fallback.is_empty() {
1780            mode = IncrementalCompilationModeV1::CleanFallback;
1781            direct.clear();
1782            closure.clear();
1783            reusable.clear();
1784            work = candidate
1785                .units
1786                .iter()
1787                .map(|unit| unit.source_unit_id.clone())
1788                .collect();
1789        }
1790    }
1791    fallback.sort();
1792    fallback.dedup();
1793    direct.sort();
1794    direct.dedup();
1795    closure.sort();
1796    closure.dedup();
1797    reusable.sort();
1798    reusable.dedup();
1799    work.sort();
1800    work.dedup();
1801    let mut plan = IncrementalCompilationPlanV1 {
1802        schema: INCREMENTAL_COMPILATION_PLAN_V1_SCHEMA,
1803        baseline_publication_identity,
1804        candidate_configuration_fingerprint: candidate.configuration_fingerprint.clone(),
1805        candidate_source_universe_fingerprint: source_universe_fingerprint_v1(candidate),
1806        input_changes: changes,
1807        directly_invalidated: direct,
1808        invalidation_closure: closure,
1809        reusable_product_identities: reusable,
1810        recompute_work_units: work,
1811        fallback_reasons: fallback,
1812        mode,
1813        plan_fingerprint: Digest::sha256([]),
1814    };
1815    plan.plan_fingerprint = Digest::sha256(l5_plan_bytes_without_fingerprint(&plan));
1816    plan
1817}
1818
1819/// Converts a candidate plan to the conservative L5 clean path without
1820/// deriving any dependency information. Used when the service detects that a
1821/// retained baseline product no longer validates.
1822pub fn force_clean_fallback_v1(
1823    plan: &mut IncrementalCompilationPlanV1,
1824    candidate: &WorkspaceSnapshot,
1825    reason: IncrementalFallbackReasonV1,
1826) {
1827    plan.mode = IncrementalCompilationModeV1::CleanFallback;
1828    plan.directly_invalidated.clear();
1829    plan.invalidation_closure.clear();
1830    plan.reusable_product_identities.clear();
1831    plan.recompute_work_units = candidate
1832        .units
1833        .iter()
1834        .map(|unit| unit.source_unit_id.clone())
1835        .collect();
1836    plan.fallback_reasons.push(reason);
1837    plan.fallback_reasons.sort();
1838    plan.fallback_reasons.dedup();
1839    plan.plan_fingerprint = Digest::sha256(l5_plan_bytes_without_fingerprint(plan));
1840}
1841fn build_full_plan(
1842    baseline: WorkspaceSnapshotId,
1843    candidate: &WorkspaceSnapshot,
1844    changes: WorkspaceChangeSet,
1845    _snapshot: &WorkspaceSnapshot,
1846) -> IncrementalPlan {
1847    let invalidated = candidate
1848        .units
1849        .iter()
1850        .map(|unit| InvalidatedUnit {
1851            source_unit_id: unit.source_unit_id.clone(),
1852            reason: InvalidationReason::ConfigurationChanged,
1853            caused_by: Vec::new(),
1854        })
1855        .collect();
1856    build_plan(
1857        baseline,
1858        candidate,
1859        changes,
1860        IncrementalMode::Full,
1861        invalidated,
1862        candidate
1863            .units
1864            .iter()
1865            .map(|u| u.source_unit_id.clone())
1866            .collect(),
1867        Vec::new(),
1868    )
1869}
1870fn build_plan(
1871    baseline: WorkspaceSnapshotId,
1872    candidate: &WorkspaceSnapshot,
1873    changes: WorkspaceChangeSet,
1874    mode: IncrementalMode,
1875    invalidated: Vec<InvalidatedUnit>,
1876    impacted: Vec<SourceUnitId>,
1877    mut discarded: Vec<ProductKey>,
1878) -> IncrementalPlan {
1879    let mut stages = Vec::new();
1880    let mut push = |kind, units: Vec<SourceUnitId>| {
1881        if !units.is_empty()
1882            || matches!(
1883                kind,
1884                IncrementalStageKind::Validate
1885                    | IncrementalStageKind::Commit
1886                    | IncrementalStageKind::AssembleApplicationModel
1887            )
1888        {
1889            let index = stages.len() as u32;
1890            stages.push(IncrementalStage {
1891                stage_index: index,
1892                kind,
1893                units,
1894            });
1895        }
1896    };
1897    push(IncrementalStageKind::Remove, changes.removed.clone());
1898    if !matches!(mode, IncrementalMode::NoOp) {
1899        push(IncrementalStageKind::Parse, impacted.clone());
1900        push(IncrementalStageKind::Analyze, impacted.clone());
1901        push(IncrementalStageKind::AssembleApplicationModel, Vec::new());
1902        push(IncrementalStageKind::Lower, impacted.clone());
1903        push(IncrementalStageKind::Emit, impacted.clone());
1904    }
1905    push(IncrementalStageKind::Validate, Vec::new());
1906    push(IncrementalStageKind::Commit, Vec::new());
1907    discarded.sort();
1908    discarded.dedup();
1909    let identity = format!(
1910        "{}|{}|{}|{:?}",
1911        baseline,
1912        candidate.snapshot_id,
1913        mode.as_str(),
1914        stages
1915    );
1916    let plan_id = IncrementalPlanId(hashed_id(
1917        "incremental-plan",
1918        "incremental-plan-v1",
1919        &[identity.as_bytes()],
1920    ));
1921    IncrementalPlan {
1922        schema_version: INCREMENTAL_PLAN_SCHEMA_VERSION,
1923        plan_id,
1924        baseline_snapshot_id: baseline,
1925        candidate_snapshot_id: candidate.snapshot_id.clone(),
1926        mode,
1927        changes,
1928        invalidated_units: invalidated,
1929        stages,
1930        retained_products: Vec::new(),
1931        discarded_products: discarded,
1932        expected_commit: ExpectedCommit {
1933            snapshot_id: candidate.snapshot_id.clone(),
1934            workspace_graph_schema_version: WORKSPACE_GRAPH_SCHEMA_VERSION,
1935        },
1936    }
1937}
1938
1939pub trait CachedProduct: Send + Sync {
1940    fn kind(&self) -> ProductKind;
1941    fn digest(&self) -> &Digest;
1942}
1943#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1944pub enum CacheEntryState {
1945    AttemptLocal,
1946    Committed,
1947}
1948#[derive(Debug, Clone, PartialEq, Eq)]
1949pub struct CacheLimits {
1950    pub maximum_entries: usize,
1951    pub maximum_weight: u64,
1952}
1953impl Default for CacheLimits {
1954    fn default() -> Self {
1955        Self {
1956            maximum_entries: 1024,
1957            maximum_weight: 268_435_456,
1958        }
1959    }
1960}
1961pub struct CacheEntry {
1962    pub key: ProductKey,
1963    pub content_digest: Digest,
1964    pub state: CacheEntryState,
1965    pub weight: u64,
1966    pub last_access: u64,
1967    pub product: Arc<dyn CachedProduct>,
1968}
1969pub struct ProductCache {
1970    entries: BTreeMap<ProductKey, CacheEntry>,
1971    total_weight: u64,
1972    limits: CacheLimits,
1973    next_access: u64,
1974}
1975impl ProductCache {
1976    #[must_use]
1977    pub fn new(limits: CacheLimits) -> Self {
1978        Self {
1979            entries: BTreeMap::new(),
1980            total_weight: 0,
1981            limits,
1982            next_access: 0,
1983        }
1984    }
1985    pub fn get(&mut self, key: &ProductKey) -> Option<Arc<dyn CachedProduct>> {
1986        let entry = self.entries.get_mut(key)?;
1987        if entry.product.kind() != product_kind_from_key(key, &entry.key)
1988            || entry.product.digest() != &entry.content_digest
1989        {
1990            self.total_weight -= entry.weight;
1991            self.entries.remove(key);
1992            return None;
1993        }
1994        self.next_access += 1;
1995        entry.last_access = self.next_access;
1996        Some(Arc::clone(&entry.product))
1997    }
1998    pub fn insert(&mut self, entry: CacheEntry) {
1999        if let Some(old) = self.entries.insert(entry.key.clone(), entry) {
2000            self.total_weight -= old.weight;
2001        }
2002        self.total_weight = self.entries.values().map(|entry| entry.weight).sum();
2003    }
2004    fn promote_and_evict(&mut self) {
2005        for entry in self.entries.values_mut() {
2006            if entry.state == CacheEntryState::AttemptLocal {
2007                entry.state = CacheEntryState::Committed;
2008            }
2009        }
2010        while self.entries.len() > self.limits.maximum_entries
2011            || self.total_weight > self.limits.maximum_weight
2012        {
2013            let candidate = self
2014                .entries
2015                .iter()
2016                .filter(|(_, entry)| entry.state == CacheEntryState::Committed)
2017                .min_by_key(|(key, entry)| (entry.last_access, (*key).clone()))
2018                .map(|(key, _)| key.clone());
2019            let Some(key) = candidate else { break };
2020            if let Some(entry) = self.entries.remove(&key) {
2021                self.total_weight -= entry.weight;
2022            }
2023        }
2024    }
2025    #[must_use]
2026    pub fn inspection(&self) -> ProductCacheInspection {
2027        let mut entries = self
2028            .entries
2029            .values()
2030            .map(|entry| CacheInspectionEntry {
2031                product_key: entry.key.clone(),
2032                product_kind: entry.product.kind(),
2033                content_digest: entry.content_digest.clone(),
2034                state: entry.state,
2035                weight: entry.weight,
2036            })
2037            .collect::<Vec<_>>();
2038        entries.sort_by_key(|e| e.product_key.clone());
2039        ProductCacheInspection {
2040            schema_version: PRODUCT_CACHE_INSPECTION_SCHEMA_VERSION,
2041            entry_count: entries.len(),
2042            total_weight: self.total_weight,
2043            limits: self.limits.clone(),
2044            entries,
2045        }
2046    }
2047}
2048fn product_kind_from_key(_: &ProductKey, _: &ProductKey) -> ProductKind {
2049    ProductKind::Parse
2050}
2051#[derive(Debug, Clone, PartialEq, Eq)]
2052pub struct CacheInspectionEntry {
2053    pub product_key: ProductKey,
2054    pub product_kind: ProductKind,
2055    pub content_digest: Digest,
2056    pub state: CacheEntryState,
2057    pub weight: u64,
2058}
2059#[derive(Debug, Clone, PartialEq, Eq)]
2060pub struct ProductCacheInspection {
2061    pub schema_version: u32,
2062    pub entry_count: usize,
2063    pub total_weight: u64,
2064    pub limits: CacheLimits,
2065    pub entries: Vec<CacheInspectionEntry>,
2066}
2067impl ProductCacheInspection {
2068    #[must_use]
2069    pub fn to_canonical_json(&self) -> Vec<u8> {
2070        json_document(cache_json(self))
2071    }
2072}
2073
2074#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2075pub enum CompilerSessionStatus {
2076    Empty,
2077    Ready,
2078    Compiling,
2079    Failed,
2080    Closed,
2081}
2082#[derive(Debug, Clone)]
2083pub struct CommittedWorkspaceState {
2084    pub snapshot: Arc<WorkspaceSnapshot>,
2085    pub graph: Arc<WorkspaceGraph>,
2086    pub committed_product_keys: Vec<ProductKey>,
2087    pub committed_workspace_product_keys: Vec<WorkspaceProductKey>,
2088}
2089#[derive(Debug, Clone)]
2090pub struct CompilationAttemptState {
2091    pub attempt_id: CompilationAttemptId,
2092    pub baseline_snapshot_id: Option<WorkspaceSnapshotId>,
2093    pub candidate_snapshot: Arc<WorkspaceSnapshot>,
2094    pub plan: Arc<IncrementalPlan>,
2095    pub phase: AttemptPhase,
2096}
2097#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2098pub enum AttemptPhase {
2099    Planned,
2100    Executing,
2101    Validating,
2102    Committing,
2103}
2104pub struct CompilerSessionState {
2105    pub session_id: CompilerSessionId,
2106    pub workspace_id: WorkspaceId,
2107    pub compiler_contract: ContractVersion,
2108    pub status: CompilerSessionStatus,
2109    pub committed: Option<CommittedWorkspaceState>,
2110    pub active_attempt: Option<CompilationAttemptState>,
2111    pub cache: ProductCache,
2112    pub next_attempt: u64,
2113}
2114#[derive(Debug, Clone)]
2115pub struct CancellationToken(Arc<AtomicBool>);
2116impl CancellationToken {
2117    #[must_use]
2118    pub fn new() -> Self {
2119        Self(Arc::new(AtomicBool::new(false)))
2120    }
2121    pub fn cancel(&self) {
2122        self.0.store(true, Ordering::Release);
2123    }
2124    #[must_use]
2125    pub fn is_cancelled(&self) -> bool {
2126        self.0.load(Ordering::Acquire)
2127    }
2128}
2129impl Default for CancellationToken {
2130    fn default() -> Self {
2131        Self::new()
2132    }
2133}
2134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2135pub enum RequestedCompilationMode {
2136    Automatic,
2137    Full,
2138}
2139#[derive(Debug, Clone)]
2140pub struct CompileWorkspaceRequest {
2141    pub workspace: WorkspaceInput,
2142    pub mode: RequestedCompilationMode,
2143    pub cancellation: CancellationToken,
2144}
2145#[derive(Debug, Clone)]
2146pub struct CommittedCompilation {
2147    pub snapshot: Arc<WorkspaceSnapshot>,
2148    pub graph: Arc<WorkspaceGraph>,
2149    pub plan: Arc<IncrementalPlan>,
2150    /// L12-C's transient source-free query product for this exact successful
2151    /// compiler invocation. It is not retained in the committed L3 state.
2152    pub query_snapshot: Arc<ToolingQuerySnapshotV1>,
2153    /// Session-local L3 products that the L5 API explicitly permits a later
2154    /// request to offer back for validation and reuse.
2155    pub reusable_products: Vec<CanonicalReusableProductV1>,
2156}
2157#[derive(Debug, Clone)]
2158pub struct RejectedCompilation {
2159    pub message: String,
2160}
2161#[derive(Debug, Clone)]
2162pub struct CancelledCompilation {
2163    pub message: String,
2164}
2165#[derive(Debug, Clone)]
2166pub enum CompilationOutcome {
2167    Committed(CommittedCompilation),
2168    Rejected(RejectedCompilation),
2169    Cancelled(CancelledCompilation),
2170    PlatformFailure(PlatformFailure),
2171}
2172impl CompilerSessionState {
2173    #[must_use]
2174    pub fn new(
2175        workspace: WorkspaceId,
2176        compiler_contract: ContractVersion,
2177        limits: CacheLimits,
2178    ) -> Self {
2179        let session = NEXT_SESSION_ID.fetch_add(1, Ordering::Relaxed);
2180        Self {
2181            session_id: CompilerSessionId(format!("session:{session}")),
2182            workspace_id: workspace,
2183            compiler_contract,
2184            status: CompilerSessionStatus::Empty,
2185            committed: None,
2186            active_attempt: None,
2187            cache: ProductCache::new(limits),
2188            next_attempt: 1,
2189        }
2190    }
2191    #[must_use]
2192    pub fn committed_snapshot(&self) -> Option<&WorkspaceSnapshot> {
2193        self.committed.as_ref().map(|state| state.snapshot.as_ref())
2194    }
2195    #[must_use]
2196    pub fn committed_graph(&self) -> Option<&WorkspaceGraph> {
2197        self.committed.as_ref().map(|state| state.graph.as_ref())
2198    }
2199    #[must_use]
2200    pub const fn status(&self) -> CompilerSessionStatus {
2201        self.status
2202    }
2203    pub fn close(&mut self) {
2204        self.status = CompilerSessionStatus::Closed;
2205        self.active_attempt = None;
2206        self.committed = None;
2207        self.cache = ProductCache::new(CacheLimits::default());
2208    }
2209    pub fn compile_workspace(&mut self, request: CompileWorkspaceRequest) -> CompilationOutcome {
2210        if self.status == CompilerSessionStatus::Closed {
2211            return CompilationOutcome::PlatformFailure(PlatformFailure::new(
2212                PlatformFailureCode::SessionClosed,
2213                "compiler session is closed",
2214            ));
2215        }
2216        if self.active_attempt.is_some() {
2217            return CompilationOutcome::PlatformFailure(PlatformFailure::new(
2218                PlatformFailureCode::ConcurrentAttempt,
2219                "compiler session already has an active attempt",
2220            ));
2221        }
2222        let snapshot = match WorkspaceSnapshot::from_input(&request.workspace) {
2223            Ok(snapshot) => snapshot,
2224            Err(error) => return self.fail(error),
2225        };
2226        if request.cancellation.is_cancelled() {
2227            return self.cancel();
2228        }
2229        let plan = Arc::new(plan_incremental(
2230            self.committed
2231                .as_ref()
2232                .map(|state| (state.snapshot.as_ref(), state.graph.as_ref())),
2233            &snapshot,
2234            request.mode == RequestedCompilationMode::Full,
2235        ));
2236        let attempt_id = CompilationAttemptId(format!(
2237            "attempt:{}:{}",
2238            self.session_id.as_str().trim_start_matches("session:"),
2239            self.next_attempt
2240        ));
2241        self.next_attempt += 1;
2242        self.status = CompilerSessionStatus::Compiling;
2243        self.active_attempt = Some(CompilationAttemptState {
2244            attempt_id,
2245            baseline_snapshot_id: self
2246                .committed
2247                .as_ref()
2248                .map(|state| state.snapshot.snapshot_id.clone()),
2249            candidate_snapshot: Arc::new(snapshot.clone()),
2250            plan: Arc::clone(&plan),
2251            phase: AttemptPhase::Executing,
2252        });
2253        let (graph, query_snapshot, reusable_products, _, _) =
2254            match build_graph_with_reuse(&request.workspace, &snapshot, &[], &BTreeSet::new()) {
2255                Ok(result) => result,
2256                Err(error) => return self.fail(error),
2257            };
2258        if request.cancellation.is_cancelled() {
2259            return self.cancel();
2260        }
2261        if let Err(error) = snapshot.validate().and_then(|()| graph.validate()) {
2262            return self.fail(error);
2263        }
2264        if let Some(attempt) = self.active_attempt.as_mut() {
2265            attempt.phase = AttemptPhase::Committing;
2266        } else {
2267            return self.fail(PlatformFailure::new(
2268                PlatformFailureCode::CommitInvariantFailed,
2269                "active compiler attempt disappeared before commit",
2270            ));
2271        }
2272        let committed = CommittedWorkspaceState {
2273            snapshot: Arc::new(snapshot),
2274            graph: Arc::new(graph),
2275            committed_product_keys: Vec::new(),
2276            committed_workspace_product_keys: Vec::new(),
2277        };
2278        self.workspace_id = committed.snapshot.workspace_id.clone();
2279        self.compiler_contract = committed.snapshot.compiler_contract.clone();
2280        self.committed = Some(committed.clone());
2281        self.active_attempt = None;
2282        self.status = CompilerSessionStatus::Ready;
2283        self.cache.promote_and_evict();
2284        CompilationOutcome::Committed(CommittedCompilation {
2285            snapshot: committed.snapshot,
2286            graph: committed.graph,
2287            plan,
2288            query_snapshot: Arc::new(query_snapshot),
2289            reusable_products,
2290        })
2291    }
2292
2293    /// L5's narrow canonical incremental entry point. It validates every
2294    /// offered parse product itself and falls back to recomputation for any
2295    /// product it cannot prove safe; the service plan is never semantic
2296    /// authority.
2297    pub fn compile_workspace_incremental_v1(
2298        &mut self,
2299        request: IncrementalCompileWorkspaceRequestV1,
2300    ) -> IncrementalCompilationOutcomeV1 {
2301        if self.status == CompilerSessionStatus::Closed {
2302            return IncrementalCompilationOutcomeV1 {
2303                outcome: CompilationOutcome::PlatformFailure(PlatformFailure::new(
2304                    PlatformFailureCode::SessionClosed,
2305                    "compiler session is closed",
2306                )),
2307                reused_product_identities: Vec::new(),
2308                recomputed_work_units: Vec::new(),
2309                rejected_reuse_product_identities: Vec::new(),
2310            };
2311        }
2312        let snapshot = match WorkspaceSnapshot::from_input(&request.workspace) {
2313            Ok(snapshot) => snapshot,
2314            Err(error) => {
2315                return IncrementalCompilationOutcomeV1 {
2316                    outcome: self.fail(error),
2317                    reused_product_identities: Vec::new(),
2318                    recomputed_work_units: Vec::new(),
2319                    rejected_reuse_product_identities: Vec::new(),
2320                };
2321            }
2322        };
2323        if request.cancellation.is_cancelled() {
2324            return IncrementalCompilationOutcomeV1 {
2325                outcome: self.cancel(),
2326                reused_product_identities: Vec::new(),
2327                recomputed_work_units: Vec::new(),
2328                rejected_reuse_product_identities: Vec::new(),
2329            };
2330        }
2331        if request.plan.to_canonical_json().is_err()
2332            || request.plan.candidate_configuration_fingerprint
2333                != snapshot.configuration_fingerprint
2334            || request.plan.candidate_source_universe_fingerprint
2335                != source_universe_fingerprint_v1(&snapshot)
2336        {
2337            return IncrementalCompilationOutcomeV1 {
2338                outcome: self.fail(PlatformFailure::new(
2339                    PlatformFailureCode::InvalidIncrementalPlan,
2340                    "L5 incremental plan does not match the canonical candidate",
2341                )),
2342                reused_product_identities: Vec::new(),
2343                recomputed_work_units: Vec::new(),
2344                rejected_reuse_product_identities: Vec::new(),
2345            };
2346        }
2347        let attempt_id = CompilationAttemptId(format!(
2348            "attempt:{}:{}",
2349            self.session_id.as_str().trim_start_matches("session:"),
2350            self.next_attempt
2351        ));
2352        self.next_attempt += 1;
2353        self.status = CompilerSessionStatus::Compiling;
2354        let legacy_plan = Arc::new(plan_incremental(None, &snapshot, true));
2355        self.active_attempt = Some(CompilationAttemptState {
2356            attempt_id,
2357            baseline_snapshot_id: request
2358                .plan
2359                .baseline_publication_identity
2360                .as_ref()
2361                .map(|value| WorkspaceSnapshotId(value.clone())),
2362            candidate_snapshot: Arc::new(snapshot.clone()),
2363            plan: Arc::clone(&legacy_plan),
2364            phase: AttemptPhase::Executing,
2365        });
2366        let invalidated = request
2367            .plan
2368            .invalidation_closure
2369            .iter()
2370            .cloned()
2371            .collect::<BTreeSet<_>>();
2372        let (graph, query_snapshot, reusable_products, reused, rejected) =
2373            match build_graph_with_reuse(
2374                &request.workspace,
2375                &snapshot,
2376                &request.reusable_products,
2377                &invalidated,
2378            ) {
2379                Ok(result) => result,
2380                Err(error) => {
2381                    return IncrementalCompilationOutcomeV1 {
2382                        outcome: self.fail(error),
2383                        reused_product_identities: Vec::new(),
2384                        recomputed_work_units: Vec::new(),
2385                        rejected_reuse_product_identities: Vec::new(),
2386                    };
2387                }
2388            };
2389        if let Err(error) = snapshot.validate().and_then(|()| graph.validate()) {
2390            return IncrementalCompilationOutcomeV1 {
2391                outcome: self.fail(error),
2392                reused_product_identities: Vec::new(),
2393                recomputed_work_units: Vec::new(),
2394                rejected_reuse_product_identities: rejected,
2395            };
2396        }
2397        let committed = CommittedWorkspaceState {
2398            snapshot: Arc::new(snapshot),
2399            graph: Arc::new(graph),
2400            committed_product_keys: Vec::new(),
2401            committed_workspace_product_keys: Vec::new(),
2402        };
2403        self.workspace_id = committed.snapshot.workspace_id.clone();
2404        self.compiler_contract = committed.snapshot.compiler_contract.clone();
2405        self.committed = Some(committed.clone());
2406        self.active_attempt = None;
2407        self.status = CompilerSessionStatus::Ready;
2408        IncrementalCompilationOutcomeV1 {
2409            outcome: CompilationOutcome::Committed(CommittedCompilation {
2410                snapshot: committed.snapshot,
2411                graph: committed.graph,
2412                plan: legacy_plan,
2413                query_snapshot: Arc::new(query_snapshot),
2414                reusable_products,
2415            }),
2416            reused_product_identities: reused,
2417            recomputed_work_units: request.plan.recompute_work_units,
2418            rejected_reuse_product_identities: rejected,
2419        }
2420    }
2421    fn fail(&mut self, error: PlatformFailure) -> CompilationOutcome {
2422        self.active_attempt = None;
2423        self.status = CompilerSessionStatus::Failed;
2424        CompilationOutcome::PlatformFailure(error)
2425    }
2426    fn cancel(&mut self) -> CompilationOutcome {
2427        self.active_attempt = None;
2428        self.status = CompilerSessionStatus::Failed;
2429        CompilationOutcome::Cancelled(CancelledCompilation {
2430            message: "compilation cancelled".into(),
2431        })
2432    }
2433    #[must_use]
2434    pub fn inspect(&self) -> CompilerSessionInspection {
2435        CompilerSessionInspection {
2436            schema_version: COMPILER_SESSION_SCHEMA_VERSION,
2437            session_id: self.session_id.clone(),
2438            workspace_id: self.workspace_id.clone(),
2439            compiler_contract: self.compiler_contract.clone(),
2440            status: self.status,
2441            committed_snapshot_id: self
2442                .committed
2443                .as_ref()
2444                .map(|value| value.snapshot.snapshot_id.clone()),
2445            active_attempt: self
2446                .active_attempt
2447                .as_ref()
2448                .map(|value| value.attempt_id.clone()),
2449            committed_product_count: self
2450                .committed
2451                .as_ref()
2452                .map_or(0, |value| value.committed_product_keys.len()),
2453            cache: CacheSummary {
2454                entry_count: self.cache.entries.len(),
2455                total_weight: self.cache.total_weight,
2456            },
2457        }
2458    }
2459}
2460type GraphBuildResult = (
2461    WorkspaceGraph,
2462    ToolingQuerySnapshotV1,
2463    Vec<CanonicalReusableProductV1>,
2464    Vec<ProductKey>,
2465    Vec<ProductKey>,
2466);
2467
2468fn build_graph_with_reuse(
2469    input: &WorkspaceInput,
2470    snapshot: &WorkspaceSnapshot,
2471    offered: &[CanonicalReusableProductV1],
2472    invalidated: &BTreeSet<SourceUnitId>,
2473) -> Result<GraphBuildResult, PlatformFailure> {
2474    let (_, fingerprint, inputs) = normalize(input)?;
2475    let parser_contract = ContractVersion::new("parser:1");
2476    let offered_by_unit = offered
2477        .iter()
2478        .map(|product| (product.source_unit_id.clone(), product))
2479        .collect::<BTreeMap<_, _>>();
2480    let mut reused = Vec::new();
2481    let mut rejected = Vec::new();
2482    let mut parsed = Vec::new();
2483    let mut reusable_products = Vec::new();
2484    for source in &inputs {
2485        let expected_key = product_key(
2486            &source.source_unit_id,
2487            &source.source_revision_id,
2488            ProductKind::Parse,
2489            &parser_contract,
2490            &fingerprint,
2491        );
2492        let reusable = offered_by_unit
2493            .get(&source.source_unit_id)
2494            .and_then(|product| {
2495                let product = *product;
2496                let valid = !invalidated.contains(&source.source_unit_id)
2497                    && product.source_revision_id == source.source_revision_id
2498                    && product.product_key == expected_key
2499                    && WorkspacePath::new(product.parsed().path.to_string_lossy())
2500                        .is_ok_and(|path| path == source.path);
2501                if valid {
2502                    Some(product)
2503                } else {
2504                    rejected.push(product.product_key.clone());
2505                    None
2506                }
2507            });
2508        let file = reusable.map_or_else(
2509            || presolve_parser::parse_file(source.path.as_str(), &source.source),
2510            |product| {
2511                reused.push(product.product_key.clone());
2512                product.parsed().clone()
2513            },
2514        );
2515        reusable_products.push(CanonicalReusableProductV1 {
2516            source_unit_id: source.source_unit_id.clone(),
2517            source_revision_id: source.source_revision_id.clone(),
2518            product_key: expected_key,
2519            parsed: Arc::new(file.clone()),
2520        });
2521        parsed.push(file);
2522    }
2523    let unit = CompilationUnit::from_parsed_files(parsed.clone());
2524    let model = build_application_semantic_model_for_unit(&unit);
2525    let query_snapshot = build_tooling_query_snapshot_v1(snapshot, &model).map_err(|_| {
2526        PlatformFailure::new(
2527            PlatformFailureCode::CommitInvariantFailed,
2528            "compiler query snapshot provenance is not bound to the validated workspace snapshot",
2529        )
2530    })?;
2531    let model_digest = Digest::sha256(format!(
2532        "components:{};diagnostics:{}",
2533        model.components.len(),
2534        model.diagnostics.len()
2535    ));
2536    let units = inputs
2537        .iter()
2538        .map(|source| {
2539            let key = product_key(
2540                &source.source_unit_id,
2541                &source.source_revision_id,
2542                ProductKind::Parse,
2543                &parser_contract,
2544                &fingerprint,
2545            );
2546            WorkspaceUnit {
2547                source_unit_id: source.source_unit_id.clone(),
2548                path: source.path.clone(),
2549                source_revision_id: source.source_revision_id.clone(),
2550                source_digest: source.source_digest.clone(),
2551                source_length: source.source.len() as u64,
2552                language: source.language,
2553                products: vec![UnitProductRef {
2554                    product_kind: ProductKind::Parse,
2555                    product_key: key,
2556                    producer_contract: parser_contract.clone(),
2557                    content_digest: source.source_digest.clone(),
2558                }],
2559            }
2560        })
2561        .collect::<Vec<_>>();
2562    let by_path = inputs
2563        .iter()
2564        .map(|source| (source.path.clone(), source.source_unit_id.clone()))
2565        .collect::<BTreeMap<_, _>>();
2566    let mut edges = Vec::new();
2567    for (source, file) in inputs.iter().zip(parsed.iter()) {
2568        for import in &file.imports {
2569            if let Some(target) = resolve_import(&source.path, &import.source, &by_path) {
2570                edges.push(WorkspaceDependencyEdge {
2571                    source: source.source_unit_id.clone(),
2572                    kind: WorkspaceDependencyKind::Compile,
2573                    target,
2574                });
2575            }
2576        }
2577    }
2578    edges.sort_by_key(|edge| (edge.source.clone(), edge.kind, edge.target.clone()));
2579    edges.dedup();
2580    let asm_contract = ContractVersion::new("asm:1");
2581    let application_products = vec![ApplicationProductRef {
2582        product_kind: ApplicationProductKind::ApplicationSemanticModel,
2583        product_key: workspace_product_key(
2584            &snapshot.workspace_id,
2585            &snapshot.snapshot_id,
2586            ApplicationProductKind::ApplicationSemanticModel,
2587            &asm_contract,
2588            &fingerprint,
2589        ),
2590        producer_contract: asm_contract,
2591        content_digest: model_digest,
2592    }];
2593    let graph = WorkspaceGraph {
2594        schema_version: WORKSPACE_GRAPH_SCHEMA_VERSION,
2595        workspace_id: snapshot.workspace_id.clone(),
2596        snapshot_id: snapshot.snapshot_id.clone(),
2597        compiler_contract: snapshot.compiler_contract.clone(),
2598        units,
2599        dependency_edges: edges,
2600        application_products,
2601    };
2602    reused.sort();
2603    reused.dedup();
2604    rejected.sort();
2605    rejected.dedup();
2606    Ok((graph, query_snapshot, reusable_products, reused, rejected))
2607}
2608fn resolve_import(
2609    source: &WorkspacePath,
2610    raw: &str,
2611    known: &BTreeMap<WorkspacePath, SourceUnitId>,
2612) -> Option<SourceUnitId> {
2613    if !raw.starts_with('.') {
2614        return None;
2615    }
2616    let mut parts = source.0.split('/').collect::<Vec<_>>();
2617    parts.pop();
2618    let separated = raw.replace('\\', "/");
2619    for part in separated.split('/') {
2620        match part {
2621            "." => {}
2622            ".." => {
2623                parts.pop()?;
2624            }
2625            value => parts.push(value),
2626        }
2627    }
2628    let base = parts.join("/");
2629    for candidate in [
2630        base.clone(),
2631        format!("{base}.ts"),
2632        format!("{base}.tsx"),
2633        format!("{base}.js"),
2634        format!("{base}.jsx"),
2635        format!("{base}/index.ts"),
2636        format!("{base}/index.tsx"),
2637    ] {
2638        if let Ok(path) = WorkspacePath::new(candidate) {
2639            if let Some(id) = known.get(&path) {
2640                return Some(id.clone());
2641            }
2642        }
2643    }
2644    None
2645}
2646
2647#[derive(Debug, Clone, PartialEq, Eq)]
2648pub struct CacheSummary {
2649    pub entry_count: usize,
2650    pub total_weight: u64,
2651}
2652#[derive(Debug, Clone, PartialEq, Eq)]
2653pub struct CompilerSessionInspection {
2654    pub schema_version: u32,
2655    pub session_id: CompilerSessionId,
2656    pub workspace_id: WorkspaceId,
2657    pub compiler_contract: ContractVersion,
2658    pub status: CompilerSessionStatus,
2659    pub committed_snapshot_id: Option<WorkspaceSnapshotId>,
2660    pub active_attempt: Option<CompilationAttemptId>,
2661    pub committed_product_count: usize,
2662    pub cache: CacheSummary,
2663}
2664impl CompilerSessionInspection {
2665    #[must_use]
2666    pub fn to_canonical_json(&self) -> Vec<u8> {
2667        json_document(session_json(self))
2668    }
2669}
2670#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2671pub enum PlatformFailureCode {
2672    InvalidWorkspacePath,
2673    DuplicateWorkspacePath,
2674    UnsupportedSourceLanguage,
2675    InputReadFailed,
2676    InvalidWorkspaceGraph,
2677    InvalidWorkspaceSnapshot,
2678    InvalidIncrementalPlan,
2679    UnsupportedSchemaVersion,
2680    UnsupportedProducerContract,
2681    CacheCorruption,
2682    ProductDigestMismatch,
2683    DuplicateProductMismatch,
2684    MissingRequiredProduct,
2685    DependencyAuthorityUnavailable,
2686    ConcurrentAttempt,
2687    SessionClosed,
2688    CommitInvariantFailed,
2689    IncrementalEquivalenceFailed,
2690}
2691#[derive(Debug, Clone, PartialEq, Eq)]
2692pub struct PlatformFailure {
2693    pub code: PlatformFailureCode,
2694    pub message: String,
2695    pub workspace_path: Option<WorkspacePath>,
2696    pub source_unit_id: Option<SourceUnitId>,
2697    pub product_key: Option<ProductKey>,
2698}
2699impl PlatformFailure {
2700    fn new(code: PlatformFailureCode, message: impl Into<String>) -> Self {
2701        Self {
2702            code,
2703            message: message.into(),
2704            workspace_path: None,
2705            source_unit_id: None,
2706            product_key: None,
2707        }
2708    }
2709    fn path(message: impl Into<String>, path: &str) -> Self {
2710        Self {
2711            code: PlatformFailureCode::InvalidWorkspacePath,
2712            message: message.into(),
2713            workspace_path: Some(WorkspacePath(path.replace('\\', "/"))),
2714            source_unit_id: None,
2715            product_key: None,
2716        }
2717    }
2718}
2719#[derive(Debug, Clone)]
2720pub struct PlatformSerializationError {
2721    pub message: String,
2722}
2723impl PlatformSerializationError {
2724    fn validation(error: PlatformFailure) -> Self {
2725        Self {
2726            message: error.message,
2727        }
2728    }
2729}
2730
2731fn quote(value: &str) -> String {
2732    serde_json::to_string(value).expect("strings serialize")
2733}
2734fn json_document(value: String) -> Vec<u8> {
2735    let mut value = value.into_bytes();
2736    value.push(b'\n');
2737    value
2738}
2739fn snapshot_json(snapshot: &WorkspaceSnapshot) -> String {
2740    format!("{{\"schema_version\":{},\"workspace_id\":{},\"snapshot_id\":{},\"compiler_contract\":{},\"configuration_fingerprint\":{},\"units\":[{}]}}",snapshot.schema_version,quote(snapshot.workspace_id.as_str()),quote(snapshot.snapshot_id.as_str()),quote(snapshot.compiler_contract.as_str()),quote(snapshot.configuration_fingerprint.as_str()),snapshot.units.iter().map(|unit|format!("{{\"source_unit_id\":{},\"path\":{},\"source_revision_id\":{},\"source_digest\":{},\"source_length\":{},\"language\":{}}}",quote(unit.source_unit_id.as_str()),quote(unit.path.as_str()),quote(unit.source_revision_id.as_str()),quote(unit.source_digest.as_str()),unit.source_length,quote(unit.language.as_str()))).collect::<Vec<_>>().join(","))
2741}
2742fn graph_json(graph: &WorkspaceGraph) -> String {
2743    format!("{{\"schema_version\":{},\"workspace_id\":{},\"snapshot_id\":{},\"compiler_contract\":{},\"units\":[{}],\"dependency_edges\":[{}],\"application_products\":[{}]}}",graph.schema_version,quote(graph.workspace_id.as_str()),quote(graph.snapshot_id.as_str()),quote(graph.compiler_contract.as_str()),graph.units.iter().map(|unit|format!("{{\"source_unit_id\":{},\"path\":{},\"source_revision_id\":{},\"source_digest\":{},\"source_length\":{},\"language\":{},\"products\":[{}]}}",quote(unit.source_unit_id.as_str()),quote(unit.path.as_str()),quote(unit.source_revision_id.as_str()),quote(unit.source_digest.as_str()),unit.source_length,quote(unit.language.as_str()),unit.products.iter().map(|product|format!("{{\"product_kind\":{},\"product_key\":{},\"producer_contract\":{},\"content_digest\":{}}}",quote(product.product_kind.as_str()),quote(product.product_key.as_str()),quote(product.producer_contract.as_str()),quote(product.content_digest.as_str()))).collect::<Vec<_>>().join(","))).collect::<Vec<_>>().join(","),graph.dependency_edges.iter().map(|edge|format!("{{\"source\":{},\"kind\":{},\"target\":{}}}",quote(edge.source.as_str()),quote(edge.kind.as_str()),quote(edge.target.as_str()))).collect::<Vec<_>>().join(","),graph.application_products.iter().map(|product|format!("{{\"product_kind\":{},\"product_key\":{},\"producer_contract\":{},\"content_digest\":{}}}",quote(product.product_kind.as_str()),quote(product.product_key.as_str()),quote(product.producer_contract.as_str()),quote(product.content_digest.as_str()))).collect::<Vec<_>>().join(","))
2744}
2745
2746fn incremental_plan_json(plan: &IncrementalPlan) -> String {
2747    let ids = |values: &[SourceUnitId]| {
2748        values
2749            .iter()
2750            .map(|value| quote(value.as_str()))
2751            .collect::<Vec<_>>()
2752            .join(",")
2753    };
2754    let changes = format!("{{\"baseline_snapshot_id\":{},\"candidate_snapshot_id\":{},\"configuration_changed\":{},\"added\":[{}],\"removed\":[{}],\"modified\":[{}],\"unchanged\":[{}]}}", quote(plan.changes.baseline_snapshot_id.as_str()), quote(plan.changes.candidate_snapshot_id.as_str()), plan.changes.configuration_changed, ids(&plan.changes.added), ids(&plan.changes.removed), ids(&plan.changes.modified), ids(&plan.changes.unchanged));
2755    let invalidated = plan
2756        .invalidated_units
2757        .iter()
2758        .map(|item| {
2759            format!(
2760                "{{\"source_unit_id\":{},\"reason\":{},\"caused_by\":[{}]}}",
2761                quote(item.source_unit_id.as_str()),
2762                quote(item.reason.as_str()),
2763                ids(&item.caused_by)
2764            )
2765        })
2766        .collect::<Vec<_>>()
2767        .join(",");
2768    let stages = plan
2769        .stages
2770        .iter()
2771        .map(|stage| {
2772            format!(
2773                "{{\"stage_index\":{},\"kind\":{},\"units\":[{}]}}",
2774                stage.stage_index,
2775                quote(stage.kind.as_str()),
2776                ids(&stage.units)
2777            )
2778        })
2779        .collect::<Vec<_>>()
2780        .join(",");
2781    format!("{{\"schema_version\":{},\"plan_id\":{},\"baseline_snapshot_id\":{},\"candidate_snapshot_id\":{},\"mode\":{},\"changes\":{},\"invalidated_units\":[{}],\"stages\":[{}],\"retained_products\":[{}],\"discarded_products\":[{}],\"expected_commit\":{{\"snapshot_id\":{},\"workspace_graph_schema_version\":{}}}}}", plan.schema_version, quote(plan.plan_id.as_str()), quote(plan.baseline_snapshot_id.as_str()), quote(plan.candidate_snapshot_id.as_str()), quote(plan.mode.as_str()), changes, invalidated, stages, plan.retained_products.iter().map(|key|quote(key.as_str())).collect::<Vec<_>>().join(","), plan.discarded_products.iter().map(|key|quote(key.as_str())).collect::<Vec<_>>().join(","), quote(plan.expected_commit.snapshot_id.as_str()), plan.expected_commit.workspace_graph_schema_version)
2782}
2783fn l5_plan_bytes_without_fingerprint(plan: &IncrementalCompilationPlanV1) -> Vec<u8> {
2784    l5_plan_json_without_fingerprint(plan).into_bytes()
2785}
2786fn l5_plan_json_without_fingerprint(plan: &IncrementalCompilationPlanV1) -> String {
2787    let ids = |values: &[SourceUnitId]| {
2788        values
2789            .iter()
2790            .map(|value| quote(value.as_str()))
2791            .collect::<Vec<_>>()
2792            .join(",")
2793    };
2794    let products = plan
2795        .reusable_product_identities
2796        .iter()
2797        .map(|value| quote(value.as_str()))
2798        .collect::<Vec<_>>()
2799        .join(",");
2800    let changes = plan
2801        .input_changes
2802        .iter()
2803        .map(|change| {
2804            format!(
2805                "{{\"kind\":{},\"identity\":{}}}",
2806                quote(change.kind.as_str()),
2807                quote(&change.identity)
2808            )
2809        })
2810        .collect::<Vec<_>>()
2811        .join(",");
2812    let fallback = plan
2813        .fallback_reasons
2814        .iter()
2815        .map(|reason| quote(reason.code()))
2816        .collect::<Vec<_>>()
2817        .join(",");
2818    format!(
2819        "{{\"schema\":{},\"baseline_publication_identity\":{},\"candidate_configuration_fingerprint\":{},\"candidate_source_universe_fingerprint\":{},\"input_changes\":[{}],\"directly_invalidated\":[{}],\"invalidation_closure\":[{}],\"reusable_product_identities\":[{}],\"recompute_work_units\":[{}],\"fallback_reasons\":[{}],\"mode\":{}}}",
2820        quote(plan.schema),
2821        plan.baseline_publication_identity.as_ref().map_or_else(|| "null".into(), |value| quote(value)),
2822        quote(plan.candidate_configuration_fingerprint.as_str()),
2823        quote(plan.candidate_source_universe_fingerprint.as_str()),
2824        changes,
2825        ids(&plan.directly_invalidated),
2826        ids(&plan.invalidation_closure),
2827        products,
2828        ids(&plan.recompute_work_units),
2829        fallback,
2830        quote(plan.mode.as_str()),
2831    )
2832}
2833fn l5_plan_json(plan: &IncrementalCompilationPlanV1) -> String {
2834    let without = l5_plan_json_without_fingerprint(plan);
2835    without
2836        .strip_suffix('}')
2837        .expect("canonical L5 plan is an object")
2838        .to_owned()
2839        + &format!(
2840            ",\"plan_fingerprint\":{}}}",
2841            quote(plan.plan_fingerprint.as_str())
2842        )
2843}
2844fn cache_json(cache: &ProductCacheInspection) -> String {
2845    let entries = cache.entries.iter().map(|entry| format!("{{\"product_key\":{},\"product_kind\":{},\"content_digest\":{},\"state\":{},\"weight\":{}}}", quote(entry.product_key.as_str()), quote(entry.product_kind.as_str()), quote(entry.content_digest.as_str()), quote(match entry.state { CacheEntryState::AttemptLocal => "attempt_local", CacheEntryState::Committed => "committed" }), entry.weight)).collect::<Vec<_>>().join(",");
2846    format!("{{\"schema_version\":{},\"entry_count\":{},\"total_weight\":{},\"limits\":{{\"maximum_entries\":{},\"maximum_weight\":{}}},\"entries\":[{}]}}", cache.schema_version, cache.entry_count, cache.total_weight, cache.limits.maximum_entries, cache.limits.maximum_weight, entries)
2847}
2848fn session_json(session: &CompilerSessionInspection) -> String {
2849    format!("{{\"schema_version\":{},\"session_id\":{},\"workspace_id\":{},\"compiler_contract\":{},\"status\":{},\"committed_snapshot_id\":{},\"active_attempt\":{},\"committed_product_count\":{},\"cache\":{{\"entry_count\":{},\"total_weight\":{}}}}}", session.schema_version, quote(session.session_id.as_str()), quote(session.workspace_id.as_str()), quote(session.compiler_contract.as_str()), quote(match session.status { CompilerSessionStatus::Empty => "empty", CompilerSessionStatus::Ready => "ready", CompilerSessionStatus::Compiling => "compiling", CompilerSessionStatus::Failed => "failed", CompilerSessionStatus::Closed => "closed" }), session.committed_snapshot_id.as_ref().map_or_else(|| "null".into(), |id|quote(id.as_str())), session.active_attempt.as_ref().map_or_else(|| "null".into(), |id|quote(id.as_str())), session.committed_product_count, session.cache.entry_count, session.cache.total_weight)
2850}
2851
2852/// Public submodule names are stable platform contracts; their values live in
2853/// this file to keep the first platform implementation compact.
2854pub mod workspace {
2855    pub use super::{
2856        ApplicationProductKind, ApplicationProductRef, ContractVersion, Digest, ProductKey,
2857        ProductKind, SourceLanguage, SourceRevisionId, SourceUnitId, UnitProductRef,
2858        WorkspaceConfiguration, WorkspaceDependencyEdge, WorkspaceDependencyKind, WorkspaceGraph,
2859        WorkspaceId, WorkspaceInput, WorkspacePath, WorkspaceProductKey, WorkspaceSource,
2860        WorkspaceUnit,
2861    };
2862}
2863pub mod snapshot {
2864    pub use super::{
2865        compare_snapshots, SnapshotUnit, WorkspaceChangeSet, WorkspaceSnapshot, WorkspaceSnapshotId,
2866    };
2867}
2868pub mod incremental {
2869    pub use super::{
2870        force_clean_fallback_v1, plan_incremental, plan_incremental_compilation_v1,
2871        source_universe_fingerprint_v1, CanonicalReusableProductV1, ExpectedCommit,
2872        IncrementalCompilationModeV1, IncrementalCompilationOutcomeV1,
2873        IncrementalCompilationPlanV1, IncrementalCompileWorkspaceRequestV1,
2874        IncrementalFallbackReasonV1, IncrementalInputChangeKindV1, IncrementalInputChangeV1,
2875        IncrementalMode, IncrementalPlan, IncrementalPlanId, IncrementalStage,
2876        IncrementalStageKind, InvalidatedUnit, InvalidationReason,
2877    };
2878}
2879pub mod cache {
2880    pub use super::{
2881        CacheEntry, CacheEntryState, CacheInspectionEntry, CacheLimits, CachedProduct,
2882        ProductCache, ProductCacheInspection,
2883    };
2884}
2885pub mod session {
2886    pub use super::{
2887        CancellationToken, CommittedCompilation, CompilationAttemptId, CompilationOutcome,
2888        CompileWorkspaceRequest, CompilerSessionId, CompilerSessionInspection,
2889        CompilerSessionState, CompilerSessionStatus, IncrementalCompilationOutcomeV1,
2890        IncrementalCompileWorkspaceRequestV1, RequestedCompilationMode,
2891    };
2892}
2893pub mod inspection {
2894    pub use super::{CompilerSessionInspection, ProductCacheInspection};
2895}
2896
2897#[cfg(test)]
2898mod tests {
2899    use super::*;
2900    fn source(path: &str, body: &str) -> WorkspaceSource {
2901        WorkspaceSource {
2902            path: path.into(),
2903            source: body.into(),
2904            language: None,
2905        }
2906    }
2907    fn input(sources: Vec<WorkspaceSource>) -> WorkspaceInput {
2908        WorkspaceInput::new(sources)
2909    }
2910    #[test]
2911    fn paths_normalize_and_reject_escape() {
2912        assert_eq!(
2913            WorkspacePath::new("src\\./Counter.tsx").unwrap().as_str(),
2914            "src/Counter.tsx"
2915        );
2916        assert!(WorkspacePath::new("../Counter.tsx").is_err());
2917        assert!(WorkspacePath::new("/Counter.tsx").is_err());
2918    }
2919    #[test]
2920    fn identities_are_content_and_path_sensitive() {
2921        let a =
2922            WorkspaceSnapshot::from_input(&input(vec![source("src/A.tsx", "class A {}")])).unwrap();
2923        let b = WorkspaceSnapshot::from_input(&input(vec![source("src/A.tsx", "class A { x=1 }")]))
2924            .unwrap();
2925        let c =
2926            WorkspaceSnapshot::from_input(&input(vec![source("src/B.tsx", "class A {}")])).unwrap();
2927        assert_eq!(a.units[0].source_unit_id, b.units[0].source_unit_id);
2928        assert_ne!(a.units[0].source_revision_id, b.units[0].source_revision_id);
2929        assert_ne!(a.units[0].source_unit_id, c.units[0].source_unit_id);
2930    }
2931    #[test]
2932    fn snapshots_and_graphs_are_enumeration_independent() {
2933        let left = input(vec![
2934            source("src/B.tsx", "class B {}"),
2935            source("src/A.tsx", "class A {}"),
2936        ]);
2937        let right = input(vec![
2938            source("src/A.tsx", "class A {}"),
2939            source("src/B.tsx", "class B {}"),
2940        ]);
2941        let l = WorkspaceSnapshot::from_input(&left).unwrap();
2942        let r = WorkspaceSnapshot::from_input(&right).unwrap();
2943        assert_eq!(
2944            l.to_canonical_json().unwrap(),
2945            r.to_canonical_json().unwrap()
2946        );
2947        let mut session = CompilerSessionState::new(
2948            l.workspace_id.clone(),
2949            l.compiler_contract.clone(),
2950            CacheLimits::default(),
2951        );
2952        let outcome = session.compile_workspace(CompileWorkspaceRequest {
2953            workspace: left,
2954            mode: RequestedCompilationMode::Full,
2955            cancellation: CancellationToken::new(),
2956        });
2957        let CompilationOutcome::Committed(done) = outcome else {
2958            panic!("expected commit")
2959        };
2960        assert!(done.graph.to_canonical_json().unwrap().ends_with(b"\n"));
2961    }
2962    #[test]
2963    fn changes_classify_rename_as_removal_and_addition() {
2964        let first =
2965            WorkspaceSnapshot::from_input(&input(vec![source("src/A.tsx", "class A {}")])).unwrap();
2966        let second =
2967            WorkspaceSnapshot::from_input(&input(vec![source("src/B.tsx", "class A {}")])).unwrap();
2968        let changes = compare_snapshots(&first, &second);
2969        assert_eq!(changes.added.len(), 1);
2970        assert_eq!(changes.removed.len(), 1);
2971    }
2972    #[test]
2973    fn session_cancellation_keeps_baseline() {
2974        let initial = input(vec![source("src/A.tsx", "class A {}")]);
2975        let snapshot = WorkspaceSnapshot::from_input(&initial).unwrap();
2976        let mut session = CompilerSessionState::new(
2977            snapshot.workspace_id.clone(),
2978            snapshot.compiler_contract.clone(),
2979            CacheLimits::default(),
2980        );
2981        assert!(matches!(
2982            session.compile_workspace(CompileWorkspaceRequest {
2983                workspace: initial,
2984                mode: RequestedCompilationMode::Full,
2985                cancellation: CancellationToken::new()
2986            }),
2987            CompilationOutcome::Committed(_)
2988        ));
2989        let baseline = session.committed_snapshot().unwrap().snapshot_id.clone();
2990        let cancellation = CancellationToken::new();
2991        cancellation.cancel();
2992        assert!(matches!(
2993            session.compile_workspace(CompileWorkspaceRequest {
2994                workspace: input(vec![source("src/A.tsx", "class A { value=1 }")]),
2995                mode: RequestedCompilationMode::Automatic,
2996                cancellation
2997            }),
2998            CompilationOutcome::Cancelled(_)
2999        ));
3000        assert_eq!(session.committed_snapshot().unwrap().snapshot_id, baseline);
3001    }
3002
3003    #[test]
3004    fn clean_full_equivalence_for_incremental_workspace_changes() {
3005        let baseline = input(vec![
3006            source("src/Dependency.tsx", "export class Dependency {}"),
3007            source(
3008                "src/App.tsx",
3009                "import { Dependency } from './Dependency'; export class App {}",
3010            ),
3011        ]);
3012        let candidate = input(vec![
3013            source(
3014                "src/Dependency.tsx",
3015                "export class Dependency { value = 1; }",
3016            ),
3017            source(
3018                "src/App.tsx",
3019                "import { Dependency } from './Dependency'; export class App {}",
3020            ),
3021            source("src/Added.tsx", "export class Added {}"),
3022        ]);
3023        let initial = WorkspaceSnapshot::from_input(&baseline).unwrap();
3024        let mut incremental = CompilerSessionState::new(
3025            initial.workspace_id.clone(),
3026            initial.compiler_contract.clone(),
3027            CacheLimits::default(),
3028        );
3029        assert!(matches!(
3030            incremental.compile_workspace(CompileWorkspaceRequest {
3031                workspace: baseline,
3032                mode: RequestedCompilationMode::Full,
3033                cancellation: CancellationToken::new(),
3034            }),
3035            CompilationOutcome::Committed(_)
3036        ));
3037        let incremental_result = incremental.compile_workspace(CompileWorkspaceRequest {
3038            workspace: candidate.clone(),
3039            mode: RequestedCompilationMode::Automatic,
3040            cancellation: CancellationToken::new(),
3041        });
3042        let CompilationOutcome::Committed(incremental_result) = incremental_result else {
3043            panic!("incremental candidate should commit");
3044        };
3045        assert_eq!(incremental_result.plan.mode, IncrementalMode::Incremental);
3046
3047        let mut clean = CompilerSessionState::new(
3048            initial.workspace_id,
3049            initial.compiler_contract,
3050            CacheLimits::default(),
3051        );
3052        let clean_result = clean.compile_workspace(CompileWorkspaceRequest {
3053            workspace: candidate,
3054            mode: RequestedCompilationMode::Full,
3055            cancellation: CancellationToken::new(),
3056        });
3057        let CompilationOutcome::Committed(clean_result) = clean_result else {
3058            panic!("clean candidate should commit");
3059        };
3060        assert_eq!(
3061            incremental_result.snapshot.to_canonical_json().unwrap(),
3062            clean_result.snapshot.to_canonical_json().unwrap()
3063        );
3064        assert_eq!(
3065            incremental_result.graph.to_canonical_json().unwrap(),
3066            clean_result.graph.to_canonical_json().unwrap()
3067        );
3068    }
3069}