1use crate::kernel::{parse, Form};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum PackageSelectorMode {
7 Base,
8 Complete,
9 Exclude(Vec<String>),
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct PackageSelector {
14 pub namespace: String,
15 pub mode: PackageSelectorMode,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct PackageBundle {
20 pub path: String,
21 pub include: Vec<String>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct PackageDefinition {
26 pub name: String,
27 pub description: Option<String>,
28 pub selectors: Vec<PackageSelector>,
29 pub dependencies: Vec<String>,
30 pub optional: Vec<String>,
31 pub bundles: Vec<PackageBundle>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct LockedPackage {
36 pub coordinate: String,
37 pub name: Option<String>,
38 pub version: String,
39 pub tap: String,
40 pub registry_commit: String,
41 pub identity_revision: String,
42 pub archive_sha256: String,
43 pub namespaces: Vec<String>,
44 pub dependencies: Vec<String>,
45}
46
47pub fn catalog_from_lock(source: &str) -> Result<Vec<LockedPackage>, String> {
48 let document = parse(source)?;
49 let root = map(&document, "project.lock.edn must be an EDN map")?;
50 if !matches!(lookup(root, "lock/format"), Some(Form::String(version)) if version == "0.0.0-alpha")
51 {
52 return Err("project.lock.edn requires :lock/format \"0.0.0-alpha\"".into());
53 }
54 let packages = match lookup(root, "packages") {
55 Some(value) => map(value, "project.lock.edn :packages must be a map")?,
56 None => return Ok(Vec::new()),
57 };
58 let mut output = Vec::with_capacity(packages.len());
59 for (coordinate, descriptor) in packages {
60 let coordinate = scalar(coordinate, "locked package coordinate")?;
61 let descriptor = map(descriptor, "locked package descriptor must be a map")?;
62 let name = lookup_any(descriptor, &["name", "package/name"])
63 .map(|value| scalar(value, "locked package :name"))
64 .transpose()?;
65 let version = string(required(descriptor, "version")?, "locked package :version")?;
66 semver::Version::parse(&version)
67 .map_err(|error| format!("locked package {coordinate} has invalid version: {error}"))?;
68 let archive_sha256 = string(
69 required(descriptor, "archive-sha256")?,
70 "locked package :archive-sha256",
71 )?;
72 validate_sha256(&archive_sha256)?;
73 let tap = string(required(descriptor, "tap")?, "locked package :tap")?;
74 let registry_commit = string(
75 required(descriptor, "registry-commit")?,
76 "locked package :registry-commit",
77 )?;
78 validate_commit(®istry_commit, "registry-commit")?;
79 let identity_revision = string(
80 required(descriptor, "identity-revision")?,
81 "locked package :identity-revision",
82 )?;
83 validate_commit(&identity_revision, "identity-revision")?;
84 let namespaces = symbols(
85 required(descriptor, "namespaces")?,
86 "locked package :namespaces",
87 )?;
88 if namespaces.is_empty() {
89 return Err(format!("locked package {coordinate} exports no namespaces"));
90 }
91 let dependencies = match lookup(descriptor, "dependencies") {
92 Some(value) => map_keys(value, "locked package :dependencies")?,
93 None => Vec::new(),
94 };
95 output.push(LockedPackage {
96 coordinate,
97 name,
98 version,
99 tap,
100 registry_commit,
101 identity_revision,
102 archive_sha256,
103 namespaces,
104 dependencies,
105 });
106 }
107 output.sort_by(|left, right| left.coordinate.cmp(&right.coordinate));
108 let mut owners = std::collections::BTreeMap::new();
109 let mut names = std::collections::BTreeMap::new();
110 for package in &output {
111 if let Some(name) = &package.name {
112 if let Some(previous) = names.insert(name, &package.coordinate) {
113 return Err(format!(
114 "package/name-conflict: {name} is used by {previous} and {}",
115 package.coordinate
116 ));
117 }
118 }
119 for namespace in &package.namespaces {
120 if let Some(previous) = owners.insert(namespace, &package.coordinate) {
121 return Err(format!(
122 "package/namespace-conflict: {namespace} is exported by {previous} and {}",
123 package.coordinate
124 ));
125 }
126 }
127 }
128 let coordinates = output
129 .iter()
130 .map(|package| package.coordinate.as_str())
131 .collect::<std::collections::BTreeSet<_>>();
132 for package in &output {
133 for dependency in &package.dependencies {
134 if dependency == &package.coordinate {
135 return Err(format!(
136 "package/dependency-cycle: {} depends on itself",
137 package.coordinate
138 ));
139 }
140 if !coordinates.contains(dependency.as_str()) {
141 return Err(format!(
142 "package/dependency-not-locked: {} requires {dependency}",
143 package.coordinate
144 ));
145 }
146 }
147 }
148 Ok(output)
149}
150
151pub fn definitions_from_packages_edn(source: &str) -> Result<Vec<PackageDefinition>, String> {
156 let document = parse(source)?;
157 let root = map(&document, "packages.edn must be an EDN map")?;
158 let packages = match lookup(root, "packages") {
159 Some(value) => map(value, "packages.edn :packages must be a map")?,
160 None => root,
161 };
162 let mut definitions = Vec::with_capacity(packages.len());
163 for (name, descriptor) in packages {
164 let name = profile_scalar(name, "package coordinate")?;
165 let descriptor = map(descriptor, "package descriptor must be a map")?;
166 let description = lookup(descriptor, "description")
167 .map(|value| string(value, "package :description"))
168 .transpose()?;
169 let selectors = parse_selectors(required(descriptor, "include")?, &name)?;
170 let dependencies = optional_identifiers(descriptor, "dependencies")?;
171 let optional = optional_identifiers(descriptor, "optional")?;
172 let bundles = parse_bundles(descriptor)?;
173 definitions.push(PackageDefinition {
174 name,
175 description,
176 selectors,
177 dependencies,
178 optional,
179 bundles,
180 });
181 }
182 definitions.sort_by(|left, right| left.name.cmp(&right.name));
183 for pair in definitions.windows(2) {
184 if pair[0].name == pair[1].name {
185 return Err(format!("package/duplicate-definition: {}", pair[0].name));
186 }
187 }
188 Ok(definitions)
189}
190
191pub fn expand_selector(selector: &PackageSelector, available: &[String]) -> Vec<String> {
194 let mut selected = available
195 .iter()
196 .filter(|namespace| selector_matches(selector, namespace))
197 .cloned()
198 .collect::<Vec<_>>();
199 selected.sort();
200 selected.dedup();
201 selected
202}
203
204pub fn package_namespaces(package: &PackageDefinition, available: &[String]) -> Vec<String> {
206 let mut selected = package
207 .selectors
208 .iter()
209 .flat_map(|selector| expand_selector(selector, available))
210 .collect::<Vec<_>>();
211 selected.sort();
212 selected.dedup();
213 selected
214}
215
216pub fn find_package_definition<'a>(
217 definitions: &'a [PackageDefinition],
218 target: &str,
219) -> Result<&'a PackageDefinition, String> {
220 let matches = definitions
221 .iter()
222 .filter(|definition| {
223 definition.name == target
224 || definition
225 .name
226 .rsplit_once('/')
227 .is_some_and(|(_, name)| name == target)
228 })
229 .collect::<Vec<_>>();
230 match matches.as_slice() {
231 [definition] => Ok(definition),
232 [] => Err(format!("package/not-defined: {target}")),
233 _ => Err(format!("package/ambiguous-definition: {target}")),
234 }
235}
236
237pub fn validate_package_definitions(
243 definitions: &[PackageDefinition],
244 available: &[String],
245) -> Result<(), String> {
246 let mut owners = std::collections::BTreeMap::new();
247 for package in definitions {
248 for namespace in package_namespaces(package, available) {
249 if let Some(previous) = owners.insert(namespace.clone(), package.name.clone()) {
250 return Err(format!(
251 "package/namespace-overlap: {namespace} belongs to {previous} and {}",
252 package.name
253 ));
254 }
255 }
256 }
257 let targets = definitions
258 .iter()
259 .map(|definition| definition.name.clone())
260 .collect::<Vec<_>>();
261 package_dependency_order(definitions, &targets).map(|_| ())
262}
263
264pub fn package_dependency_order(
268 definitions: &[PackageDefinition],
269 targets: &[String],
270) -> Result<Vec<String>, String> {
271 let definitions = definitions
272 .iter()
273 .map(|definition| (definition.name.clone(), definition))
274 .collect::<std::collections::BTreeMap<_, _>>();
275 let mut ordered = Vec::new();
276 let mut visiting = std::collections::BTreeSet::new();
277 let mut visited = std::collections::BTreeSet::new();
278 for target in targets {
279 let target = resolve_definition_name(&definitions, target)?;
280 visit_package_dependency(
281 &target,
282 &definitions,
283 &mut visiting,
284 &mut visited,
285 &mut ordered,
286 )?;
287 }
288 Ok(ordered)
289}
290
291fn visit_package_dependency(
292 name: &str,
293 definitions: &std::collections::BTreeMap<String, &PackageDefinition>,
294 visiting: &mut std::collections::BTreeSet<String>,
295 visited: &mut std::collections::BTreeSet<String>,
296 ordered: &mut Vec<String>,
297) -> Result<(), String> {
298 if visited.contains(name) {
299 return Ok(());
300 }
301 if !visiting.insert(name.to_owned()) {
302 return Err(format!("package/dependency-cycle: {name}"));
303 }
304 let package = definitions
305 .get(name)
306 .ok_or_else(|| format!("package/not-defined: {name}"))?;
307 let mut dependencies = package
308 .dependencies
309 .iter()
310 .filter_map(|dependency| {
311 if definitions.contains_key(dependency.as_str()) {
312 return Some(Ok(dependency.clone()));
313 }
314 let suffix = dependency
315 .rsplit_once('/')
316 .map(|(_, name)| name)
317 .unwrap_or(dependency);
318 let matches = definitions
319 .keys()
320 .filter(|name| {
321 name.as_str() == suffix
322 || name.rsplit_once('/').map(|(_, name)| name) == Some(suffix)
323 })
324 .cloned()
325 .collect::<Vec<_>>();
326 match matches.as_slice() {
327 [] => None,
328 [name] => Some(Ok(name.clone())),
329 _ => Some(Err(format!(
330 "package/ambiguous-dependency: {dependency} ({})",
331 matches.join(",")
332 ))),
333 }
334 })
335 .collect::<Result<Vec<_>, _>>()
336 ?;
337 dependencies.sort();
338 dependencies.dedup();
339 for dependency in dependencies {
340 visit_package_dependency(&dependency, definitions, visiting, visited, ordered)?;
341 }
342 visiting.remove(name);
343 visited.insert(name.to_owned());
344 ordered.push(name.to_owned());
345 Ok(())
346}
347
348fn resolve_definition_name(
349 definitions: &std::collections::BTreeMap<String, &PackageDefinition>,
350 target: &str,
351) -> Result<String, String> {
352 resolve_optional_definition_name(definitions, target)
353 .ok_or_else(|| format!("package/not-defined: {target}"))
354}
355
356fn resolve_optional_definition_name(
357 definitions: &std::collections::BTreeMap<String, &PackageDefinition>,
358 target: &str,
359) -> Option<String> {
360 if definitions.contains_key(target) {
361 return Some(target.to_owned());
362 }
363 let suffix = target
364 .rsplit_once('/')
365 .map(|(_, name)| name)
366 .unwrap_or(target);
367 let matches = definitions
368 .keys()
369 .filter(|name| {
370 name.as_str() == suffix || name.rsplit_once('/').map(|(_, name)| name) == Some(suffix)
371 })
372 .cloned()
373 .collect::<Vec<_>>();
374 (matches.len() == 1).then(|| matches[0].clone())
375}
376
377fn parse_selectors(form: &Form, package: &str) -> Result<Vec<PackageSelector>, String> {
378 let values = sequence(form, &format!("package {package} :include"))?;
379 values
380 .iter()
381 .enumerate()
382 .map(|(index, value)| {
383 let parts = sequence(value, &format!("package {package} selector {index}"))?;
384 let namespace = profile_scalar(
385 parts
386 .first()
387 .ok_or_else(|| format!("package {package} selector {index} is empty"))?,
388 "package selector namespace",
389 )?;
390 let mode = parts
391 .get(1)
392 .ok_or_else(|| format!("package {package} selector {index} is missing a mode"))?;
393 let mode = profile_scalar(mode, "package selector mode")?;
394 match mode.as_str() {
395 "base" if parts.len() == 2 => Ok(PackageSelector {
396 namespace,
397 mode: PackageSelectorMode::Base,
398 }),
399 "complete" if parts.len() == 2 => Ok(PackageSelector {
400 namespace,
401 mode: PackageSelectorMode::Complete,
402 }),
403 "exclude" if parts.len() == 3 => {
404 let excluded = sequence(&parts[2], "package selector exclusions")?
405 .iter()
406 .map(|value| profile_scalar(value, "package selector exclusion"))
407 .collect::<Result<Vec<_>, _>>()?;
408 Ok(PackageSelector {
409 namespace,
410 mode: PackageSelectorMode::Exclude(excluded),
411 })
412 }
413 "base" | "complete" | "exclude" => Err(format!(
414 "package {package} selector {index} has invalid arity for :{mode}"
415 )),
416 _ => Err(format!(
417 "package {package} selector {index} has unsupported mode :{mode}"
418 )),
419 }
420 })
421 .collect()
422}
423
424fn parse_bundles(descriptor: &[(Form, Form)]) -> Result<Vec<PackageBundle>, String> {
425 let Some(form) = lookup(descriptor, "bundle") else {
426 return Ok(Vec::new());
427 };
428 sequence(form, "package :bundle")?
429 .iter()
430 .enumerate()
431 .map(|(index, form)| {
432 let entries = map(form, "package bundle must be a map")?;
433 let path = string(required(entries, "path")?, "package bundle :path")?;
434 let include = sequence(required(entries, "include")?, "package bundle :include")?
435 .iter()
436 .map(|value| string(value, "package bundle include path"))
437 .collect::<Result<Vec<_>, _>>()?;
438 if path.is_empty() {
439 return Err(format!("package bundle {index} has an empty path"));
440 }
441 Ok(PackageBundle { path, include })
442 })
443 .collect()
444}
445
446fn optional_identifiers(descriptor: &[(Form, Form)], key: &str) -> Result<Vec<String>, String> {
447 let Some(form) = lookup(descriptor, key) else {
448 return Ok(Vec::new());
449 };
450 let mut values = sequence(form, &format!("package :{key}"))?
451 .iter()
452 .map(|value| profile_scalar(value, &format!("package :{key}")))
453 .collect::<Result<Vec<_>, _>>()?;
454 values.sort();
455 values.dedup();
456 Ok(values)
457}
458
459fn sequence<'a>(form: &'a Form, label: &str) -> Result<&'a Vec<Form>, String> {
460 match form {
461 Form::Vector(values) | Form::List(values) => Ok(values),
462 _ => Err(format!("{label} must be a vector or list")),
463 }
464}
465
466fn profile_scalar(form: &Form, label: &str) -> Result<String, String> {
467 match form {
468 Form::String(value) | Form::Symbol(value) | Form::Keyword(value) if !value.is_empty() => {
469 Ok(value.clone())
470 }
471 _ => Err(format!(
472 "{label} must be a non-empty string, symbol, or keyword"
473 )),
474 }
475}
476
477fn selector_matches(selector: &PackageSelector, namespace: &str) -> bool {
478 let within = |root: &str| namespace == root || namespace.starts_with(&format!("{root}."));
479 match &selector.mode {
480 PackageSelectorMode::Complete => within(&selector.namespace),
481 PackageSelectorMode::Base => {
482 namespace == selector.namespace
483 || namespace.starts_with(&format!("{}.base.", selector.namespace))
484 }
485 PackageSelectorMode::Exclude(excluded) => {
486 within(&selector.namespace) && !excluded.iter().any(|root| within(root))
487 }
488 }
489}
490
491fn map<'a>(form: &'a Form, message: &str) -> Result<&'a Vec<(Form, Form)>, String> {
492 match form {
493 Form::Map(entries) => Ok(entries),
494 _ => Err(message.into()),
495 }
496}
497fn lookup<'a>(entries: &'a [(Form, Form)], key: &str) -> Option<&'a Form> {
498 entries.iter().find_map(|(candidate, value)| {
499 matches!(candidate, Form::Keyword(name) if name == key).then_some(value)
500 })
501}
502fn lookup_any<'a>(entries: &'a [(Form, Form)], keys: &[&str]) -> Option<&'a Form> {
503 keys.iter().find_map(|key| lookup(entries, key))
504}
505fn required<'a>(entries: &'a [(Form, Form)], key: &str) -> Result<&'a Form, String> {
506 lookup(entries, key).ok_or_else(|| format!("locked package is missing :{key}"))
507}
508fn string(form: &Form, label: &str) -> Result<String, String> {
509 match form {
510 Form::String(value) => Ok(value.clone()),
511 _ => Err(format!("{label} must be a string")),
512 }
513}
514fn scalar(form: &Form, label: &str) -> Result<String, String> {
515 match form {
516 Form::String(value) | Form::Symbol(value) => Ok(value.clone()),
517 _ => Err(format!("{label} must be a string or symbol")),
518 }
519}
520fn symbols(form: &Form, label: &str) -> Result<Vec<String>, String> {
521 let Form::Vector(values) = form else {
522 return Err(format!("{label} must be a vector"));
523 };
524 let mut output = values
525 .iter()
526 .map(|value| scalar(value, label))
527 .collect::<Result<Vec<_>, _>>()?;
528 output.sort();
529 output.dedup();
530 Ok(output)
531}
532fn map_keys(form: &Form, label: &str) -> Result<Vec<String>, String> {
533 let entries = map(form, &format!("{label} must be a map"))?;
534 let mut output = entries
535 .iter()
536 .map(|(key, _)| scalar(key, label))
537 .collect::<Result<Vec<_>, _>>()?;
538 output.sort();
539 output.dedup();
540 Ok(output)
541}
542fn validate_sha256(value: &str) -> Result<(), String> {
543 let value = value.strip_prefix("sha256:").unwrap_or(value);
544 if value.len() == 64 && value.chars().all(|value| value.is_ascii_hexdigit()) {
545 Ok(())
546 } else {
547 Err("locked package :archive-sha256 must be SHA-256".into())
548 }
549}
550fn validate_commit(value: &str, label: &str) -> Result<(), String> {
551 if value.len() == 40
552 && value
553 .chars()
554 .all(|value| value.is_ascii_hexdigit() && !value.is_ascii_uppercase())
555 {
556 Ok(())
557 } else {
558 Err(format!(
559 "locked package :{label} must be a lowercase 40-character Git commit"
560 ))
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn reads_exact_lock_catalog_and_rejects_namespace_conflicts() {
570 let digest = format!("sha256:{}", "a".repeat(64));
571 let commit = "b".repeat(40);
572 let source = format!("{{:lock/format \"0.0.0-alpha\" :packages {{\"hara:demo/base\" {{:version \"1.0.0\" :tap \"hara\" :registry-commit \"{commit}\" :identity-revision \"{commit}\" :archive-sha256 \"{digest}\" :namespaces [demo.base]}} \"hara:demo/core\" {{:version \"1.2.3\" :tap \"hara\" :registry-commit \"{commit}\" :identity-revision \"{commit}\" :archive-sha256 \"{digest}\" :namespaces [demo.core demo.util] :dependencies {{\"hara:demo/base\" \"1.0.0\"}}}}}}}}");
573 let catalog = catalog_from_lock(&source).unwrap();
574 assert_eq!(catalog[1].namespaces, vec!["demo.core", "demo.util"]);
575 assert_eq!(catalog[1].dependencies, vec!["hara:demo/base"]);
576 }
577
578 #[test]
579 fn accepts_package_name_aliases_and_rejects_duplicate_semantic_names() {
580 let digest = format!("sha256:{}", "a".repeat(64));
581 let commit = "b".repeat(40);
582 let source = format!(
583 "{{:lock/format \"0.0.0-alpha\" :packages {{\"hara:demo/one\" {{:package/name \"demo.shared\" :version \"1.0.0\" :tap \"hara\" :registry-commit \"{commit}\" :identity-revision \"{commit}\" :archive-sha256 \"{digest}\" :namespaces [demo.one]}} \"hara:demo/two\" {{:name \"demo.shared\" :version \"1.0.0\" :tap \"hara\" :registry-commit \"{commit}\" :identity-revision \"{commit}\" :archive-sha256 \"{digest}\" :namespaces [demo.two]}}}}}}"
584 );
585 let error = catalog_from_lock(&source).unwrap_err();
586 assert!(error.contains("package/name-conflict"), "{error}");
587 }
588
589 #[test]
590 fn reads_foundation_selectors_and_expands_semantic_packages() {
591 let source = r#"{
592 xyz.zcaudate/code.test {:description "tests" :include [[code.test :complete]]}
593 lang.model.v1.postgres {:include [[lang.model.v1.spec-postgres :complete]
594 [postgres.core :complete]
595 [postgres.typed :complete]
596 [postgres.gen :complete]]
597 :dependencies [lang.base]}
598 lang.base {:include [[lang.base :complete]]}
599 }"#;
600 let definitions = definitions_from_packages_edn(source).unwrap();
601 assert_eq!(definitions[0].name, "lang.base");
602 let postgres = definitions
603 .iter()
604 .find(|definition| definition.name == "lang.model.v1.postgres")
605 .unwrap();
606 let available = vec![
607 "lang.model.v1.spec-postgres".into(),
608 "lang.model.v1.spec-postgres.deftype.common".into(),
609 "postgres.core".into(),
610 "postgres.core.graph".into(),
611 "postgres.typed".into(),
612 "postgres.gen.rpc".into(),
613 "db.postgres".into(),
614 ];
615 assert_eq!(
616 package_namespaces(postgres, &available),
617 vec![
618 "lang.model.v1.spec-postgres".to_owned(),
619 "lang.model.v1.spec-postgres.deftype.common".to_owned(),
620 "postgres.core".to_owned(),
621 "postgres.core.graph".to_owned(),
622 "postgres.gen.rpc".to_owned(),
623 "postgres.typed".to_owned(),
624 ]
625 );
626 validate_package_definitions(&definitions, &available).unwrap();
627 assert_eq!(
628 package_dependency_order(&definitions, &["lang.model.v1.postgres".to_owned()]).unwrap(),
629 vec!["lang.base", "lang.model.v1.postgres"]
630 );
631 }
632
633 #[test]
634 fn foundation_base_and_exclude_selectors_follow_namespace_boundaries() {
635 let source = r#"{
636 demo {:include [[demo :base]]}
637 other {:include [[other :exclude [other.internal]]]}
638 }"#;
639 let definitions = definitions_from_packages_edn(source).unwrap();
640 let available = vec![
641 "demo".into(),
642 "demo.base".into(),
643 "demo.base.value".into(),
644 "demo.extra".into(),
645 "other".into(),
646 "other.internal".into(),
647 "other.internal.deep".into(),
648 "other.public".into(),
649 ];
650 assert_eq!(
651 package_namespaces(&definitions[0], &available),
652 vec!["demo".to_owned(), "demo.base.value".to_owned()]
653 );
654 assert_eq!(
655 package_namespaces(&definitions[1], &available),
656 vec!["other".to_owned(), "other.public".to_owned()]
657 );
658 }
659
660 #[test]
661 fn rejects_overlapping_semantic_package_ownership_and_cycles() {
662 let source = r#"{
663 a {:include [[demo :complete]] :dependencies [b]}
664 b {:include [[other :complete]] :dependencies [a]}
665 c {:include [[demo.child :complete]]}
666 }"#;
667 let definitions = definitions_from_packages_edn(source).unwrap();
668 let available = vec!["demo".into(), "demo.child".into(), "other".into()];
669 let error = validate_package_definitions(&definitions, &available).unwrap_err();
670 assert!(error.contains("package/namespace-overlap"), "{error}");
671 let error = package_dependency_order(&definitions, &["a".into()]).unwrap_err();
672 assert!(error.contains("package/dependency-cycle"), "{error}");
673 }
674
675 #[test]
676 fn resolves_unique_tap_suffixes_but_rejects_ambiguous_dependencies() {
677 let source = r#"{
678 xyz.zcaudate/base {:include [[demo.base :complete]]}
679 xyz.zcaudate/app {:include [[demo.app :complete]] :dependencies [base]}
680 }"#;
681 let definitions = definitions_from_packages_edn(source).unwrap();
682 assert_eq!(
683 package_dependency_order(&definitions, &["app".into()]).unwrap(),
684 vec!["xyz.zcaudate/base", "xyz.zcaudate/app"]
685 );
686
687 let source = r#"{
688 one/base {:include [[demo.one :complete]]}
689 two/base {:include [[demo.two :complete]]}
690 app {:include [[demo.app :complete]] :dependencies [base]}
691 }"#;
692 let definitions = definitions_from_packages_edn(source).unwrap();
693 let error = package_dependency_order(&definitions, &["app".into()]).unwrap_err();
694 assert!(error.contains("package/ambiguous-dependency"), "{error}");
695 }
696}