headson 0.3.0__tar.gz → 0.4.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.

Potentially problematic release.


This version of headson might be problematic. Click here for more details.

Files changed (47) hide show
  1. {headson-0.3.0 → headson-0.4.0}/Cargo.lock +1 -1
  2. {headson-0.3.0 → headson-0.4.0}/Cargo.toml +1 -1
  3. {headson-0.3.0 → headson-0.4.0}/PKG-INFO +6 -5
  4. {headson-0.3.0 → headson-0.4.0}/README.md +4 -3
  5. {headson-0.3.0 → headson-0.4.0}/pyproject.toml +3 -3
  6. {headson-0.3.0 → headson-0.4.0}/python/Cargo.lock +2 -2
  7. {headson-0.3.0 → headson-0.4.0}/python/Cargo.toml +2 -2
  8. {headson-0.3.0 → headson-0.4.0}/python/README.md +10 -2
  9. {headson-0.3.0 → headson-0.4.0}/src/lib.rs +12 -10
  10. {headson-0.3.0 → headson-0.4.0}/src/order/build.rs +36 -34
  11. {headson-0.3.0 → headson-0.4.0}/src/order/types.rs +1 -1
  12. {headson-0.3.0 → headson-0.4.0}/src/serialization/mod.rs +123 -49
  13. headson-0.4.0/src/utils/graph.rs +61 -0
  14. headson-0.3.0/src/utils/graph.rs +0 -54
  15. {headson-0.3.0 → headson-0.4.0}/JSONTestSuite/LICENSE +0 -0
  16. {headson-0.3.0 → headson-0.4.0}/JSONTestSuite/README.md +0 -0
  17. {headson-0.3.0 → headson-0.4.0}/LICENSE +0 -0
  18. {headson-0.3.0 → headson-0.4.0}/python/headson/__init__.py +0 -0
  19. {headson-0.3.0 → headson-0.4.0}/python/src/lib.rs +0 -0
  20. {headson-0.3.0 → headson-0.4.0}/src/json_ingest/builder.rs +0 -0
  21. {headson-0.3.0 → headson-0.4.0}/src/json_ingest/mod.rs +0 -0
  22. {headson-0.3.0 → headson-0.4.0}/src/main.rs +0 -0
  23. {headson-0.3.0 → headson-0.4.0}/src/order/mod.rs +0 -0
  24. {headson-0.3.0 → headson-0.4.0}/src/order/scoring.rs +0 -0
  25. {headson-0.3.0 → headson-0.4.0}/src/order/snapshots/headson__order__build__tests__order_empty_array_order.snap +0 -0
  26. {headson-0.3.0 → headson-0.4.0}/src/order/snapshots/headson__order__build__tests__order_single_string_array_order.snap +0 -0
  27. {headson-0.3.0 → headson-0.4.0}/src/serialization/snapshots/headson__serialization__tests__arena_render_empty.snap +0 -0
  28. {headson-0.3.0 → headson-0.4.0}/src/serialization/snapshots/headson__serialization__tests__arena_render_single.snap +0 -0
  29. {headson-0.3.0 → headson-0.4.0}/src/serialization/templates/core.rs +0 -0
  30. {headson-0.3.0 → headson-0.4.0}/src/serialization/templates/js.rs +0 -0
  31. {headson-0.3.0 → headson-0.4.0}/src/serialization/templates/json.rs +0 -0
  32. {headson-0.3.0 → headson-0.4.0}/src/serialization/templates/mod.rs +0 -0
  33. {headson-0.3.0 → headson-0.4.0}/src/serialization/templates/pseudo.rs +0 -0
  34. {headson-0.3.0 → headson-0.4.0}/src/serialization/types.rs +0 -0
  35. {headson-0.3.0 → headson-0.4.0}/src/snapshots/headson__order__tests__order_empty_array_order.snap +0 -0
  36. {headson-0.3.0 → headson-0.4.0}/src/snapshots/headson__order__tests__order_single_string_array_order.snap +0 -0
  37. {headson-0.3.0 → headson-0.4.0}/src/snapshots/headson__order__tests__pq_empty_array_queue.snap +0 -0
  38. {headson-0.3.0 → headson-0.4.0}/src/snapshots/headson__order__tests__pq_single_string_array_queue.snap +0 -0
  39. {headson-0.3.0 → headson-0.4.0}/src/snapshots/headson__queue__tests__pq_empty_array_queue.snap +0 -0
  40. {headson-0.3.0 → headson-0.4.0}/src/snapshots/headson__queue__tests__pq_single_string_array_queue.snap +0 -0
  41. {headson-0.3.0 → headson-0.4.0}/src/snapshots/headson__tree__tests__build_tree_empty.snap +0 -0
  42. {headson-0.3.0 → headson-0.4.0}/src/snapshots/headson__tree__tests__build_tree_single.snap +0 -0
  43. {headson-0.3.0 → headson-0.4.0}/src/utils/json.rs +0 -0
  44. {headson-0.3.0 → headson-0.4.0}/src/utils/mod.rs +0 -0
  45. {headson-0.3.0 → headson-0.4.0}/src/utils/search.rs +0 -0
  46. {headson-0.3.0 → headson-0.4.0}/src/utils/text.rs +0 -0
  47. {headson-0.3.0 → headson-0.4.0}/src/utils/tree_arena.rs +0 -0
@@ -266,7 +266,7 @@ dependencies = [
266
266
 
267
267
  [[package]]
268
268
  name = "headson"
269
- version = "0.3.0"
269
+ version = "0.4.0"
270
270
  dependencies = [
271
271
  "anyhow",
272
272
  "assert_cmd",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "headson"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  edition = "2024"
5
5
  description = "Budget‑constrained JSON preview renderer"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: headson
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Rust
@@ -10,7 +10,7 @@ Provides-Extra: test
10
10
  License-File: LICENSE
11
11
  Summary: Budget‑constrained JSON preview renderer (Python bindings)
12
12
  Keywords: json,preview,summarize,cli,bindings
13
- Requires-Python: >=3.8
13
+ Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
15
 
16
16
  # headson
@@ -72,7 +72,8 @@ Notes:
72
72
 
73
73
  - With multiple input files:
74
74
  - JSON template outputs a single JSON object keyed by the input file paths.
75
- - Pseudo and JS templates render file sections with human-readable headers.
75
+ - Pseudo and JS templates render file sections with human-readable headers when newlines are enabled.
76
+ - If you use `--compact` or `--no-newline` (both disable newlines), fileset output falls back to standard inline rendering (no per-file headers) to remain compact.
76
77
  - Using `--global-budget` may truncate or omit entire files to respect the total budget.
77
78
  - The tool finds the largest preview that fits the budget; if even the tiniest preview exceeds it, you still get a minimal, valid preview.
78
79
  - When passing file paths, directories and binary files are ignored; a notice is printed to stderr for each (e.g., `Ignored binary file: ./path/to/file`). Stdin mode reads the stream as-is.
@@ -134,12 +135,12 @@ headson -n 120 -f json users.json
134
135
 
135
136
  A thin Python extension module is available on PyPI as `headson`.
136
137
 
137
- - Install: `pip install headson` (prebuilt wheels for CPython 3.10–3.12 on Linux/macOS/Windows). Older/newer Python versions may build from source if Rust is installed.
138
+ - Install: `pip install headson` (ABI3 wheels for Python 3.10+ on Linux/macOS/Windows).
138
139
  - API:
139
140
  - `headson.summarize(text: str, *, template: str = "pseudo", character_budget: int | None = None, tail: bool = False) -> str`
140
141
  - `template`: one of `"json" | "pseudo" | "js"`
141
142
  - `character_budget`: maximum output size in characters (default: 500)
142
- - `tail`: prefer the end of arrays when truncating; strings unaffected. Affects only display templates (`pseudo`/`js`); `json` remains strict.
143
+ - `tail`: prefer the end of arrays when truncating; strings unaffected. Affects only display templates (`pseudo`/`js`); `json` remains strict.
143
144
 
144
145
  Example:
145
146
 
@@ -57,7 +57,8 @@ Notes:
57
57
 
58
58
  - With multiple input files:
59
59
  - JSON template outputs a single JSON object keyed by the input file paths.
60
- - Pseudo and JS templates render file sections with human-readable headers.
60
+ - Pseudo and JS templates render file sections with human-readable headers when newlines are enabled.
61
+ - If you use `--compact` or `--no-newline` (both disable newlines), fileset output falls back to standard inline rendering (no per-file headers) to remain compact.
61
62
  - Using `--global-budget` may truncate or omit entire files to respect the total budget.
62
63
  - The tool finds the largest preview that fits the budget; if even the tiniest preview exceeds it, you still get a minimal, valid preview.
63
64
  - When passing file paths, directories and binary files are ignored; a notice is printed to stderr for each (e.g., `Ignored binary file: ./path/to/file`). Stdin mode reads the stream as-is.
@@ -119,12 +120,12 @@ headson -n 120 -f json users.json
119
120
 
120
121
  A thin Python extension module is available on PyPI as `headson`.
121
122
 
122
- - Install: `pip install headson` (prebuilt wheels for CPython 3.10–3.12 on Linux/macOS/Windows). Older/newer Python versions may build from source if Rust is installed.
123
+ - Install: `pip install headson` (ABI3 wheels for Python 3.10+ on Linux/macOS/Windows).
123
124
  - API:
124
125
  - `headson.summarize(text: str, *, template: str = "pseudo", character_budget: int | None = None, tail: bool = False) -> str`
125
126
  - `template`: one of `"json" | "pseudo" | "js"`
126
127
  - `character_budget`: maximum output size in characters (default: 500)
127
- - `tail`: prefer the end of arrays when truncating; strings unaffected. Affects only display templates (`pseudo`/`js`); `json` remains strict.
128
+ - `tail`: prefer the end of arrays when truncating; strings unaffected. Affects only display templates (`pseudo`/`js`); `json` remains strict.
128
129
 
129
130
  Example:
130
131
 
@@ -4,10 +4,10 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "headson"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Budget‑constrained JSON preview renderer (Python bindings)"
9
9
  readme = "README.md"
10
- requires-python = ">=3.8"
10
+ requires-python = ">=3.10"
11
11
  classifiers = [
12
12
  "Programming Language :: Python",
13
13
  "Programming Language :: Python :: 3",
@@ -32,7 +32,7 @@ python-source = "python"
32
32
  dev = [
33
33
  "pytest>=8",
34
34
  "maturin>=1.7,<2",
35
- "ruff==0.6.9",
35
+ "ruff==0.14.2",
36
36
  ]
37
37
 
38
38
  [tool.ruff]
@@ -169,7 +169,7 @@ dependencies = [
169
169
 
170
170
  [[package]]
171
171
  name = "headson"
172
- version = "0.3.0"
172
+ version = "0.4.0"
173
173
  dependencies = [
174
174
  "anyhow",
175
175
  "clap",
@@ -182,7 +182,7 @@ dependencies = [
182
182
 
183
183
  [[package]]
184
184
  name = "headson-python"
185
- version = "0.3.0"
185
+ version = "0.4.0"
186
186
  dependencies = [
187
187
  "anyhow",
188
188
  "headson",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "headson-python"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
  readme = "README.md"
@@ -11,5 +11,5 @@ crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
13
  anyhow = "1"
14
- pyo3 = { version = "0.27", features = ["extension-module"] }
14
+ pyo3 = { version = "0.27", features = ["extension-module", "abi3-py310"] }
15
15
  headson_core = { package = "headson", path = ".." }
@@ -4,19 +4,27 @@ Minimal Python API for the `headson` JSON preview renderer.
4
4
 
5
5
  Currently exported function:
6
6
 
7
- - `headson.summarize(text: str, *, template: str = "pseudo", character_budget: int | None = None) -> str`
7
+ - `headson.summarize(text: str, *, template: str = "pseudo", character_budget: int | None = None, tail: bool = False) -> str`
8
8
  - `template`: one of `"json" | "pseudo" | "js"`.
9
9
  - `character_budget`: maximum output size in characters (defaults to 500 if not set).
10
+ - `tail`: prefer the end of arrays when truncating; strings are unaffected. Only affects display templates (`pseudo`/`js`); `json` remains strict JSON with no annotations.
10
11
 
11
12
  Examples:
12
13
 
13
14
  ```python
14
15
  import headson
15
16
 
17
+ # Pseudo template with a small budget (structure-aware preview)
16
18
  print(headson.summarize('{"a": 1, "b": [1,2,3]}', template="pseudo", character_budget=80))
19
+
20
+ # Strict JSON template preserves valid JSON output
17
21
  print(headson.summarize('{"a": 1, "b": {"c": 2}}', template="json", character_budget=10_000))
22
+
23
+ # JS template with tail preference: prefer the end of arrays when truncating
18
24
  arr = ','.join(str(i) for i in range(100))
19
- print(headson.summarize('{"arr": [' + arr + ']}', template="js", character_budget=60))
25
+ print(headson.summarize('{"arr": [' + arr + ']}', template="js", character_budget=60, tail=True))
26
+
27
+ # Note: tail mode affects only pseudo/js display templates; the json template stays strict.
20
28
  ```
21
29
 
22
30
  Install for development:
@@ -70,20 +70,22 @@ fn find_largest_render_under_budget(
70
70
  // Each included node contributes at least some output; cap hi by budget.
71
71
  let lo = 1usize;
72
72
  let hi = total.min(char_budget.max(1));
73
- // Reusable inclusion marks to avoid clearing per probe
74
- let mut marks: Vec<u32> = vec![0; total];
75
- let mut mark_gen: u32 = 1;
73
+ // Reuse render-inclusion flags across render attempts to avoid clearing the vector.
74
+ // A node participates in the current render attempt when inclusion_flags[id] == render_set_id.
75
+ let mut inclusion_flags: Vec<u32> = vec![0; total];
76
+ // Each render attempt bumps this non-zero identifier to create a fresh inclusion set.
77
+ let mut render_set_id: u32 = 1;
76
78
  let mut best_str: Option<String> = None;
77
79
 
78
80
  let _ = crate::utils::search::binary_search_max(lo, hi, |mid| {
79
- let s = crate::serialization::render_arena_with_marks(
81
+ let s = crate::serialization::render_top_k(
80
82
  order_build,
81
83
  mid,
82
- &mut marks,
83
- mark_gen,
84
+ &mut inclusion_flags,
85
+ render_set_id,
84
86
  config,
85
87
  );
86
- mark_gen = mark_gen.wrapping_add(1).max(1);
88
+ render_set_id = render_set_id.wrapping_add(1).max(1);
87
89
  if s.len() <= char_budget {
88
90
  best_str = Some(s);
89
91
  true
@@ -97,11 +99,11 @@ fn find_largest_render_under_budget(
97
99
  } else {
98
100
  // Fallback: always render a single node (k=1) to produce the
99
101
  // shortest possible preview, even if it exceeds the byte budget.
100
- crate::serialization::render_arena_with_marks(
102
+ crate::serialization::render_top_k(
101
103
  order_build,
102
104
  1,
103
- &mut marks,
104
- mark_gen,
105
+ &mut inclusion_flags,
106
+ render_set_id,
105
107
  config,
106
108
  )
107
109
  }
@@ -10,15 +10,17 @@ use crate::utils::tree_arena::JsonTreeArena;
10
10
  #[derive(Clone)]
11
11
  struct Entry {
12
12
  score: u128,
13
- pq_id: usize,
13
+ // Index into the priority-ordered nodes (0..total_nodes)
14
+ priority_index: usize,
14
15
  depth: usize,
15
- // When present, we can read kind from the arena node.
16
+ // When present, we can read kind from the arena (parsed JSON) node.
16
17
  // When None, this is a synthetic entry (currently only string grapheme).
17
- arena_node: Option<usize>,
18
+ arena_index: Option<usize>,
18
19
  }
19
20
  impl PartialEq for Entry {
20
21
  fn eq(&self, other: &Self) -> bool {
21
- self.score == other.score && self.pq_id == other.pq_id
22
+ self.score == other.score
23
+ && self.priority_index == other.priority_index
22
24
  }
23
25
  }
24
26
  impl Eq for Entry {}
@@ -31,7 +33,7 @@ impl Ord for Entry {
31
33
  fn cmp(&self, other: &Self) -> std::cmp::Ordering {
32
34
  self.score
33
35
  .cmp(&other.score)
34
- .then_with(|| self.pq_id.cmp(&other.pq_id))
36
+ .then_with(|| self.priority_index.cmp(&other.priority_index))
35
37
  }
36
38
  }
37
39
 
@@ -52,12 +54,12 @@ impl<'a> Scope<'a> {
52
54
  fn push_child_common(
53
55
  &mut self,
54
56
  entry: &Entry,
55
- child_pq: usize,
56
- arena_node: Option<usize>,
57
+ child_priority_index: usize,
58
+ arena_index: Option<usize>,
57
59
  score: u128,
58
60
  ranked: RankedNode,
59
61
  ) {
60
- let id = entry.pq_id;
62
+ let id = entry.priority_index;
61
63
  self.parent.push(Some(NodeId(id)));
62
64
  self.children.push(Vec::new());
63
65
  self.metrics.push(NodeMetrics::default());
@@ -65,12 +67,12 @@ impl<'a> Scope<'a> {
65
67
  // Children created from parsing regular JSON are standard objects/arrays/etc.
66
68
  // If child is an object, default to Object type.
67
69
  self.object_type.push(ObjectType::Object);
68
- self.children[id].push(NodeId(child_pq));
70
+ self.children[id].push(NodeId(child_priority_index));
69
71
  self.heap.push(Reverse(Entry {
70
72
  score,
71
- pq_id: child_pq,
73
+ priority_index: child_priority_index,
72
74
  depth: entry.depth + 1,
73
- arena_node,
75
+ arena_index,
74
76
  }));
75
77
  }
76
78
  fn record_array_metrics(&mut self, id: usize, arena_id: usize) {
@@ -118,7 +120,7 @@ impl<'a> Scope<'a> {
118
120
  for i in 0..kept {
119
121
  let child_arena_id = self.arena.children[node.children_start + i];
120
122
  let child_kind = self.arena.nodes[child_arena_id].kind;
121
- let child_pq = *self.next_pq_id;
123
+ let child_priority_index = *self.next_pq_id;
122
124
  *self.next_pq_id += 1;
123
125
  let idx_for_priority: usize = if self.config.prefer_tail_arrays {
124
126
  kept.saturating_sub(1).saturating_sub(i)
@@ -131,11 +133,11 @@ impl<'a> Scope<'a> {
131
133
  let child_node = &self.arena.nodes[child_arena_id];
132
134
  self.push_child_common(
133
135
  entry,
134
- child_pq,
136
+ child_priority_index,
135
137
  Some(child_arena_id),
136
138
  score,
137
139
  RankedNode {
138
- node_id: NodeId(child_pq),
140
+ node_id: NodeId(child_priority_index),
139
141
  kind: child_kind,
140
142
  key_in_object: None,
141
143
  number_value: child_node.number_value.clone(),
@@ -163,17 +165,17 @@ impl<'a> Scope<'a> {
163
165
  });
164
166
  for (key_idx, child_arena_id) in items {
165
167
  let child_kind = self.arena.nodes[child_arena_id].kind;
166
- let child_pq = *self.next_pq_id;
168
+ let child_priority_index = *self.next_pq_id;
167
169
  *self.next_pq_id += 1;
168
170
  let score = entry.score + OBJECT_CHILD_BASE_INCREMENT;
169
171
  let child_node = &self.arena.nodes[child_arena_id];
170
172
  self.push_child_common(
171
173
  entry,
172
- child_pq,
174
+ child_priority_index,
173
175
  Some(child_arena_id),
174
176
  score,
175
177
  RankedNode {
176
- node_id: NodeId(child_pq),
178
+ node_id: NodeId(child_priority_index),
177
179
  kind: child_kind,
178
180
  key_in_object: Some(self.arena.obj_keys[key_idx].clone()),
179
181
  number_value: child_node.number_value.clone(),
@@ -188,13 +190,13 @@ impl<'a> Scope<'a> {
188
190
  }
189
191
 
190
192
  fn expand_string_children(&mut self, entry: &Entry) {
191
- let id = entry.pq_id;
193
+ let id = entry.priority_index;
192
194
  let full = self.nodes[id].string_value.as_deref().unwrap_or("");
193
195
  let count = UnicodeSegmentation::graphemes(full, true)
194
196
  .take(self.config.max_string_graphemes)
195
197
  .count();
196
198
  for i in 0..count {
197
- let child_pq = *self.next_pq_id;
199
+ let child_priority_index = *self.next_pq_id;
198
200
  *self.next_pq_id += 1;
199
201
  let extra = if i > STRING_INDEX_INFLECTION {
200
202
  let d = (i - STRING_INDEX_INFLECTION) as u128;
@@ -208,11 +210,11 @@ impl<'a> Scope<'a> {
208
210
  + extra;
209
211
  self.push_child_common(
210
212
  entry,
211
- child_pq,
213
+ child_priority_index,
212
214
  None,
213
215
  score,
214
216
  RankedNode {
215
- node_id: NodeId(child_pq),
217
+ node_id: NodeId(child_priority_index),
216
218
  kind: NodeKind::String,
217
219
  key_in_object: None,
218
220
  number_value: None,
@@ -227,7 +229,7 @@ impl<'a> Scope<'a> {
227
229
  }
228
230
 
229
231
  fn resolve_kind(&self, entry: &Entry) -> NodeKind {
230
- if let Some(ar_id) = entry.arena_node {
232
+ if let Some(ar_id) = entry.arena_index {
231
233
  self.arena.nodes[ar_id].kind
232
234
  } else {
233
235
  NodeKind::String
@@ -237,12 +239,12 @@ impl<'a> Scope<'a> {
237
239
  fn expand_for(&mut self, entry: &Entry, kind: NodeKind) {
238
240
  match kind {
239
241
  NodeKind::Array => {
240
- if let Some(ar_id) = entry.arena_node {
242
+ if let Some(ar_id) = entry.arena_index {
241
243
  self.expand_array_children(entry, ar_id);
242
244
  }
243
245
  }
244
246
  NodeKind::Object => {
245
- if let Some(ar_id) = entry.arena_node {
247
+ if let Some(ar_id) = entry.arena_index {
246
248
  self.expand_object_children(entry, ar_id);
247
249
  }
248
250
  }
@@ -256,10 +258,10 @@ impl<'a> Scope<'a> {
256
258
  entry: &Entry,
257
259
  ids_by_order: &mut Vec<NodeId>,
258
260
  ) {
259
- let id = entry.pq_id;
261
+ let id = entry.priority_index;
260
262
  ids_by_order.push(NodeId(id));
261
263
  let kind = self.resolve_kind(entry);
262
- if let Some(ar_id) = entry.arena_node {
264
+ if let Some(ar_id) = entry.arena_index {
263
265
  self.record_metrics_for(id, kind, ar_id);
264
266
  }
265
267
  self.expand_for(entry, kind);
@@ -282,14 +284,14 @@ pub fn build_order(
282
284
  // Seed root from arena
283
285
  let root_ar = arena.root_id;
284
286
  let root_kind = arena.nodes[root_ar].kind;
285
- let root_pq = next_pq_id;
287
+ let root_priority_index = next_pq_id;
286
288
  next_pq_id += 1;
287
289
  parent.push(None);
288
290
  children.push(Vec::new());
289
291
  metrics.push(NodeMetrics::default());
290
292
  let n = &arena.nodes[root_ar];
291
293
  nodes.push(RankedNode {
292
- node_id: NodeId(root_pq),
294
+ node_id: NodeId(root_priority_index),
293
295
  kind: root_kind,
294
296
  key_in_object: None,
295
297
  number_value: n.number_value.clone(),
@@ -305,9 +307,9 @@ pub fn build_order(
305
307
  object_type.push(root_ot);
306
308
  heap.push(Reverse(Entry {
307
309
  score: ROOT_BASE_SCORE,
308
- pq_id: root_pq,
310
+ priority_index: root_priority_index,
309
311
  depth: 0,
310
- arena_node: Some(root_ar),
312
+ arena_index: Some(root_ar),
311
313
  }));
312
314
 
313
315
  while let Some(Reverse(entry)) = heap.pop() {
@@ -335,7 +337,7 @@ pub fn build_order(
335
337
  nodes,
336
338
  parent,
337
339
  children,
338
- order,
340
+ by_priority: order,
339
341
  total_nodes: total,
340
342
  object_type,
341
343
  })
@@ -359,9 +361,9 @@ mod tests {
359
361
  )
360
362
  .unwrap();
361
363
  let mut items_sorted: Vec<_> = build.nodes.clone();
362
- // Build a transient mapping from id -> order index
364
+ // Build a transient mapping from id -> by_priority index
363
365
  let mut order_index = vec![usize::MAX; build.total_nodes];
364
- for (idx, &pid) in build.order.iter().enumerate() {
366
+ for (idx, &pid) in build.by_priority.iter().enumerate() {
365
367
  let pidx = pid.0;
366
368
  if pidx < build.total_nodes {
367
369
  order_index[pidx] = idx;
@@ -391,7 +393,7 @@ mod tests {
391
393
  .unwrap();
392
394
  let mut items_sorted: Vec<_> = build.nodes.clone();
393
395
  let mut order_index = vec![usize::MAX; build.total_nodes];
394
- for (idx, &pid) in build.order.iter().enumerate() {
396
+ for (idx, &pid) in build.by_priority.iter().enumerate() {
395
397
  let pidx = pid.0;
396
398
  if pidx < build.total_nodes {
397
399
  order_index[pidx] = idx;
@@ -62,7 +62,7 @@ pub struct PriorityOrder {
62
62
  // They correspond to `NodeId.0` in `RankedNode` for convenience when indexing.
63
63
  pub parent: Vec<Option<NodeId>>, // parent[id] = parent id (PQ id)
64
64
  pub children: Vec<Vec<NodeId>>, // children[id] = children ids (PQ ids)
65
- pub order: Vec<NodeId>, // ids sorted by ascending priority (PQ ids)
65
+ pub by_priority: Vec<NodeId>, // ids sorted by ascending priority (PQ ids)
66
66
  pub total_nodes: usize,
67
67
  pub object_type: Vec<ObjectType>,
68
68
  }
@@ -12,9 +12,15 @@ type ArrayChildPair = (usize, String);
12
12
  type ObjectChildPair = (usize, (String, String));
13
13
 
14
14
  pub(crate) struct RenderScope<'a> {
15
- pq: &'a PriorityOrder,
16
- marks: &'a [u32],
17
- mark_gen: u32,
15
+ // Priority-ordered view of the parsed JSON tree.
16
+ order: &'a PriorityOrder,
17
+ // Per-node inclusion flag: a node is included in the current render attempt
18
+ // when inclusion_flags[node_id] == render_set_id. This avoids clearing the
19
+ // vector between render attempts by bumping render_set_id each time.
20
+ inclusion_flags: &'a [u32],
21
+ // Identifier for the current inclusion set (render pass).
22
+ render_set_id: u32,
23
+ // Rendering configuration (template, whitespace, etc.).
18
24
  config: &'a crate::RenderConfig,
19
25
  }
20
26
 
@@ -59,7 +65,7 @@ impl<'a> RenderScope<'a> {
59
65
  child_pq_id: usize,
60
66
  nl: &str,
61
67
  ) {
62
- let raw_key = self.pq.nodes[child_pq_id]
68
+ let raw_key = self.order.nodes[child_pq_id]
63
69
  .key_in_object
64
70
  .as_deref()
65
71
  .unwrap_or("");
@@ -99,7 +105,7 @@ impl<'a> RenderScope<'a> {
99
105
  child_pq_id: usize,
100
106
  nl: &str,
101
107
  ) {
102
- let raw_key = self.pq.nodes[child_pq_id]
108
+ let raw_key = self.order.nodes[child_pq_id]
103
109
  .key_in_object
104
110
  .as_deref()
105
111
  .unwrap_or("");
@@ -132,10 +138,10 @@ impl<'a> RenderScope<'a> {
132
138
  }
133
139
  }
134
140
  fn count_kept_children(&self, id: usize) -> usize {
135
- if let Some(kids) = self.pq.children.get(id) {
141
+ if let Some(kids) = self.order.children.get(id) {
136
142
  let mut kept = 0usize;
137
143
  for &cid in kids {
138
- if self.marks[cid.0] == self.mark_gen {
144
+ if self.inclusion_flags[cid.0] == self.render_set_id {
139
145
  kept += 1;
140
146
  }
141
147
  }
@@ -146,7 +152,7 @@ impl<'a> RenderScope<'a> {
146
152
  }
147
153
 
148
154
  fn omitted_for_string(&self, id: usize, kept: usize) -> Option<usize> {
149
- let m = &self.pq.metrics[id];
155
+ let m = &self.order.metrics[id];
150
156
  if let Some(orig) = m.string_len {
151
157
  if orig > kept {
152
158
  return Some(orig - kept);
@@ -170,13 +176,13 @@ impl<'a> RenderScope<'a> {
170
176
  ) -> Option<usize> {
171
177
  match kind {
172
178
  NodeKind::Array => {
173
- self.pq.metrics[id].array_len.and_then(|orig| {
179
+ self.order.metrics[id].array_len.and_then(|orig| {
174
180
  if orig > kept { Some(orig - kept) } else { None }
175
181
  })
176
182
  }
177
183
  NodeKind::String => self.omitted_for_string(id, kept),
178
184
  NodeKind::Object => {
179
- self.pq.metrics[id].object_len.and_then(|orig| {
185
+ self.order.metrics[id].object_len.and_then(|orig| {
180
186
  if orig > kept { Some(orig - kept) } else { None }
181
187
  })
182
188
  }
@@ -192,7 +198,7 @@ impl<'a> RenderScope<'a> {
192
198
  ) -> String {
193
199
  let config = self.config;
194
200
  let (children_pairs, kept) = self.gather_array_children(id, depth);
195
- let node = &self.pq.nodes[id];
201
+ let node = &self.order.nodes[id];
196
202
  let omitted = self.omitted_for(id, node.kind, kept).unwrap_or(0);
197
203
  if kept == 0 && omitted == 0 {
198
204
  return "[]".to_string();
@@ -219,7 +225,7 @@ impl<'a> RenderScope<'a> {
219
225
  let config = self.config;
220
226
  // Special-case: fileset root in Pseudo/JS templates → head-style sections
221
227
  if id == ROOT_PQ_ID
222
- && self.pq.object_type.get(id) == Some(&ObjectType::Fileset)
228
+ && self.order.object_type.get(id) == Some(&ObjectType::Fileset)
223
229
  && !config.newline.is_empty()
224
230
  {
225
231
  match config.template {
@@ -233,7 +239,7 @@ impl<'a> RenderScope<'a> {
233
239
  }
234
240
  }
235
241
  let (children_pairs, kept) = self.gather_object_children(id, depth);
236
- let node = &self.pq.nodes[id];
242
+ let node = &self.order.nodes[id];
237
243
  let omitted = self.omitted_for(id, node.kind, kept).unwrap_or(0);
238
244
  if kept == 0 && omitted == 0 {
239
245
  return "{}".to_string();
@@ -248,14 +254,15 @@ impl<'a> RenderScope<'a> {
248
254
  space: &config.space,
249
255
  newline: &config.newline,
250
256
  fileset_root: id == ROOT_PQ_ID
251
- && self.pq.object_type.get(id) == Some(&ObjectType::Fileset),
257
+ && self.order.object_type.get(id)
258
+ == Some(&ObjectType::Fileset),
252
259
  };
253
260
  render_object(config.template, &ctx)
254
261
  }
255
262
 
256
263
  fn serialize_string(&mut self, id: usize) -> String {
257
264
  let kept = self.count_kept_children(id);
258
- let node = &self.pq.nodes[id];
265
+ let node = &self.order.nodes[id];
259
266
  let omitted = self.omitted_for(id, node.kind, kept).unwrap_or(0);
260
267
  let full: &str = node.string_value.as_deref().unwrap_or("");
261
268
  if omitted == 0 {
@@ -267,7 +274,7 @@ impl<'a> RenderScope<'a> {
267
274
  }
268
275
 
269
276
  fn serialize_number(&self, id: usize) -> String {
270
- let it = &self.pq.nodes[id];
277
+ let it = &self.order.nodes[id];
271
278
  if let Some(n) = it.number_value.as_ref() {
272
279
  if let Some(i) = n.as_i64() {
273
280
  return i.to_string();
@@ -283,7 +290,7 @@ impl<'a> RenderScope<'a> {
283
290
  }
284
291
 
285
292
  fn serialize_bool(&self, id: usize) -> String {
286
- let it = &self.pq.nodes[id];
293
+ let it = &self.order.nodes[id];
287
294
  match it.bool_value {
288
295
  Some(true) => "true".to_string(),
289
296
  Some(false) | None => "false".to_string(),
@@ -296,7 +303,7 @@ impl<'a> RenderScope<'a> {
296
303
  depth: usize,
297
304
  inline: bool,
298
305
  ) -> String {
299
- let it = &self.pq.nodes[id];
306
+ let it = &self.order.nodes[id];
300
307
  match it.kind {
301
308
  NodeKind::Array => self.serialize_array(id, depth, inline),
302
309
  NodeKind::Object => self.serialize_object(id, depth, inline),
@@ -314,13 +321,13 @@ impl<'a> RenderScope<'a> {
314
321
  ) -> (Vec<ArrayChildPair>, usize) {
315
322
  let mut children_pairs: Vec<ArrayChildPair> = Vec::new();
316
323
  let mut kept = 0usize;
317
- if let Some(children_ids) = self.pq.children.get(id) {
324
+ if let Some(children_ids) = self.order.children.get(id) {
318
325
  for (i, &child_id) in children_ids.iter().enumerate() {
319
- if self.marks[child_id.0] != self.mark_gen {
326
+ if self.inclusion_flags[child_id.0] != self.render_set_id {
320
327
  continue;
321
328
  }
322
329
  kept += 1;
323
- let child_kind = self.pq.nodes[child_id.0].kind;
330
+ let child_kind = self.order.nodes[child_id.0].kind;
324
331
  let rendered =
325
332
  self.serialize_node(child_id.0, depth + 1, false);
326
333
  self.push_array_child_line(
@@ -342,13 +349,13 @@ impl<'a> RenderScope<'a> {
342
349
  ) -> (Vec<ObjectChildPair>, usize) {
343
350
  let mut children_pairs: Vec<ObjectChildPair> = Vec::new();
344
351
  let mut kept = 0usize;
345
- if let Some(children_ids) = self.pq.children.get(id) {
352
+ if let Some(children_ids) = self.order.children.get(id) {
346
353
  for (i, &child_id) in children_ids.iter().enumerate() {
347
- if self.marks[child_id.0] != self.mark_gen {
354
+ if self.inclusion_flags[child_id.0] != self.render_set_id {
348
355
  continue;
349
356
  }
350
357
  kept += 1;
351
- let child = &self.pq.nodes[child_id.0];
358
+ let child = &self.order.nodes[child_id.0];
352
359
  let raw_key = child.key_in_object.as_deref().unwrap_or("");
353
360
  let key = crate::utils::json::json_string(raw_key);
354
361
  let val = self.serialize_node(child_id.0, depth + 1, true);
@@ -362,10 +369,10 @@ impl<'a> RenderScope<'a> {
362
369
  fn serialize_fileset_root_pseudo(&mut self, depth: usize) -> String {
363
370
  let nl = &self.config.newline;
364
371
  let mut out = String::new();
365
- if let Some(children_ids) = self.pq.children.get(ROOT_PQ_ID) {
372
+ if let Some(children_ids) = self.order.children.get(ROOT_PQ_ID) {
366
373
  let mut kept = 0usize;
367
374
  for &child_id in children_ids.iter() {
368
- if self.marks[child_id.0] != self.mark_gen {
375
+ if self.inclusion_flags[child_id.0] != self.render_set_id {
369
376
  continue;
370
377
  }
371
378
  if kept > 0 {
@@ -380,7 +387,7 @@ impl<'a> RenderScope<'a> {
380
387
  );
381
388
  }
382
389
  let total = self
383
- .pq
390
+ .order
384
391
  .metrics
385
392
  .get(ROOT_PQ_ID)
386
393
  .and_then(|m| m.object_len)
@@ -398,13 +405,13 @@ impl<'a> RenderScope<'a> {
398
405
  fn serialize_fileset_root_js(&mut self, depth: usize) -> String {
399
406
  let nl = &self.config.newline;
400
407
  let mut out = String::new();
401
- let Some(children_ids) = self.pq.children.get(ROOT_PQ_ID) else {
408
+ let Some(children_ids) = self.order.children.get(ROOT_PQ_ID) else {
402
409
  return out;
403
410
  };
404
411
  let kept =
405
412
  self.render_js_fileset_sections(&mut out, depth, children_ids, nl);
406
413
  let total = self
407
- .pq
414
+ .order
408
415
  .metrics
409
416
  .get(ROOT_PQ_ID)
410
417
  .and_then(|m| m.object_len)
@@ -422,7 +429,7 @@ impl<'a> RenderScope<'a> {
422
429
  ) -> usize {
423
430
  let mut kept = 0usize;
424
431
  for &child_id in children_ids.iter() {
425
- if self.marks[child_id.0] != self.mark_gen {
432
+ if self.inclusion_flags[child_id.0] != self.render_set_id {
426
433
  continue;
427
434
  }
428
435
  if kept > 0 {
@@ -435,37 +442,63 @@ impl<'a> RenderScope<'a> {
435
442
  }
436
443
  }
437
444
 
438
- /// Render a budget-limited preview directly from the arena using inclusion marks.
439
- pub fn render_arena_with_marks(
445
+ /// Prepare a render set by including the first `top_k` nodes by priority
446
+ /// and all of their ancestors so the output remains structurally valid.
447
+ pub fn prepare_render_set_top_k_and_ancestors(
440
448
  order_build: &PriorityOrder,
441
- budget: usize,
442
- marks: &mut Vec<u32>,
443
- mark_gen: u32,
444
- config: &crate::RenderConfig,
445
- ) -> String {
446
- if marks.len() < order_build.total_nodes {
447
- marks.resize(order_build.total_nodes, 0);
449
+ top_k: usize,
450
+ inclusion_flags: &mut Vec<u32>,
451
+ render_id: u32,
452
+ ) {
453
+ if inclusion_flags.len() < order_build.total_nodes {
454
+ inclusion_flags.resize(order_build.total_nodes, 0);
448
455
  }
449
- // Phase 1: Mark the first `k` nodes (order[..k]) and all their ancestors
450
- let k = budget.min(order_build.total_nodes);
456
+ let k = top_k.min(order_build.total_nodes);
451
457
  crate::utils::graph::mark_top_k_and_ancestors(
452
458
  order_build,
453
459
  k,
454
- marks,
455
- mark_gen,
460
+ inclusion_flags,
461
+ render_id,
456
462
  );
463
+ }
457
464
 
465
+ /// Render using a previously prepared render set (inclusion flags matching `render_id`).
466
+ pub fn render_from_render_set(
467
+ order_build: &PriorityOrder,
468
+ inclusion_flags: &[u32],
469
+ render_id: u32,
470
+ config: &crate::RenderConfig,
471
+ ) -> String {
458
472
  // Root PQ id is a fixed invariant (0).
459
473
  let root_id = ROOT_PQ_ID;
460
474
  let mut scope = RenderScope {
461
- pq: order_build,
462
- marks,
463
- mark_gen,
475
+ order: order_build,
476
+ inclusion_flags,
477
+ render_set_id: render_id,
464
478
  config,
465
479
  };
466
480
  scope.serialize_node(root_id, 0, false)
467
481
  }
468
482
 
483
+ /// Convenience: prepare the render set for `top_k` nodes and render in one call.
484
+ pub fn render_top_k(
485
+ order_build: &PriorityOrder,
486
+ top_k: usize,
487
+ inclusion_flags: &mut Vec<u32>,
488
+ render_id: u32,
489
+ config: &crate::RenderConfig,
490
+ ) -> String {
491
+ prepare_render_set_top_k_and_ancestors(
492
+ order_build,
493
+ top_k,
494
+ inclusion_flags,
495
+ render_id,
496
+ );
497
+ render_from_render_set(order_build, inclusion_flags, render_id, config)
498
+ }
499
+
500
+ //
501
+
469
502
  #[cfg(test)]
470
503
  mod tests {
471
504
  use super::*;
@@ -485,7 +518,7 @@ mod tests {
485
518
  )
486
519
  .unwrap();
487
520
  let mut marks = vec![0u32; build.total_nodes];
488
- let out = render_arena_with_marks(
521
+ let out = render_top_k(
489
522
  &build,
490
523
  10,
491
524
  &mut marks,
@@ -516,7 +549,7 @@ mod tests {
516
549
  )
517
550
  .unwrap();
518
551
  let mut marks = vec![0u32; build.total_nodes];
519
- let out = render_arena_with_marks(
552
+ let out = render_top_k(
520
553
  &build,
521
554
  usize::MAX,
522
555
  &mut marks,
@@ -551,7 +584,7 @@ mod tests {
551
584
  )
552
585
  .unwrap();
553
586
  let mut marks = vec![0u32; build.total_nodes];
554
- let out = render_arena_with_marks(
587
+ let out = render_top_k(
555
588
  &build,
556
589
  10,
557
590
  &mut marks,
@@ -566,4 +599,45 @@ mod tests {
566
599
  );
567
600
  assert_snapshot!("arena_render_single", out);
568
601
  }
602
+
603
+ #[test]
604
+ fn arena_render_object_partial_js() {
605
+ // Object with three properties; render top_k small so only one child is kept.
606
+ let arena = crate::json_ingest::build_json_tree_arena(
607
+ "{\"a\":1,\"b\":2,\"c\":3}",
608
+ &crate::PriorityConfig::new(usize::MAX, usize::MAX),
609
+ )
610
+ .unwrap();
611
+ let build = build_order(
612
+ &arena,
613
+ &crate::PriorityConfig::new(usize::MAX, usize::MAX),
614
+ )
615
+ .unwrap();
616
+ let mut flags = vec![0u32; build.total_nodes];
617
+ // top_k=2 → root object + first property
618
+ let out = render_top_k(
619
+ &build,
620
+ 2,
621
+ &mut flags,
622
+ 1,
623
+ &crate::RenderConfig {
624
+ template: crate::OutputTemplate::Js,
625
+ indent_unit: " ".to_string(),
626
+ space: " ".to_string(),
627
+ newline: "\n".to_string(),
628
+ prefer_tail_arrays: false,
629
+ },
630
+ );
631
+ // Should be a valid JS object with one property and an omitted summary.
632
+ assert!(out.starts_with("{\n"));
633
+ assert!(
634
+ out.contains("/* 2 more properties */"),
635
+ "missing omitted summary: {out:?}"
636
+ );
637
+ assert!(
638
+ out.contains("\"a\": 1")
639
+ || out.contains("\"b\": 2")
640
+ || out.contains("\"c\": 3")
641
+ );
642
+ }
569
643
  }
@@ -0,0 +1,61 @@
1
+ use crate::order::{NodeId, PriorityOrder};
2
+
3
+ /// Seed the work stack by including the first `k` nodes in global priority order.
4
+ fn seed_stack_with_top_k(
5
+ order: &PriorityOrder,
6
+ k: usize,
7
+ inclusion_flags: &mut [u32],
8
+ render_id: u32,
9
+ work_stack: &mut Vec<NodeId>,
10
+ ) {
11
+ for &id in order.by_priority.iter().take(k) {
12
+ let idx = id.0;
13
+ if inclusion_flags[idx] != render_id {
14
+ inclusion_flags[idx] = render_id;
15
+ work_stack.push(id);
16
+ }
17
+ }
18
+ }
19
+
20
+ /// Pop from the work stack; for each node include its parent; continue until empty.
21
+ fn propagate_marks_to_ancestors(
22
+ parent: &[Option<NodeId>],
23
+ inclusion_flags: &mut [u32],
24
+ render_id: u32,
25
+ work_stack: &mut Vec<NodeId>,
26
+ ) {
27
+ while let Some(id) = work_stack.pop() {
28
+ let idx = id.0;
29
+ match parent[idx] {
30
+ Some(parent) if inclusion_flags[parent.0] != render_id => {
31
+ inclusion_flags[parent.0] = render_id;
32
+ work_stack.push(parent);
33
+ }
34
+ _ => {}
35
+ }
36
+ }
37
+ }
38
+
39
+ /// Include the first `k` nodes by global priority order and all of their ancestors
40
+ /// in the current render set (identified by `render_id`).
41
+ pub(crate) fn mark_top_k_and_ancestors(
42
+ order: &PriorityOrder,
43
+ k: usize,
44
+ inclusion_flags: &mut [u32],
45
+ render_id: u32,
46
+ ) {
47
+ let mut work_stack: Vec<NodeId> = Vec::new();
48
+ seed_stack_with_top_k(
49
+ order,
50
+ k,
51
+ inclusion_flags,
52
+ render_id,
53
+ &mut work_stack,
54
+ );
55
+ propagate_marks_to_ancestors(
56
+ &order.parent,
57
+ inclusion_flags,
58
+ render_id,
59
+ &mut work_stack,
60
+ );
61
+ }
@@ -1,54 +0,0 @@
1
- use crate::order::{NodeId, PriorityOrder};
2
-
3
- /// Seed the work stack by marking the first `k` nodes in global order.
4
- fn seed_stack_with_top_k(
5
- order: &PriorityOrder,
6
- k: usize,
7
- marks: &mut [u32],
8
- mark_gen: u32,
9
- work_stack: &mut Vec<NodeId>,
10
- ) {
11
- for &id in order.order.iter().take(k) {
12
- let idx = id.0;
13
- if marks[idx] != mark_gen {
14
- marks[idx] = mark_gen;
15
- work_stack.push(id);
16
- }
17
- }
18
- }
19
-
20
- /// Pop from the work stack; for each node mark its parent; continue until empty.
21
- fn propagate_marks_to_ancestors(
22
- parent: &[Option<NodeId>],
23
- marks: &mut [u32],
24
- mark_gen: u32,
25
- work_stack: &mut Vec<NodeId>,
26
- ) {
27
- while let Some(id) = work_stack.pop() {
28
- let idx = id.0;
29
- match parent[idx] {
30
- Some(parent) if marks[parent.0] != mark_gen => {
31
- marks[parent.0] = mark_gen;
32
- work_stack.push(parent);
33
- }
34
- _ => {}
35
- }
36
- }
37
- }
38
-
39
- /// Mark the first `k` nodes by global order and all of their ancestors.
40
- pub(crate) fn mark_top_k_and_ancestors(
41
- order: &PriorityOrder,
42
- k: usize,
43
- marks: &mut [u32],
44
- mark_gen: u32,
45
- ) {
46
- let mut work_stack: Vec<NodeId> = Vec::new();
47
- seed_stack_with_top_k(order, k, marks, mark_gen, &mut work_stack);
48
- propagate_marks_to_ancestors(
49
- &order.parent,
50
- marks,
51
- mark_gen,
52
- &mut work_stack,
53
- );
54
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes