add position field to nodes for stable section ordering
Sections within a file have a natural order that matters — identity.md reads as a narrative, not an alphabetical index. The position field (u32) tracks section index within the file. Set during init and import from parse order. Export and load-context sort by position instead of key, preserving the author's intended structure.
This commit is contained in:
parent
57cf61de44
commit
7b811125ca
3 changed files with 22 additions and 14 deletions
|
|
@ -98,6 +98,10 @@ pub struct Node {
|
||||||
pub last_replayed: f64,
|
pub last_replayed: f64,
|
||||||
pub spaced_repetition_interval: u32,
|
pub spaced_repetition_interval: u32,
|
||||||
|
|
||||||
|
// Position within file (section index, for export ordering)
|
||||||
|
#[serde(default)]
|
||||||
|
pub position: u32,
|
||||||
|
|
||||||
// Derived fields (not in capnp, computed from graph)
|
// Derived fields (not in capnp, computed from graph)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub community_id: Option<u32>,
|
pub community_id: Option<u32>,
|
||||||
|
|
@ -488,6 +492,7 @@ impl Store {
|
||||||
state_tag: String::new(),
|
state_tag: String::new(),
|
||||||
last_replayed: 0.0,
|
last_replayed: 0.0,
|
||||||
spaced_repetition_interval: 1,
|
spaced_repetition_interval: 1,
|
||||||
|
position: 0,
|
||||||
community_id: None,
|
community_id: None,
|
||||||
clustering_coefficient: None,
|
clustering_coefficient: None,
|
||||||
schema_fit: None,
|
schema_fit: None,
|
||||||
|
|
@ -566,14 +571,16 @@ impl Store {
|
||||||
NodeType::Semantic
|
NodeType::Semantic
|
||||||
};
|
};
|
||||||
|
|
||||||
for unit in &units {
|
for (pos, unit) in units.iter().enumerate() {
|
||||||
seen_keys.insert(unit.key.clone());
|
seen_keys.insert(unit.key.clone());
|
||||||
|
|
||||||
if let Some(existing) = self.nodes.get(&unit.key) {
|
if let Some(existing) = self.nodes.get(&unit.key) {
|
||||||
// Update if content changed
|
// Update if content or position changed
|
||||||
if existing.content != unit.content {
|
let pos_changed = existing.position != pos as u32;
|
||||||
|
if existing.content != unit.content || pos_changed {
|
||||||
let mut node = existing.clone();
|
let mut node = existing.clone();
|
||||||
node.content = unit.content.clone();
|
node.content = unit.content.clone();
|
||||||
|
node.position = pos as u32;
|
||||||
node.version += 1;
|
node.version += 1;
|
||||||
if let Some(ref state) = unit.state {
|
if let Some(ref state) = unit.state {
|
||||||
node.state_tag = state.clone();
|
node.state_tag = state.clone();
|
||||||
|
|
@ -586,6 +593,7 @@ impl Store {
|
||||||
} else {
|
} else {
|
||||||
let mut node = Store::new_node(&unit.key, &unit.content);
|
let mut node = Store::new_node(&unit.key, &unit.content);
|
||||||
node.node_type = node_type;
|
node.node_type = node_type;
|
||||||
|
node.position = pos as u32;
|
||||||
if let Some(ref state) = unit.state {
|
if let Some(ref state) = unit.state {
|
||||||
node.state_tag = state.clone();
|
node.state_tag = state.clone();
|
||||||
}
|
}
|
||||||
|
|
@ -1098,6 +1106,7 @@ fn read_content_node(r: memory_capnp::content_node::Reader) -> Result<Node, Stri
|
||||||
state_tag: read_text(r.get_state_tag()),
|
state_tag: read_text(r.get_state_tag()),
|
||||||
last_replayed: r.get_last_replayed(),
|
last_replayed: r.get_last_replayed(),
|
||||||
spaced_repetition_interval: r.get_spaced_repetition_interval(),
|
spaced_repetition_interval: r.get_spaced_repetition_interval(),
|
||||||
|
position: 0,
|
||||||
community_id: None,
|
community_id: None,
|
||||||
clustering_coefficient: None,
|
clustering_coefficient: None,
|
||||||
schema_fit: None,
|
schema_fit: None,
|
||||||
|
|
|
||||||
19
src/main.rs
19
src/main.rs
|
|
@ -855,7 +855,7 @@ fn cmd_load_context() -> Result<(), String> {
|
||||||
.filter(|n| n.key == *key || n.key.starts_with(&prefix))
|
.filter(|n| n.key == *key || n.key.starts_with(&prefix))
|
||||||
.collect();
|
.collect();
|
||||||
if sections.is_empty() { continue; }
|
if sections.is_empty() { continue; }
|
||||||
sections.sort_by(|a, b| a.key.cmp(&b.key));
|
sections.sort_by(|a, b| a.position.cmp(&b.position));
|
||||||
|
|
||||||
println!("--- {} ({}) ---", key, label);
|
println!("--- {} ({}) ---", key, label);
|
||||||
for node in §ions {
|
for node in §ions {
|
||||||
|
|
@ -1018,11 +1018,13 @@ fn import_file(store: &mut capnp_store::Store, path: &std::path::Path) -> Result
|
||||||
capnp_store::NodeType::Semantic
|
capnp_store::NodeType::Semantic
|
||||||
};
|
};
|
||||||
|
|
||||||
for unit in &units {
|
for (pos, unit) in units.iter().enumerate() {
|
||||||
if let Some(existing) = store.nodes.get(&unit.key) {
|
if let Some(existing) = store.nodes.get(&unit.key) {
|
||||||
if existing.content != unit.content {
|
let pos_changed = existing.position != pos as u32;
|
||||||
|
if existing.content != unit.content || pos_changed {
|
||||||
let mut node = existing.clone();
|
let mut node = existing.clone();
|
||||||
node.content = unit.content.clone();
|
node.content = unit.content.clone();
|
||||||
|
node.position = pos as u32;
|
||||||
node.version += 1;
|
node.version += 1;
|
||||||
println!(" U {}", unit.key);
|
println!(" U {}", unit.key);
|
||||||
updated_nodes.push(node);
|
updated_nodes.push(node);
|
||||||
|
|
@ -1030,6 +1032,7 @@ fn import_file(store: &mut capnp_store::Store, path: &std::path::Path) -> Result
|
||||||
} else {
|
} else {
|
||||||
let mut node = capnp_store::Store::new_node(&unit.key, &unit.content);
|
let mut node = capnp_store::Store::new_node(&unit.key, &unit.content);
|
||||||
node.node_type = node_type;
|
node.node_type = node_type;
|
||||||
|
node.position = pos as u32;
|
||||||
println!(" + {}", unit.key);
|
println!(" + {}", unit.key);
|
||||||
new_nodes.push(node);
|
new_nodes.push(node);
|
||||||
}
|
}
|
||||||
|
|
@ -1091,15 +1094,9 @@ fn cmd_export(args: &[String]) -> Result<(), String> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort: file-level key first (no #), then sections alphabetically
|
// Sort by position (preserves original file order)
|
||||||
sections.sort_by(|a, b| {
|
sections.sort_by(|a, b| {
|
||||||
let a_is_file = !a.key.contains('#');
|
a.position.cmp(&b.position)
|
||||||
let b_is_file = !b.key.contains('#');
|
|
||||||
match (a_is_file, b_is_file) {
|
|
||||||
(true, false) => std::cmp::Ordering::Less,
|
|
||||||
(false, true) => std::cmp::Ordering::Greater,
|
|
||||||
_ => a.key.cmp(&b.key),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build output: file-level content first, then each section
|
// Build output: file-level content first, then each section
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,7 @@ pub fn migrate() -> Result<(), String> {
|
||||||
state_tag,
|
state_tag,
|
||||||
last_replayed: 0.0,
|
last_replayed: 0.0,
|
||||||
spaced_repetition_interval: 1,
|
spaced_repetition_interval: 1,
|
||||||
|
position: 0,
|
||||||
community_id: None,
|
community_id: None,
|
||||||
clustering_coefficient: None,
|
clustering_coefficient: None,
|
||||||
schema_fit: None,
|
schema_fit: None,
|
||||||
|
|
@ -266,6 +267,7 @@ pub fn migrate() -> Result<(), String> {
|
||||||
state_tag: unit.state.clone().unwrap_or_default(),
|
state_tag: unit.state.clone().unwrap_or_default(),
|
||||||
last_replayed: 0.0,
|
last_replayed: 0.0,
|
||||||
spaced_repetition_interval: 1,
|
spaced_repetition_interval: 1,
|
||||||
|
position: 0,
|
||||||
community_id: None,
|
community_id: None,
|
||||||
clustering_coefficient: None,
|
clustering_coefficient: None,
|
||||||
schema_fit: None,
|
schema_fit: None,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue