Skip to main content

omena_lsp_server/
external_sif_loader.rs

1use crate::protocol::{file_uri_to_path, is_style_document_uri, normalize_path};
2use crate::tide::{
3    TideDisownCauseV0, TideFootprintStampV0, TideFootprintV0, TideGateInputsV0, TideInputKindV0,
4    TideLaneConfigV0, TideRepublishDemandV0, TideSifDemandV0,
5};
6use crate::{LspShellState, LspTextDocumentState};
7use omena_query::{
8    OmenaQueryBridgeExternalSifTrustedResolutionV1, OmenaQueryExternalSifInputV0,
9    OmenaQueryExternalSifStorageV0, OmenaQueryExternalSifTrustV1,
10    OmenaQueryStyleResolutionInputsV0, OmenaQueryStyleSourceInputV0,
11    resolve_omena_query_bridge_external_sifs_for_seed_pairs_with_cache_storage_and_trust,
12    resolve_omena_query_bridge_external_sifs_for_seed_pairs_with_trust,
13    resolve_omena_query_bridge_external_sifs_for_style_sources_with_cache_storage_and_trust,
14    resolve_omena_query_bridge_external_sifs_for_style_sources_with_trust,
15};
16use omena_sif::OMENA_SIF_SHARD_VERDICT_DIR_V1;
17use std::{
18    collections::{BTreeMap, BTreeSet},
19    path::{Path, PathBuf},
20};
21
22#[derive(Debug, Clone)]
23pub struct LspExternalSifRefreshDocumentV0 {
24    pub uri: String,
25    pub workspace_folder_uri: Option<String>,
26    pub text: String,
27}
28
29#[derive(Debug, Clone)]
30pub struct LspExternalSifRefreshJobV0 {
31    pub stamp: TideFootprintStampV0,
32    pub generation: u64,
33    pub lockfiles: Vec<PathBuf>,
34    pub documents: Vec<LspExternalSifRefreshDocumentV0>,
35    pub package_manifests: Vec<omena_query::OmenaQueryStylePackageManifestV0>,
36    pub resolution_inputs_by_workspace_uri:
37        std::collections::BTreeMap<String, OmenaQueryStyleResolutionInputsV0>,
38}
39
40#[derive(Debug, Clone, Default)]
41pub struct LspExternalSifRefreshCacheStorageV0 {
42    by_workspace_uri: BTreeMap<String, OmenaQueryExternalSifStorageV0>,
43    by_document_uri: BTreeMap<String, OmenaQueryExternalSifStorageV0>,
44}
45
46#[derive(Debug, Clone)]
47pub struct LspExternalSifRefreshResultV0 {
48    pub stamp: TideFootprintStampV0,
49    pub generation: u64,
50    pub external_sifs: Vec<OmenaQueryExternalSifInputV0>,
51    pub bridge_external_sif_urls: BTreeSet<String>,
52    /// Wire-compatibility sentinel for the retired automatic workspace-lock
53    /// reader. Automatic LSP collection never reads a lock, so this is always
54    /// zero; absence is verified by the product behavior and source census,
55    /// not inferred from a synthetic observation set.
56    pub lock_read_count: usize,
57    pub bridge_generation_count: usize,
58    pub trust_records: Vec<OmenaQueryExternalSifTrustV1>,
59}
60
61/// The SIF job's declared input footprint (rfcs#111 §4.1). DocumentText is
62/// deliberately absent: text edits reach SIF resolution through the
63/// bridge-source delta path, which deposits a NEW demand instead of staling
64/// the in-flight job — an unfootprinted clock would discard in-flight work
65/// on every keystroke (the review BLOCKER).
66pub(crate) const EXTERNAL_SIF_FOOTPRINT: TideFootprintV0 = TideFootprintV0::of(&[
67    TideInputKindV0::DocumentSet,
68    TideInputKindV0::LockfileFingerprint,
69    TideInputKindV0::PackageManifest,
70    TideInputKindV0::ResolutionSettings,
71]);
72
73/// SettleGated lanes flush on frontier passage alone: the courtesy layer is
74/// pinned open, so the aging bound is never consulted.
75pub(crate) const TIDE_SETTLE_LANE_CONFIG: TideLaneConfigV0 = TideLaneConfigV0 {
76    aging_bound_ticks: u64::MAX,
77};
78
79pub(crate) fn refresh_external_sifs_for_state(state: &mut LspShellState) {
80    if state.external_sif_refresh_deferred {
81        crate::loop_trace!("sif-demand reason=state-refresh");
82        let tick = state.tide_tick;
83        state
84            .tide_sif_lane
85            .deposit(TideSifDemandV0::refresh(), tick);
86        return;
87    }
88    refresh_external_sifs_for_state_immediate(state);
89}
90
91fn refresh_external_sifs_for_state_immediate(state: &mut LspShellState) {
92    // Workspace lock bytes are attacker-writable automatic inputs, so the LSP
93    // product path does not read them. Only independently regenerated local
94    // bridge bytes can enter the automatic external-SIF state.
95    let bridge_result = resolve_in_process_external_sifs_for_lsp(state, &BTreeSet::new());
96    state.external_sif_bridge_generation_count = state
97        .external_sif_bridge_generation_count
98        .saturating_add(bridge_result.resolution.generation_count);
99    let external_sifs = bridge_result.resolution.external_sifs;
100
101    let trust_records = external_sif_trust_record_map(bridge_result.trust_records);
102    if state.resolution.external_sifs != external_sifs
103        || state.resolution.external_sif_trust_records != trust_records
104    {
105        state.resolution.external_sifs = external_sifs;
106        state.resolution.external_sif_trust_records = trust_records;
107        invalidate_external_sif_dependents(state);
108    }
109    state.resolution.bridge_external_sif_urls =
110        bridge_result.resolution.bridge_urls.into_iter().collect();
111}
112
113pub(crate) fn refresh_external_sifs_for_bridge_source_delta(
114    state: &mut LspShellState,
115    affected_document_uris: &[String],
116    previous_sources: &[String],
117    next_sources: &[String],
118) {
119    if state.external_sif_refresh_deferred {
120        // Mirror the immediate arm's early return: an unchanged bridge-source
121        // set has nothing to refresh, so it must not mark the deferred job
122        // dirty (previously every no-op admit wave scheduled a full external
123        // SIF re-resolution and raced the in-flight one's revision).
124        let previous_set = previous_sources.iter().collect::<BTreeSet<_>>();
125        let next_set = next_sources.iter().collect::<BTreeSet<_>>();
126        if previous_set == next_set {
127            crate::loop_trace!(
128                "sif-demand SKIPPED reason=bridge-delta-equal len={}",
129                next_set.len()
130            );
131            return;
132        }
133        crate::loop_trace!(
134            "sif-demand reason=bridge-delta prev={} next={}",
135            previous_sources.len(),
136            next_sources.len()
137        );
138        // A genuine bridge-topology change is a corpus-input mutation: it
139        // stales any in-flight SIF job (footprint member) and deposits the
140        // demand whose tide will re-resolve against the new topology.
141        state.tide_ledger.advance(&[TideInputKindV0::DocumentSet]);
142        let affected_file_ids = affected_document_uris
143            .iter()
144            .filter_map(|uri| state.document_file_id(uri))
145            .collect::<Vec<_>>();
146        state.tide_reopen_republish_window(TideDisownCauseV0::for_file_ids(
147            TideInputKindV0::DocumentSet,
148            affected_file_ids,
149        ));
150        let tick = state.tide_tick;
151        state
152            .tide_sif_lane
153            .deposit(TideSifDemandV0::refresh(), tick);
154        return;
155    }
156    let previous_sources = previous_sources.iter().cloned().collect::<BTreeSet<_>>();
157    let next_sources = next_sources.iter().cloned().collect::<BTreeSet<_>>();
158    if previous_sources == next_sources {
159        return;
160    }
161    if previous_sources
162        .iter()
163        .chain(next_sources.iter())
164        .any(|source| !source.starts_with("file://"))
165    {
166        refresh_external_sifs_for_state(state);
167        return;
168    }
169
170    let active_bridge_sources = active_bridge_sources_from_documents(state);
171    let mut changed = false;
172    let mut remove_urls = BTreeSet::new();
173    for source in previous_sources.difference(&next_sources) {
174        if active_bridge_sources.contains(source) {
175            continue;
176        }
177        collect_bridge_sif_urls_for_sources(
178            state,
179            std::iter::once(source.as_str()),
180            &BTreeSet::new(),
181        )
182        .into_iter()
183        .for_each(|url| {
184            remove_urls.insert(url);
185        });
186    }
187
188    if !remove_urls.is_empty() {
189        let before_len = state.resolution.external_sifs.len();
190        state.resolution.external_sifs.retain(|input| {
191            !state
192                .resolution
193                .bridge_external_sif_urls
194                .contains(input.canonical_url.as_str())
195                || !remove_urls.contains(input.canonical_url.as_str())
196        });
197        state
198            .resolution
199            .bridge_external_sif_urls
200            .retain(|url| !remove_urls.contains(url.as_str()));
201        changed |= before_len != state.resolution.external_sifs.len();
202        let live_canonical_urls = state
203            .resolution
204            .external_sifs
205            .iter()
206            .map(|input| input.sif.canonical_url.clone())
207            .collect::<BTreeSet<_>>();
208        let before_trust_len = state.resolution.external_sif_trust_records.len();
209        state
210            .resolution
211            .external_sif_trust_records
212            .retain(|url, _| live_canonical_urls.contains(url));
213        changed |= before_trust_len != state.resolution.external_sif_trust_records.len();
214    }
215
216    let mut covered = covered_external_sif_urls(state.resolution.external_sifs.as_slice());
217    for source in next_sources.difference(&previous_sources) {
218        if state
219            .resolution
220            .bridge_external_sif_urls
221            .contains(source.as_str())
222        {
223            continue;
224        }
225        let bridge_result = resolve_bridge_external_sifs_for_sources(
226            state,
227            std::iter::once(source.as_str()),
228            &covered,
229        );
230        let before_len = state.resolution.external_sifs.len();
231        extend_unique_external_sifs(
232            &mut state.resolution.external_sifs,
233            &mut covered,
234            bridge_result.resolution.external_sifs,
235        );
236        state
237            .resolution
238            .bridge_external_sif_urls
239            .extend(bridge_result.resolution.bridge_urls);
240        changed |= before_len != state.resolution.external_sifs.len();
241        for trust_record in bridge_result.trust_records {
242            changed |= state
243                .resolution
244                .external_sif_trust_records
245                .insert(trust_record.canonical_url.clone(), trust_record.clone())
246                .as_ref()
247                != Some(&trust_record);
248        }
249        state.external_sif_bridge_generation_count = state
250            .external_sif_bridge_generation_count
251            .saturating_add(bridge_result.resolution.generation_count);
252    }
253
254    if changed {
255        invalidate_external_sif_dependents(state);
256    }
257}
258
259pub(crate) fn bridge_sources_for_style_uris(
260    state: &LspShellState,
261    style_uris: &[String],
262) -> Vec<String> {
263    let mut sources = BTreeSet::new();
264    for uri in style_uris {
265        let Some(document) = state.document(uri.as_str()) else {
266            continue;
267        };
268        collect_bridge_sources_from_style_document(document, &mut sources);
269    }
270    sources.into_iter().collect()
271}
272
273pub fn enable_deferred_external_sif_refresh(state: &mut LspShellState) {
274    state.external_sif_refresh_deferred = true;
275    let tick = state.tide_tick;
276    state
277        .tide_sif_lane
278        .deposit(TideSifDemandV0::refresh(), tick);
279}
280
281pub fn prepare_deferred_external_sif_refresh_job(
282    state: &mut LspShellState,
283) -> Option<LspExternalSifRefreshJobV0> {
284    if !state.external_sif_refresh_deferred {
285        return None;
286    }
287    // Settle gate: the correctness layer is the index frontier — no flush
288    // while an index chain still has pending files. The lane enforces one
289    // in-flight tide, so a second prepare during a running job is a no-op.
290    let inputs = TideGateInputsV0 {
291        frontier_passed: state.workspace_index_pending_file_count == 0,
292        idle: true,
293    };
294    let flush = state
295        .tide_sif_lane
296        .try_flush(inputs, state.tide_tick, &TIDE_SETTLE_LANE_CONFIG)?;
297    crate::loop_trace!(
298        "sif-job-prepared gen={} epoch={} docs={}",
299        flush.generation,
300        state.tide_ledger.epoch(),
301        state
302            .documents
303            .values()
304            .filter(|d| is_style_document_uri(d.uri.as_str()))
305            .count()
306    );
307    Some(LspExternalSifRefreshJobV0 {
308        stamp: state.tide_ledger.stamp(EXTERNAL_SIF_FOOTPRINT),
309        generation: flush.generation,
310        lockfiles: workspace_lockfiles(state),
311        documents: state
312            .documents
313            .values()
314            .map(AsRef::as_ref)
315            .filter(|document| is_style_document_uri(document.uri.as_str()))
316            .map(|document| LspExternalSifRefreshDocumentV0 {
317                uri: document.uri.clone(),
318                workspace_folder_uri: document.workspace_folder_uri.clone(),
319                text: document.text.clone(),
320            })
321            .collect(),
322        package_manifests: state.resolution.package_manifests.clone(),
323        resolution_inputs_by_workspace_uri: state
324            .resolution
325            .workspace_style_resolution_inputs
326            .clone(),
327    })
328}
329
330pub fn prepare_deferred_external_sif_refresh_cache_storage(
331    state: &LspShellState,
332) -> LspExternalSifRefreshCacheStorageV0 {
333    let mut by_workspace_uri = BTreeMap::new();
334    for folder in state.workspace_runtime_registry.folder_snapshots() {
335        if let Some(storage) =
336            bridge_cache_storage_for_workspace_uri(state, Some(folder.uri.as_str()))
337        {
338            by_workspace_uri.insert(folder.uri, storage);
339        }
340    }
341    let by_document_uri = state
342        .documents
343        .values()
344        .map(AsRef::as_ref)
345        .filter(|document| {
346            document.workspace_folder_uri.is_none() && is_style_document_uri(document.uri.as_str())
347        })
348        .filter_map(|document| {
349            bridge_cache_storage_for_document(state, None, document.uri.as_str())
350                .map(|storage| (document.uri.clone(), storage))
351        })
352        .collect();
353    LspExternalSifRefreshCacheStorageV0 {
354        by_workspace_uri,
355        by_document_uri,
356    }
357}
358
359pub fn collect_deferred_external_sif_refresh(
360    job: LspExternalSifRefreshJobV0,
361) -> LspExternalSifRefreshResultV0 {
362    collect_deferred_external_sif_refresh_with_cache_storage(
363        job,
364        LspExternalSifRefreshCacheStorageV0::default(),
365    )
366}
367
368pub fn collect_deferred_external_sif_refresh_with_cache_storage(
369    job: LspExternalSifRefreshJobV0,
370    cache_storage: LspExternalSifRefreshCacheStorageV0,
371) -> LspExternalSifRefreshResultV0 {
372    let LspExternalSifRefreshJobV0 {
373        stamp,
374        generation,
375        lockfiles: excluded_lockfiles,
376        documents,
377        package_manifests,
378        resolution_inputs_by_workspace_uri,
379    } = job;
380    drop(excluded_lockfiles);
381    let bridge_result = resolve_external_sifs_for_refresh_documents(
382        documents.as_slice(),
383        &[],
384        package_manifests.as_slice(),
385        &resolution_inputs_by_workspace_uri,
386        Some(&cache_storage),
387    );
388    let external_sifs = bridge_result.resolution.external_sifs;
389
390    LspExternalSifRefreshResultV0 {
391        stamp,
392        generation,
393        external_sifs,
394        bridge_external_sif_urls: bridge_result.resolution.bridge_urls.into_iter().collect(),
395        lock_read_count: 0,
396        bridge_generation_count: bridge_result.resolution.generation_count,
397        trust_records: bridge_result.trust_records,
398    }
399}
400
401pub fn apply_deferred_external_sif_refresh_result(
402    state: &mut LspShellState,
403    result: LspExternalSifRefreshResultV0,
404) -> bool {
405    if !state.tide_ledger.is_current(&result.stamp) {
406        crate::loop_trace!(
407            "sif-apply DISCARDED gen={} stamp_epoch={} ledger_epoch={}",
408            result.generation,
409            result.stamp.epoch,
410            state.tide_ledger.epoch()
411        );
412        // The staling mutation also deposited a fresh demand (every advance
413        // site deposits), so completing the disowned tide re-arms the gate.
414        state.tide_sif_lane.tide_completed(result.generation);
415        return false;
416    }
417    state.external_sif_bridge_generation_count = state
418        .external_sif_bridge_generation_count
419        .saturating_add(result.bridge_generation_count);
420    let trust_records = external_sif_trust_record_map(result.trust_records);
421    let changed = state.resolution.external_sifs != result.external_sifs
422        || state.resolution.external_sif_trust_records != trust_records;
423    crate::loop_trace!(
424        "sif-apply gen={} changed={} sifs {}->{}",
425        result.generation,
426        changed,
427        state.resolution.external_sifs.len(),
428        result.external_sifs.len()
429    );
430    if changed {
431        // Cone seeding (rfcs#111 demand lattice): the republish owed by a
432        // SIF delta is the set of files that import a CHANGED fact, not the
433        // workspace. Computed BEFORE the swap so the old set is diffable.
434        let demand =
435            republish_demand_for_external_sif_delta(state, result.external_sifs.as_slice());
436        state.resolution.external_sifs = result.external_sifs;
437        state.resolution.external_sif_trust_records = trust_records;
438        invalidate_external_sif_dependents(state);
439        // Output cutoff (rfcs#111 §4.1): only a CHANGED SIF set owes the
440        // workspace republish; an Eq result blocks downstream entirely.
441        state.tide_reopen_republish_window(TideDisownCauseV0::for_republish_demand(
442            TideInputKindV0::DocumentSet,
443            demand.clone(),
444        ));
445        let tick = state.tide_tick;
446        state.tide_republish_lane.deposit(demand, tick);
447    }
448    state.resolution.bridge_external_sif_urls = result.bridge_external_sif_urls;
449    state.tide_sif_lane.tide_completed(result.generation);
450    changed
451}
452
453/// The republish demand a SIF delta deposits: `Cone(importers of every
454/// changed url)` when the loop's reverse-dependency index can attribute
455/// EVERY changed fact, `All` otherwise — a cold start has no index yet
456/// (everything owes its first publish anyway), and an unattributable url
457/// must widen rather than guess. Seeds are direct importers; the flush
458/// takes their reverse closure against the then-current graph.
459#[cfg(feature = "salsa-style-diagnostics")]
460pub(crate) fn republish_demand_for_external_sif_delta(
461    state: &LspShellState,
462    next_external_sifs: &[OmenaQueryExternalSifInputV0],
463) -> TideRepublishDemandV0 {
464    let previous: BTreeMap<&str, &OmenaQueryExternalSifInputV0> = state
465        .resolution
466        .external_sifs
467        .iter()
468        .map(|input| (input.canonical_url.as_str(), input))
469        .collect();
470    let next: BTreeMap<&str, &OmenaQueryExternalSifInputV0> = next_external_sifs
471        .iter()
472        .map(|input| (input.canonical_url.as_str(), input))
473        .collect();
474    let mut changed_urls = BTreeSet::new();
475    for (url, input) in &next {
476        if previous.get(url).is_none_or(|prev| prev != input) {
477            changed_urls.insert(*url);
478        }
479    }
480    for url in previous.keys() {
481        if !next.contains_key(url) {
482            changed_urls.insert(*url);
483        }
484    }
485    if changed_urls.is_empty() {
486        return TideRepublishDemandV0::None;
487    }
488    let memo_slot = state.reverse_dependency_index_memo.borrow();
489    let Some(memo) = memo_slot.as_ref() else {
490        crate::loop_trace!(
491            "republish-demand all: {} changed sif urls, no reverse index",
492            changed_urls.len()
493        );
494        return TideRepublishDemandV0::All;
495    };
496    // Freshness gate: a memo that predates the latest corpus-shaping input
497    // marks may hold a rev-set that is PRESENT but stale (a just-added
498    // importer missing from it) — presence alone cannot widen, so the epoch
499    // comparison does. Widen, never guess.
500    let corpus_input_mark = state
501        .tide_ledger
502        .mark(TideInputKindV0::DocumentText)
503        .max(state.tide_ledger.mark(TideInputKindV0::DocumentSet));
504    if memo.ledger_epoch < corpus_input_mark {
505        crate::loop_trace!(
506            "republish-demand all: reverse index stale (memo epoch {} < corpus mark {})",
507            memo.ledger_epoch,
508            corpus_input_mark
509        );
510        return TideRepublishDemandV0::All;
511    }
512    let mut seeds: BTreeSet<crate::LspFileId> = BTreeSet::new();
513    for url in &changed_urls {
514        // A fact can appear as an edge target under its alias key or its
515        // resolved canonical url; consult both, from whichever side of the
516        // delta knows the entry.
517        let resolved_alias = next
518            .get(url)
519            .or_else(|| previous.get(url))
520            .map(|input| input.sif.canonical_url.as_str());
521        let dependents = memo
522            .index
523            .rev
524            .get(*url)
525            .or_else(|| resolved_alias.and_then(|alias| memo.index.rev.get(alias)));
526        let Some(dependents) = dependents else {
527            crate::loop_trace!("republish-demand all: unattributed sif url {url}");
528            return TideRepublishDemandV0::All;
529        };
530        for dependent in dependents {
531            let Some(file_id) = state.document_file_id(dependent) else {
532                crate::loop_trace!(
533                    "republish-demand all: dependent has no admitted file id {dependent}"
534                );
535                return TideRepublishDemandV0::All;
536            };
537            seeds.insert(file_id);
538        }
539    }
540    crate::loop_trace!(
541        "republish-demand cone seeds={} changed_urls={}",
542        seeds.len(),
543        changed_urls.len()
544    );
545    TideRepublishDemandV0::cone(seeds)
546}
547
548#[cfg(not(feature = "salsa-style-diagnostics"))]
549pub(crate) fn republish_demand_for_external_sif_delta(
550    _state: &LspShellState,
551    _next_external_sifs: &[OmenaQueryExternalSifInputV0],
552) -> TideRepublishDemandV0 {
553    TideRepublishDemandV0::All
554}
555
556fn workspace_lockfiles(state: &LspShellState) -> Vec<PathBuf> {
557    let mut lockfiles = BTreeSet::new();
558    for folder in state.workspace_runtime_registry.folder_snapshots() {
559        let Some(root) = file_uri_to_path(folder.uri.as_str()).map(normalize_path) else {
560            continue;
561        };
562        if let Some(lockfile) = discover_omena_lockfile_for_workspace_root(root.as_path()) {
563            lockfiles.insert(lockfile);
564        }
565    }
566    lockfiles.into_iter().collect()
567}
568
569fn discover_omena_lockfile_for_workspace_root(root: &Path) -> Option<PathBuf> {
570    let mut current = Some(root);
571    while let Some(directory) = current {
572        let candidate = directory.join("omena.lock");
573        if candidate.exists() {
574            return Some(normalize_path(candidate));
575        }
576        current = directory.parent();
577    }
578    None
579}
580
581fn extend_unique_external_sifs(
582    output: &mut Vec<OmenaQueryExternalSifInputV0>,
583    covered: &mut BTreeSet<String>,
584    candidates: Vec<OmenaQueryExternalSifInputV0>,
585) {
586    for candidate in candidates {
587        if covered.insert(candidate.canonical_url.clone()) {
588            covered.insert(candidate.sif.canonical_url.clone());
589            output.push(candidate);
590        }
591    }
592}
593
594pub(crate) fn external_sif_trust_record_map(
595    records: Vec<OmenaQueryExternalSifTrustV1>,
596) -> BTreeMap<String, OmenaQueryExternalSifTrustV1> {
597    records
598        .into_iter()
599        .map(|record| (record.canonical_url.clone(), record))
600        .collect()
601}
602
603fn deduplicate_external_sif_trust_records(records: &mut Vec<OmenaQueryExternalSifTrustV1>) {
604    *records = external_sif_trust_record_map(std::mem::take(records))
605        .into_values()
606        .collect();
607}
608
609fn resolve_in_process_external_sifs_for_lsp(
610    state: &LspShellState,
611    existing_covered: &BTreeSet<String>,
612) -> OmenaQueryBridgeExternalSifTrustedResolutionV1 {
613    let mut existing_inputs = state
614        .resolution
615        .external_sifs
616        .iter()
617        .filter(|input| {
618            existing_covered.contains(input.canonical_url.as_str())
619                || existing_covered.contains(input.sif.canonical_url.as_str())
620        })
621        .cloned()
622        .collect::<Vec<_>>();
623    let mut combined = OmenaQueryBridgeExternalSifTrustedResolutionV1::default();
624    let mut bridge_urls = BTreeSet::new();
625
626    for document in state.documents.values().map(AsRef::as_ref) {
627        if !is_style_document_uri(document.uri.as_str()) {
628            continue;
629        }
630        let source = OmenaQueryStyleSourceInputV0 {
631            style_path: document.uri.clone(),
632            style_source: document.text.clone(),
633        };
634        let resolution_inputs =
635            resolution_inputs_for_document(state, document.workspace_folder_uri.as_deref());
636        let cache_storage = bridge_cache_storage_for_document(
637            state,
638            document.workspace_folder_uri.as_deref(),
639            document.uri.as_str(),
640        );
641        let result = if let Some(cache_storage) = cache_storage.as_ref() {
642            resolve_omena_query_bridge_external_sifs_for_style_sources_with_cache_storage_and_trust(
643                std::slice::from_ref(&source),
644                existing_inputs.as_slice(),
645                &resolution_inputs,
646                cache_storage,
647            )
648        } else {
649            resolve_omena_query_bridge_external_sifs_for_style_sources_with_trust(
650                std::slice::from_ref(&source),
651                existing_inputs.as_slice(),
652                &resolution_inputs,
653            )
654        };
655        combined.resolution.generation_count = combined
656            .resolution
657            .generation_count
658            .saturating_add(result.resolution.generation_count);
659        bridge_urls.extend(result.resolution.bridge_urls);
660        combined.trust_records.extend(result.trust_records);
661        for external_sif in result.resolution.external_sifs {
662            existing_inputs.push(external_sif.clone());
663            combined.resolution.external_sifs.push(external_sif);
664        }
665    }
666
667    combined.resolution.bridge_urls = bridge_urls.into_iter().collect();
668    deduplicate_external_sif_trust_records(&mut combined.trust_records);
669    combined
670}
671
672fn resolve_external_sifs_for_refresh_documents(
673    documents: &[LspExternalSifRefreshDocumentV0],
674    existing_external_sifs: &[OmenaQueryExternalSifInputV0],
675    package_manifests: &[omena_query::OmenaQueryStylePackageManifestV0],
676    resolution_inputs_by_workspace_uri: &std::collections::BTreeMap<
677        String,
678        OmenaQueryStyleResolutionInputsV0,
679    >,
680    cache_storage: Option<&LspExternalSifRefreshCacheStorageV0>,
681) -> OmenaQueryBridgeExternalSifTrustedResolutionV1 {
682    let mut existing_inputs = existing_external_sifs.to_vec();
683    let mut combined = OmenaQueryBridgeExternalSifTrustedResolutionV1::default();
684    let mut bridge_urls = BTreeSet::new();
685
686    for document in documents {
687        let source = OmenaQueryStyleSourceInputV0 {
688            style_path: document.uri.clone(),
689            style_source: document.text.clone(),
690        };
691        let resolution_inputs = document
692            .workspace_folder_uri
693            .as_deref()
694            .and_then(|uri| resolution_inputs_by_workspace_uri.get(uri))
695            .cloned()
696            .unwrap_or_else(|| OmenaQueryStyleResolutionInputsV0 {
697                package_manifests: package_manifests.to_vec(),
698                ..OmenaQueryStyleResolutionInputsV0::default()
699            });
700        let document_cache_storage = cache_storage.and_then(|storage| {
701            if let Some(workspace_uri) = document.workspace_folder_uri.as_ref() {
702                storage.by_workspace_uri.get(workspace_uri)
703            } else {
704                storage.by_document_uri.get(document.uri.as_str())
705            }
706        });
707        let result = if let Some(document_cache_storage) = document_cache_storage {
708            resolve_omena_query_bridge_external_sifs_for_style_sources_with_cache_storage_and_trust(
709                std::slice::from_ref(&source),
710                existing_inputs.as_slice(),
711                &resolution_inputs,
712                document_cache_storage,
713            )
714        } else {
715            resolve_omena_query_bridge_external_sifs_for_style_sources_with_trust(
716                std::slice::from_ref(&source),
717                existing_inputs.as_slice(),
718                &resolution_inputs,
719            )
720        };
721        combined.resolution.generation_count = combined
722            .resolution
723            .generation_count
724            .saturating_add(result.resolution.generation_count);
725        bridge_urls.extend(result.resolution.bridge_urls);
726        combined.trust_records.extend(result.trust_records);
727        for external_sif in result.resolution.external_sifs {
728            existing_inputs.push(external_sif.clone());
729            combined.resolution.external_sifs.push(external_sif);
730        }
731    }
732
733    combined.resolution.bridge_urls = bridge_urls.into_iter().collect();
734    deduplicate_external_sif_trust_records(&mut combined.trust_records);
735    combined
736}
737
738fn resolve_bridge_external_sifs_for_sources<'a>(
739    state: &LspShellState,
740    sources: impl Iterator<Item = &'a str>,
741    existing_covered: &BTreeSet<String>,
742) -> OmenaQueryBridgeExternalSifTrustedResolutionV1 {
743    let mut combined = OmenaQueryBridgeExternalSifTrustedResolutionV1::default();
744    let mut covered = existing_covered.clone();
745    let mut bridge_urls = BTreeSet::new();
746    for source in sources
747        .filter(|source| source.starts_with("file://") && !existing_covered.contains(*source))
748    {
749        let owner = state.workspace_runtime_registry.resolve_owner_uri(source);
750        let cache_storage = bridge_cache_storage_for_document(state, owner.as_deref(), source);
751        let result = if let Some(cache_storage) = cache_storage.as_ref() {
752            resolve_omena_query_bridge_external_sifs_for_seed_pairs_with_cache_storage_and_trust(
753                std::iter::once((source.to_string(), source.to_string())),
754                &[],
755                &OmenaQueryStyleResolutionInputsV0::default(),
756                cache_storage,
757            )
758        } else {
759            resolve_omena_query_bridge_external_sifs_for_seed_pairs_with_trust(
760                std::iter::once((source.to_string(), source.to_string())),
761                &[],
762                &OmenaQueryStyleResolutionInputsV0::default(),
763            )
764        };
765        combined.resolution.generation_count = combined
766            .resolution
767            .generation_count
768            .saturating_add(result.resolution.generation_count);
769        bridge_urls.extend(result.resolution.bridge_urls);
770        combined.trust_records.extend(result.trust_records);
771        extend_unique_external_sifs(
772            &mut combined.resolution.external_sifs,
773            &mut covered,
774            result.resolution.external_sifs,
775        );
776    }
777    combined.resolution.bridge_urls = bridge_urls.into_iter().collect();
778    deduplicate_external_sif_trust_records(&mut combined.trust_records);
779    combined
780}
781
782fn collect_bridge_sif_urls_for_sources<'a>(
783    state: &LspShellState,
784    sources: impl Iterator<Item = &'a str>,
785    existing_covered: &BTreeSet<String>,
786) -> BTreeSet<String> {
787    resolve_bridge_external_sifs_for_sources(state, sources, existing_covered)
788        .resolution
789        .bridge_urls
790        .into_iter()
791        .collect()
792}
793
794fn bridge_cache_storage_for_workspace_uri(
795    state: &LspShellState,
796    workspace_folder_uri: Option<&str>,
797) -> Option<OmenaQueryExternalSifStorageV0> {
798    let workspace_folder_uri = workspace_folder_uri?;
799    let workspace_root = file_uri_to_path(workspace_folder_uri)?;
800    let workspace_cache_root = crate::cache_root::resolved_bridge_workspace_cache_root(
801        &state.resolution.cache_storage,
802        workspace_folder_uri,
803        workspace_root.as_path(),
804    )?;
805    Some(
806        OmenaQueryExternalSifStorageV0::from_workspace_cache_root_and_identity(
807            workspace_cache_root,
808            workspace_folder_uri,
809        )
810        .with_recorded_verdict_dir(
811            workspace_root
812                .join(".cache")
813                .join("omena")
814                .join(OMENA_SIF_SHARD_VERDICT_DIR_V1),
815        ),
816    )
817}
818
819pub(crate) fn bridge_cache_storage_for_document(
820    state: &LspShellState,
821    workspace_folder_uri: Option<&str>,
822    document_uri: &str,
823) -> Option<OmenaQueryExternalSifStorageV0> {
824    if let Some(workspace_folder_uri) = workspace_folder_uri {
825        return bridge_cache_storage_for_workspace_uri(state, Some(workspace_folder_uri));
826    }
827    let document_path = file_uri_to_path(document_uri)?;
828    let document_root = document_path.parent()?;
829    let workspace_identity = document_root.to_string_lossy();
830    let workspace_cache_root = crate::cache_root::resolved_bridge_workspace_cache_root(
831        &state.resolution.cache_storage,
832        workspace_identity.as_ref(),
833        document_root,
834    )?;
835    Some(
836        OmenaQueryExternalSifStorageV0::from_workspace_cache_root_and_identity(
837            workspace_cache_root,
838            workspace_identity,
839        )
840        .with_recorded_verdict_dir(
841            document_root
842                .join(".cache")
843                .join("omena")
844                .join(OMENA_SIF_SHARD_VERDICT_DIR_V1),
845        ),
846    )
847}
848
849fn resolution_inputs_for_document(
850    state: &LspShellState,
851    workspace_folder_uri: Option<&str>,
852) -> OmenaQueryStyleResolutionInputsV0 {
853    workspace_folder_uri
854        .and_then(|uri| {
855            state
856                .resolution
857                .workspace_style_resolution_inputs
858                .get(uri)
859                .cloned()
860        })
861        .unwrap_or_else(|| OmenaQueryStyleResolutionInputsV0 {
862            package_manifests: state.resolution.package_manifests.clone(),
863            ..OmenaQueryStyleResolutionInputsV0::default()
864        })
865}
866
867fn active_bridge_sources_from_documents(state: &LspShellState) -> BTreeSet<String> {
868    let mut sources = BTreeSet::new();
869    for document in state.documents.values() {
870        collect_bridge_sources_from_style_document(document, &mut sources);
871    }
872    sources
873}
874
875fn collect_bridge_sources_from_style_document(
876    document: &LspTextDocumentState,
877    sources: &mut BTreeSet<String>,
878) {
879    let Some(summary) = document.style_summary.as_ref() else {
880        return;
881    };
882    let edge_sources = summary
883        .sass_module_use_sources
884        .iter()
885        .map(String::as_str)
886        .chain(
887            summary
888                .sass_module_forward_sources
889                .iter()
890                .map(String::as_str),
891        );
892    for edge_source in edge_sources {
893        if edge_source.starts_with("file://") {
894            sources.insert(edge_source.to_string());
895        }
896    }
897}
898
899fn covered_external_sif_urls(inputs: &[OmenaQueryExternalSifInputV0]) -> BTreeSet<String> {
900    inputs
901        .iter()
902        .flat_map(|input| [input.canonical_url.clone(), input.sif.canonical_url.clone()])
903        .collect()
904}
905
906fn invalidate_external_sif_dependents(state: &mut LspShellState) {
907    *state.workspace_occurrence_index_memo_lock() = None;
908    if let Ok(mut memo) = state.cascade_narrowing_substrate_memo.lock() {
909        *memo = None;
910    }
911}