types: unify all epoch timestamps to i64

All epoch timestamp fields (timestamp, last_replayed, created_at on
nodes; timestamp on relations) are now i64. Previously a mix of f64
and i64 which caused type seams and required unnecessary casts.

- Kill now_epoch() -> f64 and now_epoch_i64(), replace with single
  now_epoch() -> i64
- All formatting functions take i64
- new_node() sets created_at automatically
- journal-ts-migrate handles all nodes, with valid_range check to
  detect garbage from f64->i64 bit reinterpretation
- capnp schema: Float64 -> Int64 for all timestamp fields
This commit is contained in:
ProofOfConcept 2026-03-05 10:23:57 -05:00
parent b4bbafdf1c
commit 4747004b36
4 changed files with 232 additions and 56 deletions

View file

@ -120,18 +120,18 @@ impl StoreLock {
// Lock released automatically when _file is dropped (flock semantics)
}
pub fn now_epoch() -> f64 {
pub fn now_epoch() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64()
.as_secs() as i64
}
/// Convert epoch seconds to broken-down local time components.
/// Returns (year, month, day, hour, minute, second).
pub fn epoch_to_local(epoch: f64) -> (i32, u32, u32, u32, u32, u32) {
pub fn epoch_to_local(epoch: i64) -> (i32, u32, u32, u32, u32, u32) {
use chrono::{Datelike, Local, TimeZone, Timelike};
let dt = Local.timestamp_opt(epoch as i64, 0).unwrap();
let dt = Local.timestamp_opt(epoch, 0).unwrap();
(
dt.year(),
dt.month(),
@ -143,19 +143,19 @@ pub fn epoch_to_local(epoch: f64) -> (i32, u32, u32, u32, u32, u32) {
}
/// Format epoch as "YYYY-MM-DD"
pub fn format_date(epoch: f64) -> String {
pub fn format_date(epoch: i64) -> String {
let (y, m, d, _, _, _) = epoch_to_local(epoch);
format!("{:04}-{:02}-{:02}", y, m, d)
}
/// Format epoch as "YYYY-MM-DDTHH:MM"
pub fn format_datetime(epoch: f64) -> String {
pub fn format_datetime(epoch: i64) -> String {
let (y, m, d, h, min, _) = epoch_to_local(epoch);
format!("{:04}-{:02}-{:02}T{:02}:{:02}", y, m, d, h, min)
}
/// Format epoch as "YYYY-MM-DD HH:MM"
pub fn format_datetime_space(epoch: f64) -> String {
pub fn format_datetime_space(epoch: i64) -> String {
let (y, m, d, h, min, _) = epoch_to_local(epoch);
format!("{:04}-{:02}-{:02} {:02}:{:02}", y, m, d, h, min)
}
@ -170,7 +170,7 @@ pub fn today() -> String {
pub struct Node {
pub uuid: [u8; 16],
pub version: u32,
pub timestamp: f64,
pub timestamp: i64,
pub node_type: NodeType,
pub provenance: Provenance,
pub key: String,
@ -185,13 +185,18 @@ pub struct Node {
pub uses: u32,
pub wrongs: u32,
pub state_tag: String,
pub last_replayed: f64,
pub last_replayed: i64,
pub spaced_repetition_interval: u32,
// Position within file (section index, for export ordering)
#[serde(default)]
pub position: u32,
// Stable creation timestamp (unix epoch seconds). Set once at creation;
// never updated on rename or content update. Zero for legacy nodes.
#[serde(default)]
pub created_at: i64,
// Derived fields (not in capnp, computed from graph)
#[serde(default)]
pub community_id: Option<u32>,
@ -206,7 +211,7 @@ pub struct Node {
pub struct Relation {
pub uuid: [u8; 16],
pub version: u32,
pub timestamp: f64,
pub timestamp: i64,
pub source: [u8; 16],
pub target: [u8; 16],
pub rel_type: RelationType,
@ -306,7 +311,7 @@ capnp_message!(Node,
uuid: [uuid],
prim: [version, timestamp, weight, emotion, deleted,
retrievals, uses, wrongs, last_replayed,
spaced_repetition_interval, position],
spaced_repetition_interval, position, created_at],
enm: [node_type: NodeType, provenance: Provenance, category: Category],
skip: [community_id, clustering_coefficient, degree],
);
@ -444,9 +449,10 @@ pub fn new_node(key: &str, content: &str) -> Node {
uses: 0,
wrongs: 0,
state_tag: String::new(),
last_replayed: 0.0,
last_replayed: 0,
spaced_repetition_interval: 1,
position: 0,
created_at: now_epoch(),
community_id: None,
clustering_coefficient: None,
degree: None,