diff --git a/CHANGELOG.md b/CHANGELOG.md index b7507eb..50e6429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project uses [independent versioning](README.md#versioning) for Framewo --- +## CLI 3.2.5 — Smarter Table Column Allocation in `explore` + +### Fixed (CLI) +- Table column widths in `devtrail explore` are now allocated with a water-fill strategy: narrow columns (e.g. `CWE`, `Severity`) receive exactly their natural width and the excess flows to the columns that need it (e.g. `Description`, `Remediation`). Previously, a proportional pass gave every column a slice of the terminal budget regardless of need, which caused the narrow columns to hoard space and the wide ones to wrap unnecessarily. This is what produced the "fixes itself, breaks, fixes itself again" behavior users saw while resizing the terminal. + +--- + ## CLI 3.2.4 — Unicode-Safe Rendering Across TUI and Commands ### Fixed (CLI) diff --git a/README.md b/README.md index e02375b..bf8c936 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ DevTrail uses independent version tags for each component: | Component | Tag prefix | Example | Includes | |-----------|-----------|---------|----------| | Framework | `fw-` | `fw-4.2.0` | Templates (12 types), governance, directives | -| CLI | `cli-` | `cli-3.2.4` | The `devtrail` binary | +| CLI | `cli-` | `cli-3.2.5` | The `devtrail` binary | Check installed versions with `devtrail status` or `devtrail about`. diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 97e7741..e5c2ea8 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -537,7 +537,7 @@ dependencies = [ [[package]] name = "devtrail-cli" -version = "3.2.4" +version = "3.2.5" dependencies = [ "anyhow", "arborist-metrics", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2c17afc..4d6cab9 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "devtrail-cli" -version = "3.2.4" +version = "3.2.5" edition = "2021" description = "CLI tool for DevTrail - Documentation Governance for AI-Assisted Development" license = "MIT" diff --git a/cli/src/tui/markdown.rs b/cli/src/tui/markdown.rs index d2637fb..5c2add2 100644 --- a/cli/src/tui/markdown.rs +++ b/cli/src/tui/markdown.rs @@ -354,8 +354,9 @@ fn compute_column_widths( } } } + const MIN_COL: usize = 3; for w in &mut natural { - *w = (*w).max(3); + *w = (*w).max(MIN_COL); } // Overhead: indent is handled outside; here we account for borders @@ -368,32 +369,36 @@ fn compute_column_widths( return natural; } - // Distribute available width proportionally + // Water-fill allocation (classic: assign in ascending order of demand). + // For each column in order of natural width ascending, give it the + // smaller of its natural width and a fair share of the remaining + // budget. Narrow columns settle early with exactly what they need; the + // leftover rolls over to the wider columns that still have deficit. + // The old proportional pass gave every column a slice of the budget + // regardless of need, so narrow columns hoarded space while wide ones + // wrapped unnecessarily — exactly the "growing then shrinking" behavior + // users saw when resizing the terminal. + let mut order: Vec = (0..num_cols).collect(); + order.sort_by_key(|&i| natural[i]); + let mut widths = vec![0usize; num_cols]; - for (i, &nat) in natural.iter().enumerate() { - widths[i] = ((nat as f64 / total_natural as f64) * content_budget as f64).floor() as usize; - widths[i] = widths[i].max(3); - } - - // Distribute any remaining space to the largest columns - let assigned: usize = widths.iter().sum(); - let mut remaining = content_budget.saturating_sub(assigned); - while remaining > 0 { - // Find column with largest deficit - let mut best = 0; - let mut best_deficit = 0usize; - for (i, (&nat, &w)) in natural.iter().zip(widths.iter()).enumerate() { - let deficit = nat.saturating_sub(w); - if deficit > best_deficit { - best_deficit = deficit; - best = i; - } - } - if best_deficit == 0 { - break; - } - widths[best] += 1; - remaining -= 1; + let mut remaining_budget = content_budget; + let mut cols_left = num_cols; + + for &i in &order { + let fair_share = if cols_left > 0 { remaining_budget / cols_left } else { 0 }; + let alloc = if natural[i] <= fair_share { + // Column fits in its fair share — give it the full natural width. + natural[i] + } else { + // Column wants more than fair share — give it the fair share, + // but never less than MIN_COL (so it stays visible) and never + // more than its natural width. + fair_share.max(MIN_COL).min(natural[i]) + }; + widths[i] = alloc; + remaining_budget = remaining_budget.saturating_sub(alloc); + cols_left -= 1; } widths @@ -716,4 +721,77 @@ mod tests { let content: usize = widths.iter().sum(); assert!(content + border_overhead <= available); } + + /// Regression test for the "wide column starves" bug. With a mix of one + /// very wide column and several narrow ones, the old proportional + /// allocator gave every column a share of the budget regardless of + /// need, so narrow columns ended up with more space than they could + /// use while the wide one still wrapped. Water-fill should give each + /// narrow column exactly its natural width and pour the rest into the + /// wide column. + #[test] + fn water_fill_narrow_columns_do_not_hoard() { + let header: Vec = vec![ + "Vuln ID".to_string(), + "CWE".to_string(), + "Severity".to_string(), + "Description".to_string(), + ]; + let body: Vec> = vec![ + vec![ + "VULN-001".to_string(), + "CWE-863".to_string(), + "7.1".to_string(), + // Very long description that demands most of the budget. + "RevokeAPIKey does not validate that key_id belongs to the service_id parameter. SQL query UpdateAPIKeyStatus filters only by key_id.".to_string(), + ], + ]; + let available = 120; + let widths = compute_column_widths(&header, &body, available); + assert_eq!(widths.len(), 4); + + // Natural widths: Vuln=max("Vuln ID"=7, "VULN-001"=8)=8, CWE=7, + // Severity=8, Description≈137. Narrow columns must receive exactly + // their natural width; the rest flows to Description. + assert_eq!(widths[0], 8, "Vuln ID column got {} cols, expected 8", widths[0]); + assert_eq!(widths[1], 7, "CWE column got {} cols, expected 7", widths[1]); + assert_eq!(widths[2], 8, "Severity column got {} cols, expected 8", widths[2]); + + let border_overhead = 2 + (4 - 1) * 3 + 2; // 13 + let expected_desc = available - border_overhead - (8 + 7 + 8); + assert_eq!( + widths[3], expected_desc, + "Description got {} cols, expected {} (all leftover)", + widths[3], expected_desc, + ); + } + + #[test] + fn water_fill_tight_budget_does_not_overflow() { + // Budget smaller than sum of naturals; no column should exceed its + // natural width and the total must fit in the content budget. + let header: Vec = vec!["A".into(), "B".into(), "C".into(), "D".into()]; + let body: Vec> = vec![vec![ + "short".into(), + "mediumtext".into(), + "wide column content here".into(), + "xxx".into(), + ]]; + let available = 40; + let widths = compute_column_widths(&header, &body, available); + let border_overhead = 2 + (widths.len() - 1) * 3 + 2; + let total: usize = widths.iter().sum(); + assert!( + total + border_overhead <= available, + "total {} + overhead {} exceeds budget {}", + total, + border_overhead, + available, + ); + // No column exceeds its natural width. + let naturals = [5usize, 10, 24, 3]; + for (i, w) in widths.iter().enumerate() { + assert!(*w <= naturals[i].max(3), "col {i} exceeded its natural"); + } + } } diff --git a/docs/adopters/CLI-REFERENCE.md b/docs/adopters/CLI-REFERENCE.md index bc1e9c3..06998fe 100644 --- a/docs/adopters/CLI-REFERENCE.md +++ b/docs/adopters/CLI-REFERENCE.md @@ -49,7 +49,7 @@ DevTrail uses **independent version tags** for each component: | Component | Tag prefix | Example | What it includes | |-----------|-----------|---------|------------------| | Framework | `fw-` | `fw-4.2.0` | Templates (12 types), governance docs, directives | -| CLI | `cli-` | `cli-3.2.4` | The `devtrail` binary | +| CLI | `cli-` | `cli-3.2.5` | The `devtrail` binary | Framework and CLI are released independently. A framework update does not require a CLI update, and vice versa. @@ -110,7 +110,7 @@ $ devtrail update Updating framework... ✔ Framework updated to fw-4.2.0 Updating CLI... -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 ``` --- @@ -143,11 +143,11 @@ Use `--method` to override auto-detection: `--method=github` or `--method=cargo` ```bash $ devtrail update-cli -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 $ devtrail update-cli --method=cargo Compiling from source, this may take a few minutes... -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 ``` --- @@ -210,7 +210,7 @@ $ devtrail status ┌───────────┬──────────────────────────┐ │ Path │ /home/user/my-project │ │ Framework │ fw-4.2.0 │ - │ CLI │ cli-3.2.4 │ + │ CLI │ cli-3.2.5 │ │ Language │ en │ └───────────┴──────────────────────────┘ @@ -634,7 +634,7 @@ Show version, authorship, and license information. ```bash $ devtrail about DevTrail CLI - CLI version: cli-3.2.4 + CLI version: cli-3.2.5 Framework version: fw-4.2.0 Author: Strange Days Tech, S.A.S. License: MIT diff --git a/docs/i18n/es/README.md b/docs/i18n/es/README.md index 769e27c..a2449df 100644 --- a/docs/i18n/es/README.md +++ b/docs/i18n/es/README.md @@ -150,7 +150,7 @@ DevTrail usa tags de versión independientes para cada componente: | Componente | Prefijo de tag | Ejemplo | Incluye | |------------|---------------|---------|---------| | Framework | `fw-` | `fw-4.2.0` | Plantillas (12 tipos), gobernanza, directivas | -| CLI | `cli-` | `cli-3.2.4` | El binario `devtrail` | +| CLI | `cli-` | `cli-3.2.5` | El binario `devtrail` | Verifica las versiones instaladas con `devtrail status` o `devtrail about`. diff --git a/docs/i18n/es/adopters/CLI-REFERENCE.md b/docs/i18n/es/adopters/CLI-REFERENCE.md index 8cf7911..67ac2c6 100644 --- a/docs/i18n/es/adopters/CLI-REFERENCE.md +++ b/docs/i18n/es/adopters/CLI-REFERENCE.md @@ -49,7 +49,7 @@ DevTrail usa **tags de versión independientes** para cada componente: | Componente | Prefijo de tag | Ejemplo | Qué incluye | |------------|---------------|---------|-------------| | Framework | `fw-` | `fw-4.2.0` | Plantillas (12 tipos), docs de gobernanza, directivas | -| CLI | `cli-` | `cli-3.2.4` | El binario `devtrail` | +| CLI | `cli-` | `cli-3.2.5` | El binario `devtrail` | Framework y CLI se publican de forma independiente. Una actualización del framework no requiere actualización del CLI, y viceversa. @@ -109,7 +109,7 @@ $ devtrail update Updating framework... ✔ Framework updated to fw-4.2.0 Updating CLI... -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 ``` --- @@ -142,11 +142,11 @@ Usa `--method` para forzar el método: `--method=github` o `--method=cargo`. ```bash $ devtrail update-cli -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 $ devtrail update-cli --method=cargo Compiling from source, this may take a few minutes... -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 ``` --- @@ -204,7 +204,7 @@ DevTrail Status ─────────────── Path: /home/user/my-project Framework version: fw-4.2.0 -CLI version: cli-3.2.4 +CLI version: cli-3.2.5 Language: en Structure: ✔ Complete @@ -513,7 +513,7 @@ Muestra información de versión, autoría y licencia. ```bash $ devtrail about DevTrail CLI - CLI version: cli-3.2.4 + CLI version: cli-3.2.5 Framework version: fw-4.2.0 Author: Strange Days Tech, S.A.S. License: MIT diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index 378f32e..f252e4d 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -150,7 +150,7 @@ DevTrail 为每个组件使用独立的版本标签: | 组件 | 标签前缀 | 示例 | 包含内容 | |------|----------|------|----------| | Framework | `fw-` | `fw-4.2.0` | 模板(12 种类型)、治理文档、指令 | -| CLI | `cli-` | `cli-3.2.4` | `devtrail` 二进制文件 | +| CLI | `cli-` | `cli-3.2.5` | `devtrail` 二进制文件 | 使用 `devtrail status` 或 `devtrail about` 查看已安装的版本。 diff --git a/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md b/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md index 4313d84..5a04c09 100644 --- a/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md +++ b/docs/i18n/zh-CN/adopters/CLI-REFERENCE.md @@ -49,7 +49,7 @@ DevTrail 为每个组件使用**独立的版本标签**: | 组件 | 标签前缀 | 示例 | 包含内容 | |------|----------|------|----------| | Framework | `fw-` | `fw-4.2.0` | 模板(12 种类型)、治理文档、指令 | -| CLI | `cli-` | `cli-3.2.4` | `devtrail` 二进制文件 | +| CLI | `cli-` | `cli-3.2.5` | `devtrail` 二进制文件 | Framework 和 CLI 独立发布。Framework 更新不需要 CLI 更新,反之亦然。 @@ -110,7 +110,7 @@ $ devtrail update Updating framework... ✔ Framework updated to fw-4.2.0 Updating CLI... -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 ``` --- @@ -143,11 +143,11 @@ $ devtrail update-framework ```bash $ devtrail update-cli -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 $ devtrail update-cli --method=cargo Compiling from source, this may take a few minutes... -✔ CLI updated to cli-3.2.4 +✔ CLI updated to cli-3.2.5 ``` --- @@ -210,7 +210,7 @@ $ devtrail status ┌───────────┬──────────────────────────┐ │ Path │ /home/user/my-project │ │ Framework │ fw-4.2.0 │ - │ CLI │ cli-3.2.4 │ + │ CLI │ cli-3.2.5 │ │ Language │ en │ └───────────┴──────────────────────────┘ @@ -634,7 +634,7 @@ $ devtrail explore ```bash $ devtrail about DevTrail CLI - CLI version: cli-3.2.4 + CLI version: cli-3.2.5 Framework version: fw-4.2.0 Author: Strange Days Tech, S.A.S. License: MIT