nmarshall commited on
Commit
05e776e
·
1 Parent(s): 18d65b9

cli: preflight reports

Browse files
crates/nanobrush-cli/src/lib.rs CHANGED
@@ -1,4 +1,5 @@
1
  //! Headless CLI; source-preserving conversion with atomic destination publication.
 
2
  use mb3_dat::{Document, Limits, Version, World};
3
  use std::ffi::OsString;
4
  use std::fmt;
@@ -7,7 +8,7 @@ use std::io::{self, BufRead, BufReader, Write};
7
  use std::path::{Path, PathBuf};
8
  use std::sync::atomic::{AtomicU64, Ordering};
9
 
10
- const HELP: &str = "nanobrush inspect INPUT [--from auto|txt|dat] [LIMITS]\nnanobrush validate INPUT [--from auto|txt|dat] [LIMITS]\nnanobrush convert INPUT OUTPUT --to txt|dat-201501280|dat-201512200 [--from auto|txt|dat] [--force] [LIMITS]\nLIMITS: --max-bytes N --max-items N --max-string-bytes N --max-owned-bytes N\nUse -- before positional paths beginning with a dash. TXT input is UTF-8; output has a BOM.\nExit: 0 success/help, 1 invalid data or I/O failure, 2 usage error.\n";
11
  #[derive(Debug)]
12
  pub struct Error {
13
  pub usage: bool,
@@ -65,8 +66,13 @@ fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Option<Options>> {
65
  let command = command
66
  .into_string()
67
  .map_err(|_| usage("command must be UTF-8"))?;
68
- if !matches!(command.as_str(), "inspect" | "validate" | "convert") {
69
- return Err(usage("expected inspect, validate or convert; use --help"));
 
 
 
 
 
70
  }
71
  let mut options = Options {
72
  command,
@@ -150,7 +156,11 @@ fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Option<Options>> {
150
  options.paths.push(arg.into());
151
  }
152
  }
153
- if options.command == "convert" {
 
 
 
 
154
  if options.paths.len() != 2 || options.to.is_none() {
155
  return Err(usage("convert requires INPUT OUTPUT --to FORMAT"));
156
  }
@@ -287,6 +297,17 @@ pub fn run(args: impl IntoIterator<Item = OsString>, stdout: &mut dyn Write) ->
287
  };
288
  let input = load(&o.paths[0], o.from, &o.limits)?;
289
  match o.command.as_str() {
 
 
 
 
 
 
 
 
 
 
 
290
  "inspect" => {
291
  let w = &input.world;
292
  let planes: usize = w.brushes.iter().map(|b| b.planes.len()).sum();
 
1
  //! Headless CLI; source-preserving conversion with atomic destination publication.
2
+ pub mod preflight;
3
  use mb3_dat::{Document, Limits, Version, World};
4
  use std::ffi::OsString;
5
  use std::fmt;
 
8
  use std::path::{Path, PathBuf};
9
  use std::sync::atomic::{AtomicU64, Ordering};
10
 
11
+ const HELP: &str = "nanobrush preflight INPUT --to txt|dat-201501280|dat-201512200 [LIMITS]\nnanobrush inspect INPUT [--from auto|txt|dat] [LIMITS]\nnanobrush validate INPUT [--from auto|txt|dat] [LIMITS]\nnanobrush convert INPUT OUTPUT --to txt|dat-201501280|dat-201512200 [--from auto|txt|dat] [--force] [LIMITS]\nLIMITS: --max-bytes N --max-items N --max-string-bytes N --max-owned-bytes N\nUse -- before positional paths beginning with a dash. TXT input is UTF-8; output has a BOM.\nExit: 0 success/help, 1 invalid data or I/O failure, 2 usage error.\n";
12
  #[derive(Debug)]
13
  pub struct Error {
14
  pub usage: bool,
 
66
  let command = command
67
  .into_string()
68
  .map_err(|_| usage("command must be UTF-8"))?;
69
+ if !matches!(
70
+ command.as_str(),
71
+ "inspect" | "validate" | "convert" | "preflight"
72
+ ) {
73
+ return Err(usage(
74
+ "expected inspect, validate, convert or preflight; use --help",
75
+ ));
76
  }
77
  let mut options = Options {
78
  command,
 
156
  options.paths.push(arg.into());
157
  }
158
  }
159
+ if options.command == "preflight" {
160
+ if options.paths.len() != 1 || options.to.is_none() || options.force {
161
+ return Err(usage("preflight requires INPUT --to FORMAT"));
162
+ }
163
+ } else if options.command == "convert" {
164
  if options.paths.len() != 2 || options.to.is_none() {
165
  return Err(usage("convert requires INPUT OUTPUT --to FORMAT"));
166
  }
 
297
  };
298
  let input = load(&o.paths[0], o.from, &o.limits)?;
299
  match o.command.as_str() {
300
+ "preflight" => {
301
+ let target = match o.to.ok_or_else(|| usage("missing --to"))? {
302
+ To::Txt => preflight::Target::Txt,
303
+ To::Dat(v) => preflight::Target::Dat(v),
304
+ };
305
+ writeln!(
306
+ stdout,
307
+ "{}",
308
+ preflight::analyze(&input.world, target).json()
309
+ )?;
310
+ }
311
  "inspect" => {
312
  let w = &input.world;
313
  let planes: usize = w.brushes.iter().map(|b| b.planes.len()).sum();
crates/nanobrush-cli/src/preflight.rs ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Compatibility reporting only: parsing/encoding remain authoritative validators.
2
+ use mb3_dat::{Version, World};
3
+ use std::collections::HashSet;
4
+ #[derive(Clone, Copy, Debug)]
5
+ pub enum Target {
6
+ Txt,
7
+ Dat(Version),
8
+ }
9
+ impl Target {
10
+ pub fn name(self) -> &'static str {
11
+ match self {
12
+ Self::Txt => "txt",
13
+ Self::Dat(Version::V201501280) => "dat-201501280",
14
+ Self::Dat(Version::V201512200) => "dat-201512200",
15
+ }
16
+ }
17
+ }
18
+ #[derive(Clone, Debug, PartialEq, Eq)]
19
+ pub struct Issue {
20
+ pub code: &'static str,
21
+ pub count: usize,
22
+ }
23
+ #[derive(Clone, Debug)]
24
+ pub struct Report {
25
+ pub target: Target,
26
+ pub issues: Vec<Issue>,
27
+ }
28
+ impl Report {
29
+ pub fn compatible(&self) -> bool {
30
+ self.issues.is_empty()
31
+ }
32
+ pub fn json(&self) -> String {
33
+ let issues = self
34
+ .issues
35
+ .iter()
36
+ .map(|i| format!("{{\"code\":\"{}\",\"count\":{}}}", i.code, i.count))
37
+ .collect::<Vec<_>>()
38
+ .join(",");
39
+ format!(
40
+ "{{\"schema\":1,\"target\":\"{}\",\"compatible\":{},\"issues\":[{}]}}",
41
+ self.target.name(),
42
+ self.compatible(),
43
+ issues
44
+ )
45
+ }
46
+ }
47
+ pub fn analyze(world: &World, target: Target) -> Report {
48
+ let mut issues = Vec::new();
49
+ let planes = || world.brushes.iter().flat_map(|b| &b.planes);
50
+ match target {
51
+ Target::Dat(version) => {
52
+ let n = world
53
+ .string_properties
54
+ .iter()
55
+ .chain(&world.uv_properties)
56
+ .filter(|s| !s.is_ascii())
57
+ .count()
58
+ .saturating_add(
59
+ planes()
60
+ .flat_map(|p| &p.strings)
61
+ .filter(|p| !p.value.is_ascii())
62
+ .count(),
63
+ );
64
+ if n != 0 {
65
+ issues.push(Issue {
66
+ code: "non_ascii",
67
+ count: n,
68
+ });
69
+ }
70
+ if version == Version::V201501280 {
71
+ let n = world
72
+ .string_properties
73
+ .len()
74
+ .saturating_add(world.uv_properties.len())
75
+ .saturating_add(
76
+ planes()
77
+ .map(|p| p.strings.len().saturating_add(p.uv.len()))
78
+ .fold(0usize, usize::saturating_add),
79
+ );
80
+ if n != 0 {
81
+ issues.push(Issue {
82
+ code: "legacy_properties",
83
+ count: n,
84
+ });
85
+ }
86
+ }
87
+ }
88
+ Target::Txt => {
89
+ let mut n = 0usize;
90
+ for names in [&world.string_properties, &world.uv_properties] {
91
+ let mut seen = HashSet::new();
92
+ for name in names {
93
+ if !seen.insert(name) {
94
+ n = n.saturating_add(1);
95
+ }
96
+ }
97
+ }
98
+ if n != 0 {
99
+ issues.push(Issue {
100
+ code: "duplicate_names",
101
+ count: n,
102
+ });
103
+ }
104
+ }
105
+ }
106
+ Report { target, issues }
107
+ }
108
+ #[cfg(test)]
109
+ mod tests {
110
+ use super::*;
111
+ #[test]
112
+ fn grouped_reports_and_no_mutation() {
113
+ let mut w = World {
114
+ string_properties: vec!["é".into(), "é".into()],
115
+ ..World::default()
116
+ };
117
+ let original = w.clone();
118
+ assert_eq!(analyze(&w, Target::Txt).issues[0].count, 1);
119
+ let r = analyze(&w, Target::Dat(Version::V201501280));
120
+ assert_eq!(r.issues.len(), 2);
121
+ assert!(!r.compatible());
122
+ assert!(r.json().contains("\"schema\":1"));
123
+ assert_eq!(w, original);
124
+ w.string_properties.clear();
125
+ assert!(analyze(&w, Target::Txt).compatible());
126
+ }
127
+ }
docs/CLI.md CHANGED
@@ -54,3 +54,5 @@ Incompatible exports fail before publication: for example, Unicode TXT→ASCII D
54
  ## Tests
55
 
56
  Eight asset-free unit tests cover options, limits, inspection, both DAT directions, Unicode paths, overwrite/same-file/hard-link/symlink guards, validation before any output, preservation of existing destinations after incompatible conversion, exclusive publication races and temporary cleanup. Two process integration tests check documented exit streams/codes and invoke the real binary for inspect, DAT→TXT, validate and TXT→DAT against **all 58 DAT outputs** (232 successful commands), verifying exact final DAT bytes. Fixture tests fail when assets are missing. See [E04](experiments/txt-cli-2026-10-21.md).
 
 
 
54
  ## Tests
55
 
56
  Eight asset-free unit tests cover options, limits, inspection, both DAT directions, Unicode paths, overwrite/same-file/hard-link/symlink guards, validation before any output, preservation of existing destinations after incompatible conversion, exclusive publication races and temporary cleanup. Two process integration tests check documented exit streams/codes and invoke the real binary for inspect, DAT→TXT, validate and TXT→DAT against **all 58 DAT outputs** (232 successful commands), verifying exact final DAT bytes. Fixture tests fail when assets are missing. See [E04](experiments/txt-cli-2026-10-21.md).
57
+
58
+ `preflight INPUT --to FORMAT` emits schema-1 compatibility JSON without writing a document. A successful report command exits 0 even when `compatible` is false. Grouped issues cover non-ASCII DAT values, property-bearing old-DAT exports and duplicate TXT table names. Counts refer to affected names/entries, not lost bytes. Full structural/size validation remains the encoder’s responsibility.
docs/PROGRESS_2026-10-26.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ten medium tasks — 2026-10-26
2
+
3
+ Scope: extend native headless formats and geometry. Each stage is independently checked and committed. Native geometry policies do not claim measured editor parity. All fixtures remain in Xet; authored code/tests/docs remain ordinary Git.
4
+
5
+ | Stage | TODO | Scope | Status/evidence |
6
+ | --- | --- | --- | --- |
7
+ | 1 | P6.23 | Structured preflight reports | Complete: `preflight INPUT --to FORMAT` emits schema-1 JSON with grouped ASCII/downgrade/duplicate-name incompatibilities; reports never mutate documents. Parser/encoder validation remains authoritative. Unit tests and existing CLI unit regressions pass. |
8
+ | 2 | P6.24 | Explicit Latin-1 TXT input | Pending |
9
+ | 3 | P6.25 | Bounded computed OBJ writer | Pending |
10
+ | 4 | P6.26 | CLI OBJ with loss acknowledgement | Pending |
11
+ | 5 | P5.15 | Geometry primitives/bounds | Pending |
12
+ | 6 | P5.16 | Convex brush contours | Pending |
13
+ | 7 | P5.17 | Patch9 tessellation | Pending |
14
+ | 8 | P5.18 | Computed world pipeline/CLI | Pending |
15
+ | 9 | P5.19 | Nearest-hit ray queries | Pending |
16
+ | 10 | P5.20 | Property table edits/reindexing | Pending |
docs/TODO.md CHANGED
@@ -157,6 +157,8 @@ Each task ID is stable. Split tasks into child IDs as evidence grows. Record dep
157
 
158
  - [x] P6.22 Implement bounded Rust TXT readers/writers and native `nanobrush inspect/validate/convert` for TXT and both DAT versions, with explicit Unicode/loss policies, structural validation, guarded atomic publication and Nix packaging. All 58 DAT outputs survive CLI DAT→TXT→DAT exactly; seven TXT outputs and 16 direct TXT→DAT pairs are covered; nine invalid/lossy inputs are rejected ([RUST_TXT.md](RUST_TXT.md), [CLI.md](CLI.md), E04/F30/F31).
159
 
 
 
160
  ## P7 — Input, cameras, grids, views, and editing workflows
161
 
162
  **Depends on:** P4 dispatch and P5 geometry. **Exit:** complete interaction/state-machine specifications and reproducible replays.
 
157
 
158
  - [x] P6.22 Implement bounded Rust TXT readers/writers and native `nanobrush inspect/validate/convert` for TXT and both DAT versions, with explicit Unicode/loss policies, structural validation, guarded atomic publication and Nix packaging. All 58 DAT outputs survive CLI DAT→TXT→DAT exactly; seven TXT outputs and 16 direct TXT→DAT pairs are covered; nine invalid/lossy inputs are rejected ([RUST_TXT.md](RUST_TXT.md), [CLI.md](CLI.md), E04/F30/F31).
159
 
160
+ - [x] P6.23 Implement grouped, versioned conversion preflight reports for TXT/DAT incompatibilities without mutation ([ten-task ledger](PROGRESS_2026-10-26.md)).
161
+
162
  ## P7 — Input, cameras, grids, views, and editing workflows
163
 
164
  **Depends on:** P4 dispatch and P5 geometry. **Exit:** complete interaction/state-machine specifications and reproducible replays.