Skip to main content

pi/core/resources/
source_info.rs

1//! Source provenance for discovered product resources.
2//!
3//! Port of `.references/pi/packages/coding-agent/src/core/source-info.ts`.
4
5use super::discovery::PathMetadata;
6
7/// Where a resource sits relative to the project boundary.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum SourceScope {
10    /// Global agent directory (`~/.pi/agent` or `PI_CODING_AGENT_DIR`).
11    User,
12    /// Project-local (typically under `{cwd}/.pi`).
13    Project,
14    /// Temporary/CLI or synthetic paths.
15    Temporary,
16}
17
18impl SourceScope {
19    /// Wire discriminant used by diagnostics and source info.
20    #[must_use]
21    pub const fn as_str(self) -> &'static str {
22        match self {
23            Self::User => "user",
24            Self::Project => "project",
25            Self::Temporary => "temporary",
26        }
27    }
28}
29
30/// Whether a path came from a package or top-level settings/auto discovery.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum SourceOrigin {
33    /// Installed or local package root.
34    Package,
35    /// Settings array, auto-discovery, or CLI temporary path.
36    TopLevel,
37}
38
39impl SourceOrigin {
40    /// Wire discriminant.
41    #[must_use]
42    pub const fn as_str(self) -> &'static str {
43        match self {
44            Self::Package => "package",
45            Self::TopLevel => "top-level",
46        }
47    }
48}
49
50/// Provenance attached to a loaded skill, prompt, theme, or extension path.
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct SourceInfo {
53    /// Absolute (or synthetic) path of the resource.
54    pub path: String,
55    /// Source label (`local`, `auto`, `cli`, package id, …).
56    pub source: String,
57    /// Scope relative to the project boundary.
58    pub scope: SourceScope,
59    /// Package vs top-level origin.
60    pub origin: SourceOrigin,
61    /// Optional base directory used for relative resolution.
62    pub base_dir: Option<String>,
63}
64
65/// Build [`SourceInfo`] from path metadata produced by discovery.
66#[must_use]
67pub fn create_source_info(path: impl Into<String>, metadata: &PathMetadata) -> SourceInfo {
68    SourceInfo {
69        path: path.into(),
70        source: metadata.source.clone(),
71        scope: metadata.scope,
72        origin: metadata.origin,
73        base_dir: metadata.base_dir.clone(),
74    }
75}
76
77/// Options for synthetic source info when no discovery metadata exists.
78#[derive(Clone, Debug)]
79pub struct SyntheticSourceInfoOptions {
80    /// Source label.
81    pub source: String,
82    /// Scope (defaults to temporary).
83    pub scope: Option<SourceScope>,
84    /// Origin (defaults to top-level).
85    pub origin: Option<SourceOrigin>,
86    /// Optional base directory.
87    pub base_dir: Option<String>,
88}
89
90/// Build source info with temporary/top-level defaults.
91#[must_use]
92pub fn create_synthetic_source_info(
93    path: impl Into<String>,
94    options: SyntheticSourceInfoOptions,
95) -> SourceInfo {
96    SourceInfo {
97        path: path.into(),
98        source: options.source,
99        scope: options.scope.unwrap_or(SourceScope::Temporary),
100        origin: options.origin.unwrap_or(SourceOrigin::TopLevel),
101        base_dir: options.base_dir,
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::core::resources::discovery::PathMetadata;
109
110    #[test]
111    fn create_source_info_copies_metadata() {
112        let metadata = PathMetadata {
113            source: "auto".into(),
114            scope: SourceScope::Project,
115            origin: SourceOrigin::TopLevel,
116            base_dir: Some("/tmp/.pi".into()),
117        };
118        let info = create_source_info("/tmp/.pi/skills/a.md", &metadata);
119        assert_eq!(info.path, "/tmp/.pi/skills/a.md");
120        assert_eq!(info.source, "auto");
121        assert_eq!(info.scope, SourceScope::Project);
122        assert_eq!(info.origin, SourceOrigin::TopLevel);
123        assert_eq!(info.base_dir.as_deref(), Some("/tmp/.pi"));
124    }
125
126    #[test]
127    fn synthetic_defaults_temporary_top_level() {
128        let info = create_synthetic_source_info(
129            "/x",
130            SyntheticSourceInfoOptions {
131                source: "local".into(),
132                scope: None,
133                origin: None,
134                base_dir: None,
135            },
136        );
137        assert_eq!(info.scope, SourceScope::Temporary);
138        assert_eq!(info.origin, SourceOrigin::TopLevel);
139        assert_eq!(info.source, "local");
140    }
141}