forge/manifest_ledger/
mod.rs1mod collect;
6mod hooks;
7mod ranking;
8mod report;
9mod semantics;
10mod shapes;
11mod types;
12
13use std::collections::BTreeMap;
14
15use anyhow::{Context, Result};
16use clap::Args;
17use wowlab_engine_adapter_data::LocalCsvResolver;
18use wowlab_engine_ports::DataResolver;
19use wowlab_fs::path::{Path, PathBuf};
20use wowlab_manifest_schema::ManifestRepository;
21
22use self::types::{BehaviorShapeSummary, LedgerFormat, ManifestLedger};
23use crate::constants::{default_data_dir, engine_dir};
24
25#[derive(Args, Debug)]
26pub(crate) struct ManifestLedgerArgs {
27 #[arg(long)]
29 manifests: Option<PathBuf>,
30
31 #[arg(long)]
33 hooks: Option<PathBuf>,
34
35 #[arg(long)]
37 data_dir: Option<PathBuf>,
38
39 #[arg(long, value_enum, default_value_t = LedgerFormat::Markdown)]
41 format: LedgerFormat,
42
43 #[arg(long)]
45 output: Option<PathBuf>,
46}
47
48pub(crate) fn run(args: &ManifestLedgerArgs) -> Result<()> {
49 let manifests = args
50 .manifests
51 .clone()
52 .unwrap_or_else(|| engine_dir().join("manifests"));
53 let hooks = args.hooks.clone().unwrap_or_else(default_hooks_dir);
54 let data_dir = args
55 .data_dir
56 .clone()
57 .unwrap_or_else(|| PathBuf::from(default_data_dir()));
58 let data_dir = data_dir.to_string_lossy();
59 let resolver = LocalCsvResolver::new(data_dir.as_ref());
60 let runtime = tokio::runtime::Runtime::new().context("failed to create tokio runtime")?;
61 let ledger = runtime.block_on(build_ledger(&manifests, &hooks, &resolver))?;
62 let rendered = report::render(&ledger, args.format)?;
63
64 report::emit(&rendered, args.output.as_deref())
65}
66
67async fn build_ledger(
68 manifests: &Path,
69 hooks_dir: &Path,
70 resolver: &impl DataResolver,
71) -> Result<ManifestLedger> {
72 let repository = ManifestRepository::new(manifests);
73 let spec_paths = repository.spec_paths()?;
74 let mut rows = Vec::new();
75 let mut shape_observations = BTreeMap::new();
76 let mut shape_specs = BTreeMap::new();
77
78 for path in &spec_paths {
79 let slug = repository.spec_slug(path)?;
80 let manifest = repository
81 .load_spec(path)
82 .with_context(|| format!("failed to load {}", path.display()))?;
83
84 for (shape, observations) in shapes::spec_counts(&manifest) {
85 *shape_observations.entry(shape).or_default() += observations;
86
87 if observations != 0 {
88 *shape_specs.entry(shape).or_default() += 1;
89 }
90
91 tokio::task::yield_now().await;
92 }
93
94 rows.extend(collect::spec_rows(&slug, path, &manifest));
95 hooks::replace_hook_placeholders(&mut rows, &slug, hooks_dir, &manifest)?;
96 tokio::task::yield_now().await;
97 }
98
99 let items = repository.load_items()?;
100 let items_path = repository.root().join("items.toml");
101
102 for (name, item) in &items.items {
103 rows.extend(collect::item_rows(name, &items_path, item));
104 hooks::replace_item_hook_placeholders(&mut rows, name, hooks_dir, item)?;
105 tokio::task::yield_now().await;
106 }
107
108 hooks::collect_imperative_proc_registrations(&mut rows, hooks_dir)?;
109 semantics::attach(&mut rows, resolver).await;
110 ranking::rank_and_sort(&mut rows);
111 let category_totals = ranking::total_by(&rows, |row| row.category.as_str());
112 let disposition_totals = ranking::total_by(&rows, |row| row.disposition.as_str());
113 let behavior_shapes = shape_observations
114 .into_iter()
115 .map(|(shape, observations)| BehaviorShapeSummary {
116 shape: shape.to_string(),
117 observations,
118 specs: shape_specs.get(shape).copied().unwrap_or_default(),
119 })
120 .collect();
121
122 Ok(ManifestLedger {
123 composed_specs: spec_paths.len(),
124 items: items.items.len(),
125 behavior_shapes,
126 category_totals,
127 disposition_totals,
128 rows,
129 })
130}
131
132fn default_hooks_dir() -> PathBuf {
133 engine_dir()
134 .parent()
135 .expect("engine crate has crates parent")
136 .join("engine-content/src/hooks")
137}
138
139#[cfg(test)]
140mod aura_tests;
141
142#[cfg(test)]
143mod periodic_tests;
144
145#[cfg(test)]
146mod tests;