Skip to main content

forge/manifest_ledger/
mod.rs

1// #t(file: rust_alloc_in_loop) the offline report collector owns deterministic row data.
2
3//! Operation-level inventory of composed manifest and hand-written hook behavior.
4
5mod 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    /// Path to the engine manifest repository.
28    #[arg(long)]
29    manifests: Option<PathBuf>,
30
31    /// Path to hand-written engine-content hooks.
32    #[arg(long)]
33    hooks: Option<PathBuf>,
34
35    /// Override the local `WoW` CSV data checkout.
36    #[arg(long)]
37    data_dir: Option<PathBuf>,
38
39    /// Output format.
40    #[arg(long, value_enum, default_value_t = LedgerFormat::Markdown)]
41    format: LedgerFormat,
42
43    /// Write the rendered report to a file instead of stdout.
44    #[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;