gitronics 0.5.25__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {gitronics-0.5.25 → gitronics-0.6.0}/Cargo.lock +20 -7
  2. {gitronics-0.5.25 → gitronics-0.6.0}/Cargo.toml +2 -2
  3. {gitronics-0.5.25 → gitronics-0.6.0}/PKG-INFO +2 -2
  4. {gitronics-0.5.25 → gitronics-0.6.0}/pyproject.toml +1 -1
  5. {gitronics-0.5.25 → gitronics-0.6.0}/python/gitronics/__init__.py +2 -1
  6. {gitronics-0.5.25 → gitronics-0.6.0}/python/gitronics/gitronics.pyi +16 -0
  7. {gitronics-0.5.25 → gitronics-0.6.0}/src/build_model.rs +212 -185
  8. gitronics-0.6.0/src/migrate_model.rs +193 -0
  9. {gitronics-0.5.25 → gitronics-0.6.0}/src/project_manager/load_metadata.rs +7 -6
  10. gitronics-0.6.0/src/project_manager/load_project_files.rs +104 -0
  11. {gitronics-0.5.25 → gitronics-0.6.0}/src/project_manager.rs +1 -1
  12. {gitronics-0.5.25 → gitronics-0.6.0}/src/python.rs +14 -1
  13. {gitronics-0.5.25 → gitronics-0.6.0}/src/types.rs +0 -43
  14. {gitronics-0.5.25 → gitronics-0.6.0}/src/utils.rs +65 -0
  15. {gitronics-0.5.25 → gitronics-0.6.0}/tests/test_example_project.rs +39 -0
  16. gitronics-0.5.25/src/migrate_model.rs +0 -235
  17. gitronics-0.5.25/src/project_manager/load_project_files.rs +0 -148
  18. {gitronics-0.5.25 → gitronics-0.6.0}/.github/workflows/ci.yml +0 -0
  19. {gitronics-0.5.25 → gitronics-0.6.0}/.github/workflows/docs.yml +0 -0
  20. {gitronics-0.5.25 → gitronics-0.6.0}/.github/workflows/release.yml +0 -0
  21. {gitronics-0.5.25 → gitronics-0.6.0}/.gitignore +0 -0
  22. {gitronics-0.5.25 → gitronics-0.6.0}/.vscode/settings.json +0 -0
  23. {gitronics-0.5.25 → gitronics-0.6.0}/LICENSE +0 -0
  24. {gitronics-0.5.25 → gitronics-0.6.0}/README.md +0 -0
  25. {gitronics-0.5.25 → gitronics-0.6.0}/docs/assets/logo.png +0 -0
  26. {gitronics-0.5.25 → gitronics-0.6.0}/docs/best_practices.md +0 -0
  27. {gitronics-0.5.25 → gitronics-0.6.0}/docs/examples.md +0 -0
  28. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/assessments.md +0 -0
  29. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/building_a_model.md +0 -0
  30. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/card_id_numbers.md +0 -0
  31. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/concepts.md +0 -0
  32. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/configuration_file.md +0 -0
  33. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/file_types.md +0 -0
  34. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/migrating.md +0 -0
  35. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/new_project.md +0 -0
  36. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started/project_structure.md +0 -0
  37. {gitronics-0.5.25 → gitronics-0.6.0}/docs/getting_started.md +0 -0
  38. {gitronics-0.5.25 → gitronics-0.6.0}/docs/hooks/version_from_git.py +0 -0
  39. {gitronics-0.5.25 → gitronics-0.6.0}/docs/index.md +0 -0
  40. {gitronics-0.5.25 → gitronics-0.6.0}/docs/installation.md +0 -0
  41. {gitronics-0.5.25 → gitronics-0.6.0}/docs/requirements.txt +0 -0
  42. {gitronics-0.5.25 → gitronics-0.6.0}/docs/usage/build.md +0 -0
  43. {gitronics-0.5.25 → gitronics-0.6.0}/docs/usage/migrate.md +0 -0
  44. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/assessment_specific/filler_model_3.mcnp +0 -0
  45. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/assessment_specific/filler_model_3.metadata +0 -0
  46. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/assessment_specific/small_override.yaml +0 -0
  47. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/configurations/valid_configuration.yaml +0 -0
  48. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/output/.gitignore +0 -0
  49. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/data_cards/fine_mesh.tally +0 -0
  50. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/data_cards/materials.mat +0 -0
  51. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/data_cards/my_transform.transform +0 -0
  52. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/data_cards/volumetric_source.source +0 -0
  53. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/envelope_structure.mcnp +0 -0
  54. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/envelope_structure.metadata +0 -0
  55. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/filler_models/filler_model_1.mcnp +0 -0
  56. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/filler_models/filler_model_1.metadata +0 -0
  57. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/filler_models/filler_model_2.mcnp +0 -0
  58. {gitronics-0.5.25 → gitronics-0.6.0}/example_project/reference_model/filler_models/filler_model_2.metadata +0 -0
  59. {gitronics-0.5.25 → gitronics-0.6.0}/mkdocs.yml +0 -0
  60. {gitronics-0.5.25 → gitronics-0.6.0}/python/gitronics/__main__.py +0 -0
  61. {gitronics-0.5.25 → gitronics-0.6.0}/python/tests/test_build_model.py +0 -0
  62. {gitronics-0.5.25 → gitronics-0.6.0}/python/tests/test_cli_works.py +0 -0
  63. {gitronics-0.5.25 → gitronics-0.6.0}/resources/simple_model.mcnp +0 -0
  64. {gitronics-0.5.25 → gitronics-0.6.0}/src/build_report.rs +0 -0
  65. {gitronics-0.5.25 → gitronics-0.6.0}/src/cli.rs +0 -0
  66. {gitronics-0.5.25 → gitronics-0.6.0}/src/lib.rs +0 -0
  67. {gitronics-0.5.25 → gitronics-0.6.0}/src/main.rs +0 -0
  68. {gitronics-0.5.25 → gitronics-0.6.0}/src/model_config.rs +0 -0
  69. {gitronics-0.5.25 → gitronics-0.6.0}/src/project_manager/load_model_config.rs +0 -0
  70. {gitronics-0.5.25 → gitronics-0.6.0}/src/report.css +0 -0
@@ -390,7 +390,7 @@ dependencies = [
390
390
 
391
391
  [[package]]
392
392
  name = "gitronics"
393
- version = "0.5.25"
393
+ version = "0.6.0"
394
394
  dependencies = [
395
395
  "chrono",
396
396
  "clap",
@@ -636,14 +636,21 @@ dependencies = [
636
636
 
637
637
  [[package]]
638
638
  name = "migjorn"
639
- version = "0.1.8"
639
+ version = "0.2.2"
640
640
  source = "registry+https://github.com/rust-lang/crates.io-index"
641
- checksum = "d3f2fe960921f8c049fc7ac5536e96cba576e902cf040c258e8092eabb9eeaaf"
641
+ checksum = "ce11924d4ef1f0496f0be14e68860ab11bbc0b63222057d03bc92b22b4084f3d"
642
642
  dependencies = [
643
- "clap",
644
- "memchr",
645
- "rayon",
646
- "thiserror",
643
+ "migjorn-syntax",
644
+ "rustc-hash",
645
+ ]
646
+
647
+ [[package]]
648
+ name = "migjorn-syntax"
649
+ version = "0.2.2"
650
+ source = "registry+https://github.com/rust-lang/crates.io-index"
651
+ checksum = "42d3806f43fee8380cfaa0e26e9fee3a2dbece142e5314b6c7f9a0babf03fe05"
652
+ dependencies = [
653
+ "rustc-hash",
647
654
  ]
648
655
 
649
656
  [[package]]
@@ -858,6 +865,12 @@ version = "0.8.10"
858
865
  source = "registry+https://github.com/rust-lang/crates.io-index"
859
866
  checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
860
867
 
868
+ [[package]]
869
+ name = "rustc-hash"
870
+ version = "2.1.3"
871
+ source = "registry+https://github.com/rust-lang/crates.io-index"
872
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
873
+
861
874
  [[package]]
862
875
  name = "rustix"
863
876
  version = "1.1.4"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "gitronics"
3
- version = "0.5.25"
3
+ version = "0.6.0"
4
4
  edition = "2024"
5
5
  description = "Build MCNP neutronics models from modular components"
6
6
  license = "EUPL-1.2"
@@ -16,7 +16,7 @@ crate-type = ["cdylib", "rlib"]
16
16
 
17
17
  [dependencies]
18
18
  pyo3 = { version = "0.23", features = ["extension-module", "abi3-py39"] }
19
- migjorn = { version = "0.1.8" }
19
+ migjorn = { version = "0.2.2" }
20
20
  walkdir = "2.5.0"
21
21
  serde = { version = "1.0.228", features = ["derive"] }
22
22
  serde-saphyr = "0.0.26"
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitronics
3
- Version: 0.5.25
3
+ Version: 0.6.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
7
7
  License-File: LICENSE
8
8
  Summary: Build MCNP neutronics models from modular components
9
- Requires-Python: >=3.8
9
+ Requires-Python: >=3.9
@@ -6,7 +6,7 @@ build-backend = "maturin"
6
6
  name = "gitronics"
7
7
  dynamic = ["version"]
8
8
  description = "Build MCNP neutronics models from modular components"
9
- requires-python = ">=3.8"
9
+ requires-python = ">=3.9"
10
10
  classifiers = [
11
11
  "Programming Language :: Rust",
12
12
  "Programming Language :: Python :: Implementation :: CPython",
@@ -11,9 +11,10 @@ from __future__ import annotations
11
11
  import sys
12
12
 
13
13
  from .gitronics import py_build_model as build_model # type: ignore[import]
14
+ from .gitronics import py_migrate_model as migrate_model # type: ignore[import]
14
15
  from .gitronics import run # type: ignore[import]
15
16
 
16
- __all__ = ["build_model", "run"]
17
+ __all__ = ["build_model", "migrate_model", "run"]
17
18
 
18
19
 
19
20
  def _cli_main() -> None:
@@ -43,3 +43,19 @@ def py_build_model(config_path: Path | str, output_path: Path | str) -> None:
43
43
  )
44
44
  """
45
45
  ...
46
+
47
+ def py_migrate_model(mcnp_input: Path | str, output_path: Path | str) -> None:
48
+ """Migrate a monolithic MCNP model into a new gitronics project.
49
+
50
+ Splits the deck at *mcnp_input* into an envelope structure, per-universe
51
+ filler models, and a data-cards file, and writes a baseline configuration
52
+ that rebuilds the original model with :func:`build_model`.
53
+
54
+ Args:
55
+ mcnp_input: Path to the monolithic MCNP input file.
56
+ output_path: Directory where the new project will be created.
57
+
58
+ Raises:
59
+ RuntimeError: If any step of the migration fails.
60
+ """
61
+ ...
@@ -1,15 +1,15 @@
1
1
  use crate::build_report::{BuildReport, EnvelopeEntry, FillerEntry};
2
2
  use crate::project_manager::ProjectManager;
3
- use crate::types::{CellId, EnvelopeName, FillerName, UniverseId};
3
+ use crate::types::{EnvelopeName, FillerName, UniverseId};
4
4
  use crate::utils::GitronicsError;
5
5
 
6
6
  use git2::Repository;
7
7
  use log::{info, warn};
8
- use migjorn::{Card, CellCard, CellParam, DataCard, Model, ParamType};
8
+ use migjorn::Model;
9
9
  use regex::Regex;
10
10
  use std::collections::HashSet;
11
+ use std::fs;
11
12
  use std::{collections::HashMap, path::Path, sync::LazyLock};
12
- use std::{env, fs};
13
13
 
14
14
  pub fn build_model(config_path: &Path, output_path: &Path) -> Result<(), GitronicsError> {
15
15
  info!(
@@ -19,34 +19,35 @@ pub fn build_model(config_path: &Path, output_path: &Path) -> Result<(), Gitroni
19
19
 
20
20
  let mut project_manager = ProjectManager::new(config_path, output_path)?;
21
21
 
22
- // Load all files
22
+ // Load all files.
23
23
  let mut envelope_structure = project_manager.load_envelope_structure()?;
24
24
  let unordered_fillers = project_manager.load_fillers()?;
25
- let unordered_transforms = project_manager.load_transforms()?;
26
- let unordered_materials = project_manager.load_materials()?;
27
- let unordered_tallies = project_manager.load_tallies()?;
25
+ let transforms = project_manager.load_transforms()?;
26
+ let materials = project_manager.load_materials()?;
27
+ let tallies = project_manager.load_tallies()?;
28
28
  let source = project_manager.load_source()?;
29
29
 
30
- // Reorder by IDs
31
- let fillers = order_models_by_cell_id(unordered_fillers)?;
32
- let transforms = order_data_cards_groups_by_id(unordered_transforms);
33
- let materials = order_data_cards_groups_by_id(unordered_materials);
34
- let tallies = order_data_cards_groups_by_id(unordered_tallies);
30
+ // Order fillers by their first cell id for deterministic output.
31
+ let fillers = order_fillers_by_cell_id(unordered_fillers)?;
35
32
 
36
- // Load metadata for each filler and cache it in the ProjectManager
37
- project_manager.load_metadata_for_fillers(&fillers)?;
33
+ // Load metadata for each filler and cache it in the ProjectManager.
34
+ project_manager.load_metadata_for_fillers(fillers.iter().map(|(name, _)| name))?;
38
35
 
39
- // Get the filler universe ID of every filler model
36
+ // Universe id of every filler (read from its first cell's `u=`).
40
37
  let universe_ids: HashMap<FillerName, UniverseId> = fillers
41
38
  .iter()
42
- .map(|filler| Ok((FillerName::from(filler), get_universe_id_of_model(filler)?)))
39
+ .map(|(name, model)| {
40
+ let universe = filler_universe(model)
41
+ .ok_or_else(|| GitronicsError::FirstCellWithoutUniverseID(name.clone()))?;
42
+ Ok((name.clone(), universe))
43
+ })
43
44
  .collect::<Result<HashMap<_, _>, GitronicsError>>()?;
44
45
 
45
- // Adapt envelope cells with the FILL cards
46
+ // Insert FILL cards into the placeholder envelope cells.
46
47
  info!("Adapting envelope structure with FILL cards");
47
48
  add_fill_cards_to_envelopes(&project_manager, &universe_ids, &mut envelope_structure)?;
48
49
 
49
- // Collect build-report data before fillers are moved into the composed model
50
+ // Collect build-report data before the models are consumed by composition.
50
51
  let report = collect_build_report(
51
52
  config_path,
52
53
  &project_manager,
@@ -55,31 +56,51 @@ pub fn build_model(config_path: &Path, output_path: &Path) -> Result<(), Gitroni
55
56
  &universe_ids,
56
57
  )?;
57
58
 
58
- // Compose model
59
+ // Compose: drop the ignored data blocks, then merge every filler's geometry
60
+ // into the envelope structure (collision-checked against the disjoint-range
61
+ // convention).
59
62
  info!("Composing model");
60
- fillers.into_iter().for_each(|filler| {
61
- envelope_structure.cells.extend(filler.cells);
62
- envelope_structure.surfaces.extend(filler.surfaces);
63
- });
64
- envelope_structure.data_cards.extend(transforms);
65
- envelope_structure.data_cards.extend(materials);
66
- envelope_structure.data_cards.extend(tallies);
67
- envelope_structure.data_cards.extend(source);
68
-
69
- // Perform validation checks
70
- info!("Performing validation checks on the assembled model");
63
+ envelope_structure.clear_data_cards();
64
+ let mut filler_models = fillers;
65
+ for (_, model) in filler_models.iter_mut() {
66
+ model.clear_data_cards();
67
+ }
68
+ let filler_refs: Vec<&Model> = filler_models.iter().map(|(_, model)| model).collect();
71
69
  envelope_structure
72
- .validation_checks()
73
- .map_err(|e| GitronicsError::ValidationError(e.to_string()))?;
70
+ .merge(&filler_refs)
71
+ .map_err(|conflicts| GitronicsError::MergeConflicts(format_conflicts(&conflicts)))?;
72
+
73
+ // Append the configured data cards to the data block.
74
+ let data_text = [transforms, materials, tallies, source]
75
+ .into_iter()
76
+ .filter(|s| !s.is_empty())
77
+ .collect::<Vec<_>>()
78
+ .join("\n");
79
+ let mut assembled_source = envelope_structure.to_source();
80
+ if !data_text.is_empty() {
81
+ if !assembled_source.ends_with('\n') {
82
+ assembled_source.push('\n');
83
+ }
84
+ assembled_source.push_str(&data_text);
85
+ assembled_source.push('\n');
86
+ }
87
+ let assembled_model = Model::parse(assembled_source);
88
+
89
+ // Validate the assembled model.
90
+ info!("Performing validation checks on the assembled model");
91
+ let problems = assembled_model.validate();
92
+ if !problems.is_empty() {
93
+ return Err(GitronicsError::ValidationError(problems.join("\n")));
94
+ }
74
95
 
75
- // Write model
96
+ // Write the assembled model with the provenance banner.
76
97
  info!("Writing assembled model to file");
77
98
  let assembled_path = project_manager.output_path().join("assembled.mcnp");
78
- write_assembled_header(&mut envelope_structure, config_path)?;
79
- envelope_structure.write_to_file(&assembled_path)?;
99
+ let final_source = insert_banner(&assembled_model.to_source(), config_path);
100
+ fs::write(&assembled_path, final_source)?;
80
101
  fs::write(output_path.join(".gitignore"), "*\n")?;
81
102
 
82
- // Write build report
103
+ // Write the HTML build report.
83
104
  info!("Writing HTML build report");
84
105
  let report_path = project_manager.output_path().join("build_report.html");
85
106
  fs::write(&report_path, report.generate_html())?;
@@ -94,17 +115,134 @@ pub fn build_model(config_path: &Path, output_path: &Path) -> Result<(), Gitroni
94
115
  static ENVELOPE_RE: LazyLock<Regex> =
95
116
  LazyLock::new(|| Regex::new(r"\$\s*@env:\s*([[:alnum:]_.-]+)\s*").unwrap());
96
117
 
118
+ /// The directory that contains the configuration file — the anchor for git
119
+ /// repository discovery (the project, not the process's working directory).
120
+ fn project_dir(config_path: &Path) -> &Path {
121
+ config_path.parent().unwrap_or(Path::new("."))
122
+ }
123
+
124
+ /// The universe id declared by a filler's first cell (`u=`), if any.
125
+ fn filler_universe(model: &Model) -> Option<UniverseId> {
126
+ let first = model.cells().next()?;
127
+ model
128
+ .cell_universe(first.card_index)
129
+ .map(|u| UniverseId::new(u as u32))
130
+ }
131
+
132
+ /// Order fillers by the id of their first cell, erroring on a filler with no
133
+ /// cells.
134
+ fn order_fillers_by_cell_id(
135
+ fillers: Vec<(FillerName, Model)>,
136
+ ) -> Result<Vec<(FillerName, Model)>, GitronicsError> {
137
+ let mut keyed: Vec<(i64, FillerName, Model)> = fillers
138
+ .into_iter()
139
+ .map(|(name, model)| {
140
+ let cell_id = model
141
+ .cells()
142
+ .next()
143
+ .map(|c| c.id)
144
+ .ok_or_else(|| GitronicsError::NoCellID(name.clone()))?;
145
+ Ok((cell_id, name, model))
146
+ })
147
+ .collect::<Result<Vec<_>, GitronicsError>>()?;
148
+ keyed.sort_by_key(|(cell_id, _, _)| *cell_id);
149
+ Ok(keyed
150
+ .into_iter()
151
+ .map(|(_, name, model)| (name, model))
152
+ .collect())
153
+ }
154
+
155
+ fn add_fill_cards_to_envelopes(
156
+ project_manager: &ProjectManager,
157
+ universe_ids: &HashMap<FillerName, UniverseId>,
158
+ envelope_structure: &mut Model,
159
+ ) -> Result<(), GitronicsError> {
160
+ let mut missing_envelopes_in_file: HashSet<EnvelopeName> =
161
+ project_manager.envelopes_in_config().cloned().collect();
162
+
163
+ // Collect cell card indices up front: FILL insertion is a token splice that
164
+ // leaves indices stable, so we can read then mutate by the same index.
165
+ let cell_indices: Vec<usize> = envelope_structure.cells().map(|c| c.card_index).collect();
166
+
167
+ for card_index in cell_indices {
168
+ let original_text = envelope_structure.card_source(card_index);
169
+ let Some(caps) = ENVELOPE_RE.captures(&original_text) else {
170
+ continue;
171
+ };
172
+ let envelope_name =
173
+ EnvelopeName::new(caps.get(1).map(|m| m.as_str()).ok_or_else(|| {
174
+ GitronicsError::FailedToExtractEnvelopeName(
175
+ ENVELOPE_RE.to_string(),
176
+ original_text.clone(),
177
+ )
178
+ })?);
179
+
180
+ let Some(env_config) = project_manager.filler_by_envelope(&envelope_name) else {
181
+ warn!(
182
+ "The envelope `{envelope_name}` is not considered in the configuration file. \
183
+ It is better to explicitly set it as `{envelope_name}: null` if you want the \
184
+ envelope to not be filled with any model."
185
+ );
186
+ continue;
187
+ };
188
+
189
+ // We found the envelope in the file.
190
+ missing_envelopes_in_file.remove(&envelope_name);
191
+
192
+ // Envelope explicitly set to null in config: leave it unfilled.
193
+ let Some(filler_name) = env_config.as_ref() else {
194
+ continue;
195
+ };
196
+
197
+ let universe_id = universe_ids
198
+ .get(filler_name)
199
+ .ok_or_else(|| GitronicsError::FillerUniverseIdMissing(filler_name.clone()))?;
200
+
201
+ let transform = project_manager
202
+ .transformation(filler_name, &envelope_name)?
203
+ .unwrap_or("");
204
+ let fill_card_text = if let Some(transform_without_star) = transform.strip_prefix('*') {
205
+ format!("*fill={universe_id} {transform_without_star}")
206
+ } else {
207
+ format!("fill={universe_id} {transform}")
208
+ };
209
+
210
+ envelope_structure
211
+ .add_cell_param(card_index, fill_card_text.trim())
212
+ .map_err(|e| GitronicsError::InvalidFillCard(fill_card_text, e.to_string()))?;
213
+ }
214
+
215
+ if !missing_envelopes_in_file.is_empty() {
216
+ warn!(
217
+ "The following envelopes were defined in the configuration file but not found in the envelope structure file: {}. \
218
+ Please check that the `$ @env:envelope_name` pattern is satisfied.",
219
+ missing_envelopes_in_file
220
+ .into_iter()
221
+ .map(|e| e.to_string())
222
+ .collect::<Vec<_>>()
223
+ .join(", ")
224
+ );
225
+ }
226
+ Ok(())
227
+ }
228
+
97
229
  fn collect_build_report(
98
230
  config_path: &Path,
99
231
  project_manager: &ProjectManager,
100
232
  envelope_structure: &Model,
101
- fillers: &[Model],
233
+ fillers: &[(FillerName, Model)],
102
234
  universe_ids: &HashMap<FillerName, UniverseId>,
103
235
  ) -> Result<BuildReport, GitronicsError> {
104
- let total_cells =
105
- envelope_structure.cells.len() + fillers.iter().map(|f| f.cells.len()).sum::<usize>();
106
- let total_surfaces =
107
- envelope_structure.surfaces.len() + fillers.iter().map(|f| f.surfaces.len()).sum::<usize>();
236
+ let total_cells = envelope_structure.cells().count()
237
+ + fillers
238
+ .iter()
239
+ .map(|(_, m)| m.cells().count())
240
+ .sum::<usize>();
241
+ let total_surfaces = envelope_structure.surfaces().count()
242
+ + fillers
243
+ .iter()
244
+ .map(|(_, m)| m.surfaces().count())
245
+ .sum::<usize>();
108
246
 
109
247
  let envelope_entries: Vec<EnvelopeEntry> = project_manager
110
248
  .envelopes_in_config()
@@ -146,16 +284,15 @@ fn collect_build_report(
146
284
 
147
285
  let filler_entries: Vec<FillerEntry> = fillers
148
286
  .iter()
149
- .filter_map(|filler| {
150
- let name = FillerName::from(filler);
151
- let universe_id = *universe_ids.get(&name)?;
152
- let envelope_count = *filler_envelope_counts.get(&name).unwrap_or(&0);
287
+ .filter_map(|(name, model)| {
288
+ let universe_id = *universe_ids.get(name)?;
289
+ let envelope_count = *filler_envelope_counts.get(name).unwrap_or(&0);
153
290
  Some(FillerEntry {
154
291
  universe_id,
155
292
  envelope_count,
156
- cell_count: filler.cells.len(),
157
- surface_count: filler.surfaces.len(),
158
- name,
293
+ cell_count: model.cells().count(),
294
+ surface_count: model.surfaces().count(),
295
+ name: name.clone(),
159
296
  })
160
297
  })
161
298
  .collect();
@@ -163,7 +300,7 @@ fn collect_build_report(
163
300
  Ok(BuildReport {
164
301
  config_path: config_path.display().to_string(),
165
302
  gitronics_version: env!("CARGO_PKG_VERSION"),
166
- commit_hash: get_hash_of_project(),
303
+ commit_hash: get_hash_of_project(project_dir(config_path)),
167
304
  date_time: chrono::Utc::now()
168
305
  .format("%Y-%m-%d %H:%M:%S UTC")
169
306
  .to_string(),
@@ -190,153 +327,43 @@ fn collect_build_report(
190
327
  })
191
328
  }
192
329
 
193
- fn add_fill_cards_to_envelopes(
194
- project_manager: &ProjectManager,
195
- universe_ids: &HashMap<FillerName, UniverseId>,
196
- envelope_structure: &mut Model,
197
- ) -> Result<(), GitronicsError> {
198
- let mut missing_envelopes_in_file: HashSet<EnvelopeName> =
199
- project_manager.envelopes_in_config().cloned().collect();
200
-
201
- for cell in envelope_structure.cells.iter_mut() {
202
- let original_text = cell.original_text();
203
- let Some(caps) = ENVELOPE_RE.captures(original_text) else {
204
- continue;
205
- };
206
-
207
- let envelope_name =
208
- EnvelopeName::new(caps.get(1).map(|m| m.as_str()).ok_or_else(|| {
209
- GitronicsError::FailedToExtractEnvelopeName(
210
- ENVELOPE_RE.to_string(),
211
- original_text.to_string(),
212
- )
213
- })?);
214
-
215
- let Some(env_config) = project_manager.filler_by_envelope(&envelope_name) else {
216
- warn!(
217
- "The envelope `{envelope_name}` is not considered in the configuration file. \
218
- It is better to explicitly set it as `{envelope_name}: null` if you want the \
219
- envelope to not be filled with any model."
220
- );
221
- continue;
222
- };
223
-
224
- // Remove the envelope from the set of missing envelopes, as we have found it in the file
225
- missing_envelopes_in_file.remove(&envelope_name);
226
-
227
- // Envelope explicitly set to null in config, so we skip it
228
- let Some(filler_name) = env_config.as_ref() else {
229
- continue;
230
- };
231
-
232
- let universe_id = universe_ids
233
- .get(filler_name)
234
- .ok_or_else(|| GitronicsError::FillerUniverseIdMissing(filler_name.clone()))?;
235
-
236
- let transform = project_manager.transformation(filler_name, &envelope_name)?;
237
- let transform = transform.unwrap_or("");
238
-
239
- let fill_card_text = if let Some(transform_without_star) = transform.strip_prefix('*') {
240
- format!("*FILL={} {}", universe_id, transform_without_star)
241
- } else {
242
- format!("FILL={} {}", universe_id, transform)
243
- };
244
-
245
- cell.insert_param(
246
- cell.params().len(),
247
- CellParam::try_from(fill_card_text.clone())
248
- .map_err(|e| GitronicsError::InvalidFillCard(fill_card_text, e.to_string()))?,
249
- );
250
- }
251
-
252
- if !missing_envelopes_in_file.is_empty() {
253
- warn!(
254
- "The following envelopes were defined in the configuration file but not found in the envelope structure file: {}. \
255
- Please check that the `$ @env:envelope_name` pattern is satisfied.",
256
- missing_envelopes_in_file
257
- .into_iter()
258
- .map(|e| e.to_string())
259
- .collect::<Vec<_>>()
260
- .join(", ")
261
- );
262
- }
263
- Ok(())
264
- }
265
-
266
- fn order_models_by_cell_id(models: Vec<Model>) -> Result<Vec<Model>, GitronicsError> {
267
- let mut decorated: Vec<(CellId, Model)> = models
268
- .into_iter()
269
- .map(|filler| {
270
- let cell_id = filler
271
- .cells
272
- .first()
273
- .map(|cell| CellId::new(cell.cell_id()))
274
- .ok_or_else(|| GitronicsError::NoCellID(FillerName::from(&filler)))?;
275
- Ok((cell_id, filler))
276
- })
277
- .collect::<Result<Vec<_>, GitronicsError>>()?;
278
- decorated.sort_by_key(|(cell_id, _)| *cell_id);
279
- Ok(decorated.into_iter().map(|(_, filler)| filler).collect())
280
- }
281
-
282
- fn order_data_cards_groups_by_id(mut data_cards_groups: Vec<Vec<DataCard>>) -> Vec<DataCard> {
283
- data_cards_groups.sort_by_key(|data_cards| data_cards.first().map(|card| card.card_id()));
284
- data_cards_groups.into_iter().flatten().collect()
285
- }
286
-
287
- fn get_universe_id_of_model(model: &Model) -> Result<UniverseId, GitronicsError> {
288
- model
289
- .cells
290
- .first()
291
- .and_then(|cell| {
292
- cell.params()
293
- .iter()
294
- .find_map(|param| match param.param_type {
295
- ParamType::U(id) => Some(UniverseId::new(id)),
296
- _ => None,
297
- })
298
- })
299
- .ok_or_else(|| GitronicsError::FirstCellWithoutUniverseID(FillerName::from(model)))
330
+ /// Render merge conflicts as a human-readable, newline-separated list.
331
+ fn format_conflicts(conflicts: &[migjorn::MergeConflict]) -> String {
332
+ conflicts
333
+ .iter()
334
+ .map(|c| c.to_string())
335
+ .collect::<Vec<_>>()
336
+ .join("\n")
300
337
  }
301
338
 
302
- fn write_assembled_header(
303
- assembled_model: &mut Model,
304
- config_path: &Path,
305
- ) -> Result<(), GitronicsError> {
339
+ /// Insert the provenance banner as comment lines just after the model's title.
340
+ fn insert_banner(source: &str, config_path: &Path) -> String {
306
341
  let configuration = config_path.display().to_string();
307
342
  let date_time = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
308
343
  let gitronics_version = env!("CARGO_PKG_VERSION");
309
- let commit_hash = get_hash_of_project();
344
+ let commit_hash = get_hash_of_project(project_dir(config_path));
310
345
 
311
346
  let banner = format!(
312
- "C ============================================================
313
- C Built by gitronics v{gitronics_version}
314
- C Configuration : {configuration}
315
- C Git commit : {commit_hash}
316
- C Date / time : {date_time}
317
- C ============================================================"
347
+ "C ============================================================\n\
348
+ C Built by gitronics v{gitronics_version}\n\
349
+ C Configuration : {configuration}\n\
350
+ C Git commit : {commit_hash}\n\
351
+ C Date / time : {date_time}\n\
352
+ C ============================================================"
318
353
  );
319
- let new_card_text = format!(
320
- "{banner}\n{}",
321
- assembled_model
322
- .cells
323
- .first()
324
- .map(|c| c.original_text())
325
- .unwrap_or_default()
326
- );
327
-
328
- let banner_card = CellCard::try_from(new_card_text.as_str())
329
- .expect("Adding comments as header should not fail");
330
354
 
331
- // Replace the first cell (previously a comment placeholder) with the banner card
332
- assembled_model.cells[0] = banner_card;
333
- Ok(())
355
+ match source.split_once('\n') {
356
+ Some((title, rest)) => format!("{title}\n{banner}\n{rest}"),
357
+ None => format!("{source}\n{banner}\n"),
358
+ }
334
359
  }
335
360
 
336
- fn get_hash_of_project() -> String {
337
- let repo = env::current_dir()
338
- .ok()
339
- .and_then(|cwd| Repository::discover(cwd).ok());
361
+ /// Describe the git state of the repository that contains `start_dir` (the
362
+ /// project being built), not the process's current working directory — so the
363
+ /// recorded commit reflects what was actually assembled regardless of where the
364
+ /// binary was invoked from.
365
+ fn get_hash_of_project(start_dir: &Path) -> String {
366
+ let repo = Repository::discover(start_dir).ok();
340
367
  repo.as_ref()
341
368
  .and_then(|r| {
342
369
  let mut opts = git2::DescribeOptions::new();