Skip to main content

sim_incremental_core/projection/
builtin.rs

1use std::{collections::BTreeSet, sync::Arc};
2
3use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
4use sim_kernel::{ContentId, Datum, Symbol};
5
6use super::{
7    PackageIdentity, ProjectionError, ProjectionInputs, ProjectionKindRef, ProjectionOutput,
8    ProjectionProvider, ProjectionRegistry,
9};
10
11/// Baseline open projection kinds required for source and release reasoning.
12pub const BASELINE_PROJECTION_KINDS: &[&str] = &[
13    "world/path-set-v1",
14    "world/manifest-dependencies-v1",
15    "world/public-api-v1",
16    "world/exact-command-environment-v1",
17    "world/generated-ownership-v1",
18    "world/package-assembly-v1",
19    "world/git-refs-v1",
20    "world/index-routes-v1",
21    "world/external-release-facts-v1",
22    "no-v3/disclosure-policy-v1",
23];
24
25/// Canonical include/glob/ignore semantics for logical world paths.
26///
27/// Paths use relative `/`-separated logical names. Absolute paths, parent
28/// traversal, backslashes, empty components, and `.` components are refused so
29/// host path spelling cannot enter semantic identity. Includes form a union;
30/// ignores subtract from it.
31#[derive(Clone, Debug, Eq, PartialEq)]
32pub struct PathSelectionRules {
33    includes: Vec<String>,
34    ignores: Vec<String>,
35}
36
37impl PathSelectionRules {
38    /// Checks and canonicalizes an explicit ordered rule set.
39    pub fn new(
40        includes: impl IntoIterator<Item = String>,
41        ignores: impl IntoIterator<Item = String>,
42    ) -> Result<Self, ProjectionError> {
43        let includes = includes.into_iter().collect::<Vec<_>>();
44        let ignores = ignores.into_iter().collect::<Vec<_>>();
45        if includes.is_empty() {
46            return Err(ProjectionError::InvalidPathSelection(
47                "at least one include glob is required".to_owned(),
48            ));
49        }
50        compile_globs(&includes)?;
51        compile_globs(&ignores)?;
52        Ok(Self { includes, ignores })
53    }
54
55    /// Selects exact logical path fact identities in canonical order.
56    pub fn select(
57        &self,
58        paths: impl IntoIterator<Item = String>,
59    ) -> Result<BTreeSet<super::FactId>, ProjectionError> {
60        let includes = compile_globs(&self.includes)?;
61        let ignores = compile_globs(&self.ignores)?;
62        let mut selected = BTreeSet::new();
63        for path in paths {
64            validate_logical_path(&path)?;
65            if includes.is_match(&path) && !ignores.is_match(&path) {
66                selected.insert(super::FactId::new(format!("path/{path}"))?);
67            }
68        }
69        Ok(selected)
70    }
71
72    /// Returns the canonical checked configuration value bound into a digest.
73    #[must_use]
74    pub fn config(&self) -> Datum {
75        Datum::Node {
76            tag: Symbol::qualified("projection", "path-selection-v1"),
77            fields: vec![
78                (
79                    Symbol::new("include"),
80                    Datum::Vector(self.includes.iter().cloned().map(Datum::String).collect()),
81                ),
82                (
83                    Symbol::new("ignore"),
84                    Datum::Vector(self.ignores.iter().cloned().map(Datum::String).collect()),
85                ),
86            ],
87        }
88    }
89}
90
91fn compile_globs(patterns: &[String]) -> Result<GlobSet, ProjectionError> {
92    let mut builder = GlobSetBuilder::new();
93    for pattern in patterns {
94        if pattern.starts_with('/') || pattern.contains('\\') {
95            return Err(ProjectionError::InvalidPathSelection(format!(
96                "glob is not a canonical logical path pattern: {pattern}"
97            )));
98        }
99        let glob = GlobBuilder::new(pattern)
100            .literal_separator(true)
101            .backslash_escape(false)
102            .build()
103            .map_err(|error| ProjectionError::InvalidPathSelection(error.to_string()))?;
104        builder.add(glob);
105    }
106    builder
107        .build()
108        .map_err(|error| ProjectionError::InvalidPathSelection(error.to_string()))
109}
110
111fn validate_logical_path(path: &str) -> Result<(), ProjectionError> {
112    if path.is_empty()
113        || path.starts_with('/')
114        || path.contains('\\')
115        || path
116            .split('/')
117            .any(|part| part.is_empty() || part == "." || part == "..")
118    {
119        return Err(ProjectionError::InvalidPathSelection(format!(
120            "path is not a canonical relative logical path: {path}"
121        )));
122    }
123    Ok(())
124}
125
126/// Native baseline provider that projects all facts selected by its policy.
127///
128/// Each instance owns one open kind and one config Shape. Its output keeps fact
129/// identity beside each semantic value so two differently named inputs cannot
130/// alias even when their values are equal.
131pub struct SelectFactsProvider {
132    kind: ProjectionKindRef,
133    config_shape: ContentId,
134}
135
136impl SelectFactsProvider {
137    /// Constructs one provider instance for an open kind.
138    pub fn new(kind: impl Into<String>, config_shape: ContentId) -> Result<Self, ProjectionError> {
139        Ok(Self {
140            kind: ProjectionKindRef::new(kind)?,
141            config_shape,
142        })
143    }
144}
145
146impl ProjectionProvider for SelectFactsProvider {
147    fn kind(&self) -> &ProjectionKindRef {
148        &self.kind
149    }
150
151    fn config_shape(&self) -> &ContentId {
152        &self.config_shape
153    }
154
155    fn project(
156        &self,
157        inputs: &ProjectionInputs,
158        config: &Datum,
159    ) -> Result<ProjectionOutput, ProjectionError> {
160        let mut dependencies = BTreeSet::new();
161        let facts = inputs
162            .iter()
163            .map(|(id, value)| {
164                dependencies.insert(id.clone());
165                Datum::Node {
166                    tag: Symbol::qualified("projection", "fact-v1"),
167                    fields: vec![
168                        (Symbol::new("id"), Datum::String(id.as_str().to_owned())),
169                        (Symbol::new("value"), value.clone()),
170                    ],
171                }
172            })
173            .collect();
174        Ok(ProjectionOutput {
175            value: Datum::Node {
176                tag: Symbol::qualified("projection", "selected-facts-v1"),
177                fields: vec![
178                    (
179                        Symbol::new("kind"),
180                        Datum::String(self.kind.as_str().to_owned()),
181                    ),
182                    (Symbol::new("config"), config.clone()),
183                    (Symbol::new("facts"), Datum::Vector(facts)),
184                ],
185            },
186            dependencies,
187        })
188    }
189}
190
191/// Installs every baseline kind into an open registry.
192pub fn install_baseline_providers(
193    registry: &mut ProjectionRegistry,
194    config_shape: ContentId,
195    package: PackageIdentity,
196) -> Result<(), ProjectionError> {
197    for kind in BASELINE_PROJECTION_KINDS {
198        registry.register(
199            package.clone(),
200            Arc::new(SelectFactsProvider::new(*kind, config_shape.clone())?),
201        )?;
202    }
203    Ok(())
204}