use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub const PACKAGE_MANIFEST_FILE: &str = "inauguration.package";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageManifest {
pub name: String,
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entry: Option<String>,
pub targets: BTreeMap<String, bool>,
pub dependencies: BTreeMap<String, PackageDependency>,
pub capabilities: Vec<String>,
pub extensions: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct PackageDependency {
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rev: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub targets: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub build: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub install_path: Option<String>,
}
impl PackageDependency {
pub fn resolved_source_path(&self) -> Option<PathBuf> {
if let Some(source) = self.source.as_deref().filter(|value| !value.is_empty())
&& (self.kind.as_deref() == Some("path")
|| self.version.strip_prefix("path:").is_some())
{
return Some(PathBuf::from(source));
}
self.version.strip_prefix("path:").map(PathBuf::from)
}
pub fn resolved_install_path(&self, package_root: &Path) -> Option<PathBuf> {
self.install_path
.as_ref()
.map(|path| package_root.join(path))
.filter(|path| path.is_dir())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageCompileContext {
pub name: String,
pub entry: Option<String>,
pub dependency_search_paths: Vec<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageRoot {
pub root: PathBuf,
pub manifest_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageTargetSelection {
pub requested: Vec<String>,
pub enabled: Vec<String>,
pub disabled: Vec<String>,
pub unknown: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityPolicyValidation {
pub valid: bool,
pub allowed: Vec<String>,
pub required: Vec<String>,
pub missing: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageReport {
pub root: PathBuf,
pub manifest_path: PathBuf,
pub manifest: PackageManifest,
pub target_selection: PackageTargetSelection,
pub capability_policy: CapabilityPolicyValidation,
pub graph: PackageGraphReport,
pub source_identity: Option<PackageSourceIdentity>,
pub semantic_imports: Vec<PackageSemanticImport>,
pub semantic_bindings: Vec<PackageSemanticBinding>,
pub symbol_index: Vec<PackageSymbolIndexEntry>,
pub diagnostics: Vec<PackageDiagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageGraphReport {
pub package_id: String,
pub nodes: Vec<PackageGraphNode>,
pub edges: Vec<PackageGraphEdge>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageGraphNode {
pub id: String,
pub kind: String,
pub label: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageGraphEdge {
pub from: String,
pub to: String,
pub kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageSourceIdentity {
pub package: Option<String>,
pub module: Option<String>,
pub manifest_name: Option<String>,
pub status: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageSemanticImport {
pub import: String,
pub dependency: Option<String>,
pub status: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageSemanticBinding {
pub import: String,
pub alias: String,
pub dependency: Option<String>,
pub status: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageSymbolIndexEntry {
pub id: String,
pub kind: String,
pub name: String,
pub source_import: String,
pub dependency: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageDiagnostic {
pub code: String,
pub severity: String,
pub import: String,
pub reason: String,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Section {
Targets,
Dependencies,
Capabilities,
Extensions,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DependencySubsection {
Targets,
Capabilities,
Build,
}
pub fn load_package_manifest(path: &Path) -> Result<PackageManifest, String> {
let manifest_path = if path.is_dir() {
path.join(PACKAGE_MANIFEST_FILE)
} else {
path.to_path_buf()
};
let source = fs::read_to_string(&manifest_path)
.map_err(|err| format!("failed to read {}: {err}", manifest_path.display()))?;
parse_package_manifest(&source)
}
pub fn write_package_manifest(path: &Path, manifest: &PackageManifest) -> Result<(), String> {
let source = format_package_manifest(manifest);
fs::write(path, source).map_err(|err| format!("write {}: {err}", path.display()))
}
pub fn format_package_manifest(manifest: &PackageManifest) -> String {
let mut out = String::new();
out.push_str(&format!("name: {}\n", manifest.name));
out.push_str(&format!("version: {}\n", manifest.version));
if let Some(entry) = &manifest.entry {
out.push_str(&format!("entry: {entry}\n"));
}
if !manifest.targets.is_empty() {
out.push_str("targets:\n");
for (target, enabled) in &manifest.targets {
out.push_str(&format!(" {target}: {enabled}\n"));
}
}
if !manifest.dependencies.is_empty() {
out.push_str("dependencies:\n");
for (key, dependency) in &manifest.dependencies {
out.push_str(&format!(" {key}:\n"));
out.push_str(&format!(" version: {}\n", dependency.version));
if let Some(kind) = &dependency.kind {
out.push_str(&format!(" kind: {kind}\n"));
}
if let Some(source) = &dependency.source {
out.push_str(&format!(" source: {source}\n"));
}
if let Some(rev) = &dependency.rev {
out.push_str(&format!(" rev: {rev}\n"));
}
if let Some(checksum) = &dependency.checksum {
out.push_str(&format!(" checksum: {checksum}\n"));
}
if let Some(install_path) = &dependency.install_path {
out.push_str(&format!(" install_path: {install_path}\n"));
}
}
}
if !manifest.capabilities.is_empty() {
out.push_str("capabilities:\n");
for capability in &manifest.capabilities {
out.push_str(&format!(" - {capability}\n"));
}
}
if !manifest.extensions.is_empty() {
out.push_str("extensions:\n");
for extension in &manifest.extensions {
out.push_str(&format!(" - {extension}\n"));
}
}
out
}
pub fn resolve_dependency_install_path(
package_root: &Path,
dependency_key: &str,
dependency: &PackageDependency,
lock: Option<&crate::package_lock::PackageLock>,
) -> Option<PathBuf> {
let locked = lock.and_then(|lock| lock.dependencies.get(dependency_key));
locked
.and_then(|dep| dep.resolved_install_path(package_root))
.or_else(|| dependency.resolved_install_path(package_root))
.or_else(|| {
dependency
.resolved_source_path()
.map(|path| {
if path.is_absolute() {
path
} else {
package_root.join(path)
}
})
.filter(|path| path.is_dir())
})
}
pub fn discover_package_root(path: &Path) -> Option<PackageRoot> {
let mut current = if path
.file_name()
.is_some_and(|name| name == PACKAGE_MANIFEST_FILE)
{
path.parent()
} else if path.is_dir() {
Some(path)
} else {
path.parent()
};
while let Some(dir) = current {
let manifest_path = dir.join(PACKAGE_MANIFEST_FILE);
if manifest_path.is_file() {
return Some(PackageRoot {
root: dir.to_path_buf(),
manifest_path,
});
}
current = dir.parent();
}
None
}
pub fn load_package_manifest_from_source(
source_path: &Path,
) -> Result<(PackageRoot, PackageManifest), String> {
let root = discover_package_root(source_path).ok_or_else(|| {
format!(
"could not find {PACKAGE_MANIFEST_FILE} for {}",
source_path.display()
)
})?;
let manifest = load_package_manifest(&root.manifest_path)?;
Ok((root, manifest))
}
pub fn compile_context_in_dir(dir: &Path) -> Option<PackageCompileContext> {
discover_package_root(dir).and_then(|root| compile_context_at_root(&root.root))
}
pub fn compile_context_for_source(source_path: &Path) -> Option<PackageCompileContext> {
discover_package_root(source_path).and_then(|root| compile_context_at_root(&root.root))
}
pub fn compile_context_at_root(package_root: &Path) -> Option<PackageCompileContext> {
let manifest_path = package_root.join(PACKAGE_MANIFEST_FILE);
if !manifest_path.is_file() {
return None;
}
let manifest = load_package_manifest(&manifest_path).ok()?;
let lock = crate::package_lock::discover_package_lock(package_root)
.and_then(|root| crate::package_lock::load_package_lock(&root.lock_path).ok());
let dependency_search_paths =
dependency_search_paths_for_root(package_root, &manifest, lock.as_ref());
Some(PackageCompileContext {
name: manifest.name,
entry: manifest.entry,
dependency_search_paths,
})
}
fn dependency_search_paths_for_root(
package_root: &Path,
manifest: &PackageManifest,
lock: Option<&crate::package_lock::PackageLock>,
) -> Vec<PathBuf> {
let mut paths = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for (key, dependency) in &manifest.dependencies {
let locked = lock.and_then(|lock| lock.dependencies.get(key));
let candidate = locked
.and_then(|dep| dep.resolved_install_path(package_root))
.or_else(|| dependency.resolved_install_path(package_root))
.or_else(|| {
dependency
.resolved_source_path()
.map(|path| {
if path.is_absolute() {
path
} else {
package_root.join(path)
}
})
.filter(|path| path.is_dir())
});
if let Some(path) = candidate {
let key = path.display().to_string();
if seen.insert(key) {
paths.push(path);
}
}
}
paths
}
pub fn load_package_report_from_source<I, S, J, T>(
source_path: &Path,
requested_targets: I,
required_capabilities: J,
) -> Result<PackageReport, String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
J: IntoIterator<Item = T>,
T: AsRef<str>,
{
let (root, manifest) = load_package_manifest_from_source(source_path)?;
let mut report = package_report(root, manifest, requested_targets, required_capabilities);
let semantic_imports = source_semantic_imports(source_path).unwrap_or_default();
let semantic_bindings = source_semantic_bindings(source_path).unwrap_or_default();
report.source_identity = Some(source_identity_for_path(
source_path,
Some(&report.manifest.name),
));
report.semantic_imports = resolve_semantic_imports(&semantic_imports, Some(&report.manifest));
report.semantic_bindings =
resolve_semantic_bindings(&semantic_bindings, &report.semantic_imports);
let lock = crate::package_lock::discover_package_lock(&report.root)
.and_then(|root| crate::package_lock::load_package_lock(&root.lock_path).ok());
report.symbol_index = symbol_index_for_semantic_imports_with_context(
&report.semantic_imports,
Some(&report.root),
Some(&report.manifest),
lock.as_ref(),
);
report
.symbol_index
.extend(symbol_index_for_semantic_bindings(
&report.semantic_bindings,
));
report.diagnostics = diagnostics_for_semantic_imports(&report.semantic_imports);
Ok(report)
}
pub fn package_report<I, S, J, T>(
root: PackageRoot,
manifest: PackageManifest,
requested_targets: I,
required_capabilities: J,
) -> PackageReport
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
J: IntoIterator<Item = T>,
T: AsRef<str>,
{
let target_selection = manifest.select_targets(requested_targets);
let capability_policy = manifest.validate_capability_policy(required_capabilities);
let graph = package_graph_report(&manifest);
PackageReport {
root: root.root,
manifest_path: root.manifest_path,
manifest,
target_selection,
capability_policy,
graph,
source_identity: None,
semantic_imports: Vec::new(),
semantic_bindings: Vec::new(),
symbol_index: Vec::new(),
diagnostics: Vec::new(),
}
}
fn source_semantic_imports(source_path: &Path) -> Result<Vec<String>, String> {
if source_path.extension().and_then(|ext| ext.to_str()) != Some("in") {
return Ok(Vec::new());
}
let source = fs::read_to_string(source_path)
.map_err(|err| format!("failed to read {}: {err}", source_path.display()))?;
let surface = crate::in_lang_parse::parse_in_surface_info(&source)?;
Ok(surface.semantic_imports)
}
fn source_semantic_bindings(
source_path: &Path,
) -> Result<Vec<crate::in_lang_parse::InSemanticBinding>, String> {
if source_path.extension().and_then(|ext| ext.to_str()) != Some("in") {
return Ok(Vec::new());
}
let source = fs::read_to_string(source_path).map_err(|err| err.to_string())?;
let surface = crate::in_lang_parse::parse_in_surface_info(&source)?;
Ok(surface.semantic_bindings)
}
pub fn semantic_imports_for_source_path(
source_path: &Path,
manifest: Option<&PackageManifest>,
) -> Result<Vec<PackageSemanticImport>, String> {
let imports = source_semantic_imports(source_path)?;
Ok(resolve_semantic_imports(&imports, manifest))
}
pub fn resolve_semantic_imports(
imports: &[String],
manifest: Option<&PackageManifest>,
) -> Vec<PackageSemanticImport> {
imports
.iter()
.map(|import| resolve_semantic_import(import, manifest))
.collect()
}
pub fn resolve_semantic_bindings(
bindings: &[crate::in_lang_parse::InSemanticBinding],
imports: &[PackageSemanticImport],
) -> Vec<PackageSemanticBinding> {
bindings
.iter()
.map(|binding| {
let resolved = imports
.iter()
.find(|import| import.import == binding.import && import.status == "resolved");
if let Some(import) = resolved {
PackageSemanticBinding {
import: binding.import.clone(),
alias: binding.alias.clone(),
dependency: import.dependency.clone(),
status: "resolved".to_string(),
reason: "semantic-import-resolved".to_string(),
}
} else {
PackageSemanticBinding {
import: binding.import.clone(),
alias: binding.alias.clone(),
dependency: None,
status: "unresolved".to_string(),
reason: "semantic-import-unresolved".to_string(),
}
}
})
.collect()
}
fn resolve_semantic_import(
import: &str,
manifest: Option<&PackageManifest>,
) -> PackageSemanticImport {
let Some(manifest) = manifest else {
return PackageSemanticImport {
import: import.to_string(),
dependency: None,
status: "unresolved".to_string(),
reason: "package-manifest-missing".to_string(),
};
};
if manifest.dependencies.contains_key(import) {
return PackageSemanticImport {
import: import.to_string(),
dependency: Some(import.to_string()),
status: "resolved".to_string(),
reason: dependency_resolution_reason(import).to_string(),
};
}
if let Some(package_ref) = crate::package_ref::parse_package_ref(import) {
for (key, dependency) in &manifest.dependencies {
if let Some(dep_ref) = crate::package_ref::package_ref_for_dependency(key, dependency)
&& dep_ref == package_ref
{
return PackageSemanticImport {
import: import.to_string(),
dependency: Some(key.clone()),
status: "resolved".to_string(),
reason: "dependency-ecosystem-match".to_string(),
};
}
}
}
let suffix = import.rsplit('.').next().unwrap_or(import);
if suffix != import && manifest.dependencies.contains_key(suffix) {
return PackageSemanticImport {
import: import.to_string(),
dependency: Some(suffix.to_string()),
status: "resolved".to_string(),
reason: "dependency-suffix-match".to_string(),
};
}
PackageSemanticImport {
import: import.to_string(),
dependency: None,
status: "unresolved".to_string(),
reason: "dependency-not-declared".to_string(),
}
}
fn dependency_resolution_reason(import: &str) -> &'static str {
if crate::package_ref::parse_package_ref(import).is_some() {
"dependency-ecosystem-exact-match"
} else {
"dependency-exact-match"
}
}
pub fn parse_package_manifest_source(source: &str) -> Result<PackageManifest, String> {
parse_package_manifest(source)
}
pub fn symbol_index_for_semantic_imports(
imports: &[PackageSemanticImport],
) -> Vec<PackageSymbolIndexEntry> {
symbol_index_for_semantic_imports_with_context(imports, None, None, None)
}
pub fn symbol_index_for_semantic_bindings(
bindings: &[PackageSemanticBinding],
) -> Vec<PackageSymbolIndexEntry> {
bindings
.iter()
.filter(|binding| binding.status == "resolved")
.filter_map(|binding| {
let dependency = binding.dependency.as_ref()?;
Some(PackageSymbolIndexEntry {
id: format!("symbol:binding:{}", binding.alias),
kind: "binding".to_string(),
name: binding.alias.clone(),
source_import: binding.import.clone(),
dependency: dependency.clone(),
})
})
.collect()
}
pub fn symbol_index_for_semantic_imports_with_context(
imports: &[PackageSemanticImport],
package_root: Option<&Path>,
manifest: Option<&PackageManifest>,
lock: Option<&crate::package_lock::PackageLock>,
) -> Vec<PackageSymbolIndexEntry> {
let mut entries = Vec::new();
for import in imports {
let dependency = match import.dependency.as_ref() {
Some(dependency) if import.status == "resolved" => dependency,
_ => continue,
};
entries.push(PackageSymbolIndexEntry {
id: format!("symbol:dependency:{dependency}"),
kind: "dependency".to_string(),
name: dependency.clone(),
source_import: import.import.clone(),
dependency: dependency.clone(),
});
if let (Some(root), Some(manifest)) = (package_root, manifest) {
let exports = crate::package_extern::export_symbols_for_resolved_import(
import, root, manifest, lock,
);
for export in exports {
entries.push(PackageSymbolIndexEntry {
id: format!("symbol:export:{dependency}:{export}"),
kind: "export".to_string(),
name: export,
source_import: import.import.clone(),
dependency: dependency.clone(),
});
}
}
}
entries
}
pub fn diagnostics_for_semantic_imports(
imports: &[PackageSemanticImport],
) -> Vec<PackageDiagnostic> {
imports
.iter()
.filter(|import| import.status != "resolved")
.map(|import| PackageDiagnostic {
code: "INPKG001".to_string(),
severity: "warning".to_string(),
import: import.import.clone(),
reason: import.reason.clone(),
message: format!(
"semantic import `{}` is not declared in the nearest inauguration.package",
import.import
),
})
.collect()
}
pub fn source_identity_for_path(
source_path: &Path,
manifest_name: Option<&str>,
) -> PackageSourceIdentity {
if source_path.extension().and_then(|ext| ext.to_str()) != Some("in") {
return PackageSourceIdentity {
package: None,
module: None,
manifest_name: manifest_name.map(str::to_string),
status: "not_in_source".to_string(),
reason: "source-not-inlang".to_string(),
};
}
let source = match fs::read_to_string(source_path) {
Ok(source) => source,
Err(_) => {
return PackageSourceIdentity {
package: None,
module: None,
manifest_name: manifest_name.map(str::to_string),
status: "unavailable".to_string(),
reason: "source-read-failed".to_string(),
};
}
};
let surface = match crate::in_lang_parse::parse_in_surface_info(&source) {
Ok(surface) => surface,
Err(_) => {
return PackageSourceIdentity {
package: None,
module: None,
manifest_name: manifest_name.map(str::to_string),
status: "unavailable".to_string(),
reason: "surface-parse-failed".to_string(),
};
}
};
source_identity_for_surface(surface.package, surface.module, manifest_name)
}
pub fn source_identity_for_surface(
package: Option<String>,
module: Option<String>,
manifest_name: Option<&str>,
) -> PackageSourceIdentity {
let manifest_name = manifest_name.map(str::to_string);
let (status, reason) = match (
package.as_deref(),
module.as_deref(),
manifest_name.as_deref(),
) {
(None, None, _) => ("not_declared", "source-identity-not-declared"),
(_, _, None) => ("missing_manifest", "package-manifest-missing"),
(Some(package), _, Some(manifest)) if package != manifest => {
("mismatch", "package-mismatch")
}
(Some(package), Some(module), Some(_))
if module != package && !module.starts_with(&format!("{package}.")) =>
{
("mismatch", "module-outside-package")
}
(None, Some(module), Some(manifest))
if module != manifest && !module.starts_with(&format!("{manifest}.")) =>
{
("mismatch", "module-outside-package")
}
_ => ("match", "package-module-match"),
};
PackageSourceIdentity {
package,
module,
manifest_name,
status: status.to_string(),
reason: reason.to_string(),
}
}
pub fn package_graph_report(manifest: &PackageManifest) -> PackageGraphReport {
let package_id = format!("package:{}", manifest.name);
let mut nodes = vec![PackageGraphNode {
id: package_id.clone(),
kind: "package".to_string(),
label: format!("{}@{}", manifest.name, manifest.version),
}];
let mut edges = Vec::new();
for target in manifest.targets.keys() {
let node_id = format!("target:{target}");
nodes.push(PackageGraphNode {
id: node_id.clone(),
kind: "target".to_string(),
label: target.clone(),
});
edges.push(PackageGraphEdge {
from: package_id.clone(),
to: node_id,
kind: "targets".to_string(),
});
}
for dependency in manifest.dependencies.keys() {
let node_id = format!("dependency:{dependency}");
nodes.push(PackageGraphNode {
id: node_id.clone(),
kind: "dependency".to_string(),
label: dependency.clone(),
});
edges.push(PackageGraphEdge {
from: package_id.clone(),
to: node_id,
kind: "depends-on".to_string(),
});
}
for capability in &manifest.capabilities {
let node_id = format!("capability:{capability}");
nodes.push(PackageGraphNode {
id: node_id.clone(),
kind: "capability".to_string(),
label: capability.clone(),
});
edges.push(PackageGraphEdge {
from: package_id.clone(),
to: node_id,
kind: "allows-capability".to_string(),
});
}
for extension in &manifest.extensions {
let node_id = format!("extension:{extension}");
nodes.push(PackageGraphNode {
id: node_id.clone(),
kind: "extension".to_string(),
label: extension.clone(),
});
edges.push(PackageGraphEdge {
from: package_id.clone(),
to: node_id,
kind: "uses-extension".to_string(),
});
}
PackageGraphReport {
package_id,
nodes,
edges,
}
}
impl PackageManifest {
pub fn enabled_targets(&self) -> Vec<String> {
self.targets
.iter()
.filter(|(_, enabled)| **enabled)
.map(|(target, _)| target.clone())
.collect()
}
pub fn disabled_targets(&self) -> Vec<String> {
self.targets
.iter()
.filter(|(_, enabled)| !**enabled)
.map(|(target, _)| target.clone())
.collect()
}
pub fn target_enabled(&self, target: &str) -> bool {
self.targets.get(target).copied().unwrap_or(false)
}
pub fn select_targets<I, S>(&self, requested_targets: I) -> PackageTargetSelection
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let requested = unique_strings(requested_targets);
if requested.is_empty() {
return PackageTargetSelection {
requested,
enabled: self.enabled_targets(),
disabled: Vec::new(),
unknown: Vec::new(),
};
}
let mut enabled = Vec::new();
let mut disabled = Vec::new();
let mut unknown = Vec::new();
for target in &requested {
match self.targets.get(target.as_str()) {
Some(true) => enabled.push(target.clone()),
Some(false) => disabled.push(target.clone()),
None => unknown.push(target.clone()),
}
}
PackageTargetSelection {
requested,
enabled,
disabled,
unknown,
}
}
pub fn has_capability(&self, capability: &str) -> bool {
self.capabilities.iter().any(|item| item == capability)
}
pub fn validate_capability_policy<I, S>(
&self,
required_capabilities: I,
) -> CapabilityPolicyValidation
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let allowed = unique_strings(self.capabilities.iter().map(String::as_str));
let required = unique_strings(required_capabilities);
let allowed_set: BTreeSet<&str> = allowed.iter().map(String::as_str).collect();
let missing = required
.iter()
.filter(|capability| !allowed_set.contains(capability.as_str()))
.cloned()
.collect::<Vec<_>>();
CapabilityPolicyValidation {
valid: missing.is_empty(),
allowed,
required,
missing,
}
}
}
fn unique_strings<I, S>(items: I) -> Vec<String>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut seen = BTreeSet::new();
let mut unique = Vec::new();
for item in items {
let item = item.as_ref().trim();
if !item.is_empty() && seen.insert(item.to_string()) {
unique.push(item.to_string());
}
}
unique
}
fn parse_package_manifest(source: &str) -> Result<PackageManifest, String> {
let mut manifest = PackageManifest {
name: String::new(),
version: String::new(),
entry: None,
targets: BTreeMap::new(),
dependencies: BTreeMap::new(),
capabilities: Vec::new(),
extensions: Vec::new(),
};
let mut section = None;
let mut dependency_name: Option<String> = None;
let mut dependency_subsection: Option<DependencySubsection> = None;
for (index, raw_line) in source.lines().enumerate() {
let line_number = index + 1;
if raw_line.trim().is_empty() {
continue;
}
if raw_line.contains('\t') {
return Err(format!(
"line {line_number}: tabs are not valid indentation in inauguration.package"
));
}
let indent = raw_line.len() - raw_line.trim_start_matches(' ').len();
let line = raw_line.trim_start_matches(' ');
match indent {
0 => {
dependency_name = None;
dependency_subsection = None;
section = parse_top_level(line, line_number, &mut manifest)?;
}
2 => match section {
Some(Section::Targets) => {
dependency_name = None;
dependency_subsection = None;
parse_target(line, line_number, &mut manifest)?;
}
Some(Section::Dependencies) => {
dependency_subsection = None;
dependency_name =
Some(parse_dependency_header(line, line_number, &mut manifest)?);
}
Some(Section::Capabilities) => {
dependency_name = None;
dependency_subsection = None;
parse_list_item(
line,
line_number,
"capabilities",
&mut manifest.capabilities,
)?;
}
Some(Section::Extensions) => {
dependency_name = None;
dependency_subsection = None;
parse_list_item(line, line_number, "extensions", &mut manifest.extensions)?;
}
None => {
return Err(format!(
"line {line_number}: indentation is only valid inside a section"
));
}
},
4 => {
if section != Some(Section::Dependencies) {
return Err(format!(
"line {line_number}: indentation is only valid for dependency metadata"
));
}
let name = dependency_name.as_deref().ok_or_else(|| {
format!("line {line_number}: dependency metadata requires a dependency name")
})?;
dependency_subsection =
parse_dependency_field(line, line_number, name, &mut manifest)?;
}
6 => {
if section != Some(Section::Dependencies) {
return Err(format!(
"line {line_number}: indentation is only valid for dependency metadata"
));
}
let name = dependency_name.as_deref().ok_or_else(|| {
format!("line {line_number}: dependency metadata requires a dependency name")
})?;
parse_dependency_nested_field(
line,
line_number,
name,
dependency_subsection,
&mut manifest,
)?;
}
_ => {
return Err(format!(
"line {line_number}: malformed indentation; use 0, 2, 4, or 6 spaces"
));
}
}
}
if manifest.name.is_empty() {
return Err("missing required field `name`".into());
}
if manifest.version.is_empty() {
return Err("missing required field `version`".into());
}
for (name, dependency) in &manifest.dependencies {
if dependency.version.is_empty() {
return Err(format!(
"dependency `{name}` is missing required field `version`"
));
}
}
for extension in &manifest.extensions {
if !crate::extension_registry::is_known_extension(extension) {
return Err(format!("unknown extension `{extension}`"));
}
}
Ok(manifest)
}
fn parse_top_level(
line: &str,
line_number: usize,
manifest: &mut PackageManifest,
) -> Result<Option<Section>, String> {
let (key, value) = split_field(line, line_number)?;
match key {
"name" => {
manifest.name = required_scalar(value, line_number, "name")?.to_string();
Ok(None)
}
"version" => {
manifest.version = required_scalar(value, line_number, "version")?.to_string();
Ok(None)
}
"entry" => {
manifest.entry = Some(required_scalar(value, line_number, "entry")?.to_string());
Ok(None)
}
"targets" => parse_section_header(value, line_number, "targets", Section::Targets),
"dependencies" => {
parse_section_header(value, line_number, "dependencies", Section::Dependencies)
}
"capabilities" => {
parse_section_header(value, line_number, "capabilities", Section::Capabilities)
}
"extensions" => parse_section_header(value, line_number, "extensions", Section::Extensions),
other => Err(format!(
"line {line_number}: unknown top-level field `{other}`"
)),
}
}
fn parse_section_header(
value: &str,
line_number: usize,
name: &str,
section: Section,
) -> Result<Option<Section>, String> {
if value.is_empty() {
Ok(Some(section))
} else {
Err(format!(
"line {line_number}: section `{name}` must not have an inline value"
))
}
}
fn parse_target(
line: &str,
line_number: usize,
manifest: &mut PackageManifest,
) -> Result<(), String> {
let (key, value) = split_field(line, line_number)?;
let enabled = match required_scalar(value, line_number, key)? {
"true" => true,
"false" => false,
other => {
return Err(format!(
"line {line_number}: target `{key}` expects true or false, got `{other}`"
));
}
};
if manifest.targets.insert(key.to_string(), enabled).is_some() {
return Err(format!("line {line_number}: duplicate target `{key}`"));
}
Ok(())
}
fn parse_dependency_header(
line: &str,
line_number: usize,
manifest: &mut PackageManifest,
) -> Result<String, String> {
let key = crate::package_ref::split_dependency_header(line, line_number)?;
if manifest
.dependencies
.insert(key.clone(), PackageDependency::default())
.is_some()
{
return Err(format!("line {line_number}: duplicate dependency `{key}`"));
}
Ok(key)
}
fn parse_dependency_field(
line: &str,
line_number: usize,
dependency_name: &str,
manifest: &mut PackageManifest,
) -> Result<Option<DependencySubsection>, String> {
let (key, value) = split_field(line, line_number)?;
let dependency = manifest
.dependencies
.get_mut(dependency_name)
.ok_or_else(|| format!("line {line_number}: unknown dependency `{dependency_name}`"))?;
match key {
"version" => {
let version = required_scalar(value, line_number, "version")?;
if !dependency.version.is_empty() {
return Err(format!(
"line {line_number}: duplicate version for dependency `{dependency_name}`"
));
}
dependency.version = version.to_string();
Ok(None)
}
"kind" => {
let kind = required_scalar(value, line_number, "kind")?;
if dependency.kind.is_some() {
return Err(format!(
"line {line_number}: duplicate kind for dependency `{dependency_name}`"
));
}
dependency.kind = Some(kind.to_string());
Ok(None)
}
"source" => {
let source = required_scalar(value, line_number, "source")?;
if dependency.source.is_some() {
return Err(format!(
"line {line_number}: duplicate source for dependency `{dependency_name}`"
));
}
dependency.source = Some(source.to_string());
Ok(None)
}
"rev" => {
let rev = required_scalar(value, line_number, "rev")?;
if dependency.rev.is_some() {
return Err(format!(
"line {line_number}: duplicate rev for dependency `{dependency_name}`"
));
}
dependency.rev = Some(rev.to_string());
Ok(None)
}
"checksum" => {
let checksum = required_scalar(value, line_number, "checksum")?;
if dependency.checksum.is_some() {
return Err(format!(
"line {line_number}: duplicate checksum for dependency `{dependency_name}`"
));
}
dependency.checksum = Some(checksum.to_string());
Ok(None)
}
"install_path" => {
let install_path = required_scalar(value, line_number, "install_path")?;
if dependency.install_path.is_some() {
return Err(format!(
"line {line_number}: duplicate install_path for dependency `{dependency_name}`"
));
}
dependency.install_path = Some(install_path.to_string());
Ok(None)
}
"targets" => parse_dependency_subsection_header(
value,
line_number,
"targets",
DependencySubsection::Targets,
),
"capabilities" => parse_dependency_subsection_header(
value,
line_number,
"capabilities",
DependencySubsection::Capabilities,
),
"build" => parse_dependency_subsection_header(
value,
line_number,
"build",
DependencySubsection::Build,
),
other => Err(format!(
"line {line_number}: unknown dependency field `{other}` for `{dependency_name}`"
)),
}
}
fn parse_dependency_subsection_header(
value: &str,
line_number: usize,
name: &str,
subsection: DependencySubsection,
) -> Result<Option<DependencySubsection>, String> {
if value.is_empty() {
Ok(Some(subsection))
} else {
Err(format!(
"line {line_number}: dependency subsection `{name}` must not have an inline value"
))
}
}
fn parse_dependency_nested_field(
line: &str,
line_number: usize,
dependency_name: &str,
subsection: Option<DependencySubsection>,
manifest: &mut PackageManifest,
) -> Result<(), String> {
let subsection = subsection.ok_or_else(|| {
format!("line {line_number}: nested dependency metadata requires a subsection")
})?;
let dependency = manifest
.dependencies
.get_mut(dependency_name)
.ok_or_else(|| format!("line {line_number}: unknown dependency `{dependency_name}`"))?;
match subsection {
DependencySubsection::Targets => parse_list_item(
line,
line_number,
"dependency targets",
&mut dependency.targets,
),
DependencySubsection::Capabilities => parse_list_item(
line,
line_number,
"dependency capabilities",
&mut dependency.capabilities,
),
DependencySubsection::Build => {
let (key, value) = split_field(line, line_number)?;
let value = required_scalar(value, line_number, key)?;
if dependency
.build
.insert(key.to_string(), value.to_string())
.is_some()
{
return Err(format!(
"line {line_number}: duplicate build field `{key}` for dependency `{dependency_name}`"
));
}
Ok(())
}
}
}
fn parse_list_item(
line: &str,
line_number: usize,
section: &str,
values: &mut Vec<String>,
) -> Result<(), String> {
let Some(value) = line.strip_prefix("- ") else {
return Err(format!(
"line {line_number}: section `{section}` only supports list items"
));
};
let value = value.trim();
if value.is_empty() {
return Err(format!(
"line {line_number}: section `{section}` contains an empty list item"
));
}
values.push(value.to_string());
Ok(())
}
fn split_field(line: &str, line_number: usize) -> Result<(&str, &str), String> {
let Some((key, value)) = line.split_once(':') else {
return Err(format!("line {line_number}: expected `key: value`"));
};
let key = key.trim();
if key.is_empty() {
return Err(format!("line {line_number}: empty key"));
}
Ok((key, value.trim()))
}
fn required_scalar<'a>(value: &'a str, line_number: usize, field: &str) -> Result<&'a str, String> {
if value.is_empty() {
Err(format!(
"line {line_number}: field `{field}` requires a value"
))
} else {
Ok(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static TEMP_COUNTER: AtomicUsize = AtomicUsize::new(0);
struct TempDirGuard {
path: PathBuf,
}
impl TempDirGuard {
fn new() -> Self {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX_EPOCH")
.as_nanos();
let path = std::env::temp_dir().join(format!(
"inauguration-package-manifest-{}-{}-{}",
std::process::id(),
unique,
TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&path).expect("create temp dir");
Self { path }
}
}
impl Drop for TempDirGuard {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn parse_text(source: &str) -> Result<PackageManifest, String> {
let temp = TempDirGuard::new();
let manifest_path = temp.path.join("inauguration.package");
fs::write(&manifest_path, source).expect("write manifest");
load_package_manifest(&manifest_path)
}
#[test]
fn loads_package_manifest_from_file() {
let manifest = parse_text(
r#"name: hyperchat
version: 0.1.0
targets:
linux: true
macos: true
web: true
dependencies:
postgres:
version: ^1.0.0
redis:
version: latest
capabilities:
- filesystem.read
- filesystem.write
- network.http
extensions:
- postgres-driver
- distributed-workers
- gpu-optimizer
"#,
)
.expect("parse package manifest");
assert_eq!(manifest.name, "hyperchat");
assert_eq!(manifest.version, "0.1.0");
assert_eq!(manifest.targets.get("linux"), Some(&true));
assert_eq!(manifest.targets.get("macos"), Some(&true));
assert_eq!(manifest.targets.get("web"), Some(&true));
assert_eq!(
manifest.dependencies.get("postgres"),
Some(&PackageDependency {
version: "^1.0.0".into(),
..Default::default()
})
);
assert_eq!(
manifest.dependencies.get("redis"),
Some(&PackageDependency {
version: "latest".into(),
..Default::default()
})
);
assert_eq!(
manifest.capabilities,
vec![
"filesystem.read".to_string(),
"filesystem.write".to_string(),
"network.http".to_string()
]
);
assert_eq!(
manifest.extensions,
vec![
"postgres-driver".to_string(),
"distributed-workers".to_string(),
"gpu-optimizer".to_string()
]
);
}
#[test]
fn loads_package_manifest_from_directory() {
let temp = TempDirGuard::new();
fs::write(
temp.path.join("inauguration.package"),
r#"name: sample
version: 1.2.3
"#,
)
.expect("write manifest");
let manifest = load_package_manifest(&temp.path).expect("load manifest from directory");
assert_eq!(manifest.name, "sample");
assert_eq!(manifest.version, "1.2.3");
assert!(manifest.targets.is_empty());
assert!(manifest.dependencies.is_empty());
assert!(manifest.capabilities.is_empty());
assert!(manifest.extensions.is_empty());
}
#[test]
fn loads_optional_package_entry() {
let manifest = parse_text(
r#"name: entrypoint
version: 0.1.0
entry: start
"#,
)
.expect("parse manifest");
assert_eq!(manifest.entry.as_deref(), Some("start"));
}
#[test]
fn loads_extended_dependency_metadata() {
let manifest = parse_text(
r#"name: hyperchat
version: 0.1.0
dependencies:
postgres:
version: ^1.0.0
kind: registry
source: https://registry.inauguration.dev
rev: main
checksum: sha256:abc123
targets:
- macos
- linux
capabilities:
- network.http
build:
profile: release
features: gpu
"#,
)
.expect("parse extended dependency metadata");
let postgres = manifest
.dependencies
.get("postgres")
.expect("postgres dependency");
assert_eq!(postgres.version, "^1.0.0");
assert_eq!(postgres.kind.as_deref(), Some("registry"));
assert_eq!(
postgres.source.as_deref(),
Some("https://registry.inauguration.dev")
);
assert_eq!(postgres.rev.as_deref(), Some("main"));
assert_eq!(postgres.checksum.as_deref(), Some("sha256:abc123"));
assert_eq!(
postgres.targets,
vec!["macos".to_string(), "linux".to_string()]
);
assert_eq!(postgres.capabilities, vec!["network.http".to_string()]);
assert_eq!(
postgres.build.get("profile").map(String::as_str),
Some("release")
);
assert_eq!(
postgres.build.get("features").map(String::as_str),
Some("gpu")
);
}
#[test]
fn resolves_path_dependency_from_kind_and_source() {
let dependency = PackageDependency {
version: "0.1.0".into(),
kind: Some("path".into()),
source: Some("../local".into()),
..Default::default()
};
assert_eq!(
dependency.resolved_source_path(),
Some(PathBuf::from("../local"))
);
}
#[test]
fn discovers_compile_context_from_manifest() {
let temp = TempDirGuard::new();
fs::create_dir_all(temp.path.join("vendor/local")).expect("vendor dir");
fs::write(
temp.path.join("inauguration.package"),
r#"name: compile-context
version: 0.1.0
entry: boot
dependencies:
local:
version: path:vendor/local
"#,
)
.expect("write manifest");
let context = compile_context_in_dir(&temp.path).expect("discover package compile context");
assert_eq!(context.name, "compile-context");
assert_eq!(context.entry.as_deref(), Some("boot"));
assert_eq!(context.dependency_search_paths.len(), 1);
assert!(context.dependency_search_paths[0].ends_with("vendor/local"));
}
#[test]
fn rejects_bad_indentation() {
let err = parse_text(
r#"name: bad
version: 0.1.0
targets:
linux: true
"#,
)
.expect_err("reject malformed indentation");
assert!(err.contains("line 4"), "{err}");
assert!(err.contains("indentation"), "{err}");
}
#[test]
fn rejects_unknown_section_shape() {
let err = parse_text(
r#"name: bad
version: 0.1.0
capabilities:
filesystem.read: true
"#,
)
.expect_err("reject map-shaped capabilities section");
assert!(err.contains("line 4"), "{err}");
assert!(err.contains("capabilities"), "{err}");
}
#[test]
fn rejects_unknown_top_level_field() {
let err = parse_text(
r#"name: bad
version: 0.1.0
scripts:
test: in test
"#,
)
.expect_err("reject unknown top-level field");
assert!(err.contains("line 3"), "{err}");
assert!(err.contains("scripts"), "{err}");
}
#[test]
fn rejects_unknown_extension() {
let err = parse_text(
r#"name: bad
version: 0.1.0
extensions:
- unknown-runtime
"#,
)
.expect_err("reject unknown extension");
assert!(err.contains("unknown extension"), "{err}");
}
#[test]
fn discovers_package_root_from_nested_source_path() {
let temp = TempDirGuard::new();
fs::write(
temp.path.join("inauguration.package"),
r#"name: rooted
version: 0.1.0
"#,
)
.expect("write manifest");
let source_path = temp.path.join("Sources").join("App").join("main.in");
fs::create_dir_all(source_path.parent().expect("source parent")).expect("create sources");
fs::write(&source_path, "fn main() -> void { return; }\n").expect("write source");
let root = discover_package_root(&source_path).expect("discover package root");
assert_eq!(root.root, temp.path);
assert_eq!(root.manifest_path, temp.path.join("inauguration.package"));
}
#[test]
fn selects_enabled_disabled_and_unknown_targets() {
let manifest = parse_text(
r#"name: targets
version: 0.1.0
targets:
macos: true
linux: false
web: true
"#,
)
.expect("parse manifest");
let selection = manifest.select_targets(["web", "linux", "ios"]);
assert_eq!(manifest.enabled_targets(), vec!["macos", "web"]);
assert_eq!(selection.enabled, vec!["web"]);
assert_eq!(selection.disabled, vec!["linux"]);
assert_eq!(selection.unknown, vec!["ios"]);
}
#[test]
fn validates_required_capabilities_against_package_policy() {
let manifest = parse_text(
r#"name: caps
version: 0.1.0
capabilities:
- fs.read
- process.stdout
"#,
)
.expect("parse manifest");
let validation = manifest.validate_capability_policy(["process.stdout", "network.http"]);
assert!(!validation.valid);
assert_eq!(validation.allowed, vec!["fs.read", "process.stdout"]);
assert_eq!(validation.required, vec!["process.stdout", "network.http"]);
assert_eq!(validation.missing, vec!["network.http"]);
}
#[test]
fn builds_package_report_with_graph_nodes_and_edges() {
let temp = TempDirGuard::new();
fs::write(
temp.path.join("inauguration.package"),
r#"name: graphable
version: 0.1.0
targets:
macos: true
dependencies:
corelib:
version: ^1.2.0
capabilities:
- process.stdout
extensions:
- preview-host
"#,
)
.expect("write manifest");
let source_path = temp.path.join("src").join("main.in");
fs::create_dir_all(source_path.parent().expect("source parent"))
.expect("create source dir");
fs::write(&source_path, "capability process.stdout;\n").expect("write source");
let report = load_package_report_from_source(
&source_path,
["macos", "web"],
["process.stdout", "fs.read"],
)
.expect("load package report");
assert_eq!(report.root, temp.path);
assert_eq!(report.manifest.name, "graphable");
assert_eq!(report.target_selection.enabled, vec!["macos"]);
assert_eq!(report.target_selection.unknown, vec!["web"]);
assert_eq!(report.capability_policy.missing, vec!["fs.read"]);
assert!(
report
.graph
.nodes
.iter()
.any(|node| node.id == "package:graphable")
);
assert!(
report
.graph
.edges
.iter()
.any(|edge| edge.from == "package:graphable" && edge.to == "dependency:corelib")
);
}
#[test]
fn source_package_report_carries_identity_status() {
let temp = TempDirGuard::new();
fs::write(
temp.path.join("inauguration.package"),
"name: agents.sample\nversion: 0.1.0\n",
)
.expect("write manifest");
let source_path = temp.path.join("main.in");
fs::write(
&source_path,
"package agents.sample;\nmodule agents.sample.main;\nfn main() -> void { return; }\n",
)
.expect("write source");
let report = load_package_report_from_source(
&source_path,
std::iter::empty::<&str>(),
std::iter::empty::<&str>(),
)
.expect("load source package report");
let identity = report.source_identity.expect("source identity");
assert_eq!(identity.manifest_name.as_deref(), Some("agents.sample"));
assert_eq!(identity.status, "match");
assert_eq!(identity.reason, "package-module-match");
}
#[test]
fn source_package_report_carries_semantic_symbol_index_and_diagnostics() {
let temp = TempDirGuard::new();
fs::write(
temp.path.join("inauguration.package"),
r#"name: agents.sample
version: 0.1.0
dependencies:
postgres:
version: ^1.0.0
"#,
)
.expect("write manifest");
let source_path = temp.path.join("main.in");
fs::write(
&source_path,
"package agents.sample;\nuse database.postgres;\nbind database.postgres as postgres;\nuse cache.redis;\nfn main() -> void { return; }\n",
)
.expect("write source");
let report = load_package_report_from_source(
&source_path,
std::iter::empty::<&str>(),
std::iter::empty::<&str>(),
)
.expect("load source package report");
assert_eq!(report.semantic_bindings.len(), 1);
assert_eq!(report.semantic_bindings[0].alias, "postgres");
assert_eq!(report.semantic_bindings[0].status, "resolved");
assert_eq!(report.symbol_index.len(), 2);
assert_eq!(report.symbol_index[0].id, "symbol:dependency:postgres");
assert_eq!(report.symbol_index[0].source_import, "database.postgres");
assert!(
report
.symbol_index
.iter()
.any(|entry| entry.id == "symbol:binding:postgres"
&& entry.kind == "binding"
&& entry.name == "postgres")
);
assert_eq!(report.diagnostics.len(), 1);
assert_eq!(report.diagnostics[0].code, "INPKG001");
assert_eq!(report.diagnostics[0].import, "cache.redis");
}
#[test]
fn reports_source_identity_match_and_mismatch() {
let matching = source_identity_for_surface(
Some("graphable".into()),
Some("graphable.main".into()),
Some("graphable"),
);
assert_eq!(matching.status, "match");
assert_eq!(matching.reason, "package-module-match");
let package_mismatch = source_identity_for_surface(
Some("other".into()),
Some("other.main".into()),
Some("graphable"),
);
assert_eq!(package_mismatch.status, "mismatch");
assert_eq!(package_mismatch.reason, "package-mismatch");
let missing_manifest = source_identity_for_surface(Some("graphable".into()), None, None);
assert_eq!(missing_manifest.status, "missing_manifest");
assert_eq!(missing_manifest.reason, "package-manifest-missing");
let module_mismatch =
source_identity_for_surface(None, Some("other.main".into()), Some("graphable"));
assert_eq!(module_mismatch.status, "mismatch");
assert_eq!(module_mismatch.reason, "module-outside-package");
}
#[test]
fn ecosystem_semantic_imports_resolve_against_manifest_dependencies() {
let manifest = parse_text(
r#"name: ecosystem-demo
version: 0.1.0
dependencies:
cargo:crepuscularity:
version: path:vendor/cargo/crepuscularity
kind: cargo
npm:hono:
version: path:vendor/npm/hono
kind: npm
"#,
)
.expect("parse manifest");
let imports = resolve_semantic_imports(
&[
"cargo:crepuscularity".to_string(),
"npm:hono".to_string(),
"cargo:missing".to_string(),
],
Some(&manifest),
);
assert_eq!(imports[0].status, "resolved");
assert_eq!(
imports[0].dependency.as_deref(),
Some("cargo:crepuscularity")
);
assert_eq!(imports[0].reason, "dependency-ecosystem-exact-match");
assert_eq!(imports[1].status, "resolved");
assert_eq!(imports[1].dependency.as_deref(), Some("npm:hono"));
assert_eq!(imports[2].status, "unresolved");
}
#[test]
fn semantic_imports_resolve_against_manifest_dependencies() {
let manifest = parse_text(
r#"name: hyperchat
version: 0.1.0
dependencies:
postgres:
version: ^1.0.0
"#,
)
.expect("parse manifest");
let imports = resolve_semantic_imports(
&["database.postgres".to_string(), "cache.redis".to_string()],
Some(&manifest),
);
assert_eq!(imports[0].status, "resolved");
assert_eq!(imports[0].dependency.as_deref(), Some("postgres"));
assert_eq!(imports[0].reason, "dependency-suffix-match");
assert_eq!(imports[1].status, "unresolved");
assert_eq!(imports[1].reason, "dependency-not-declared");
let symbols = symbol_index_for_semantic_imports(&imports);
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].id, "symbol:dependency:postgres");
assert_eq!(symbols[0].source_import, "database.postgres");
let diagnostics = diagnostics_for_semantic_imports(&imports);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code, "INPKG001");
assert_eq!(diagnostics[0].severity, "warning");
assert_eq!(diagnostics[0].import, "cache.redis");
}
}