// moomem 唯一的 JSON 编解码处(共享约定 7.2)。
// 使用 core/json;JSONL 一行一条 entry;快照首行为 header。
//
// 快照格式:
// {"v":1,"gen":<递增代数>,"clock":<逻辑时钟>}
// {entry JSON}
// {entry JSON}
// ...
//
// 内部函数采用 raise 风格传播错误,包内入口统一返回 Result[T, MoomemError]。
// ---------------------------------------------------------------------------
// MemoryEntry 编解码
// ---------------------------------------------------------------------------
///|
/// MemoryEntry → Json(字段名与磁盘格式一一对应,恢复时不必重算 keywords)。
fn entry_to_json(e : MemoryEntry) -> Json {
let obj : Map[String, Json] = Map([])
obj.set("id", Json::string(e.id))
obj.set("user_id", Json::string(e.user_id))
obj.set("content", Json::string(e.content))
obj.set("kind", kind_to_json(e.kind))
obj.set("embedding", Json::array(e.embedding.map(v => Json::number(v))))
obj.set("keywords", Json::array(e.keywords.map(k => Json::string(k))))
obj.set("status", status_to_json(e.status))
obj.set("created_at", Json::number(Int64::to_double(e.created_at)))
obj.set(
"superseded_by",
match e.superseded_by {
Some(id) => Json::string(id)
None => Json::null()
},
)
obj.set("seq", Json::number(e.seq.to_double()))
obj.set("metadata", Json::object(e.metadata))
Json::object(obj)
}
///|
/// Json → MemoryEntry;字段缺失 / 类型不符 → StoreCorrupted。
fn entry_from_json(j : Json) -> Result[MemoryEntry, MoomemError] {
Ok(entry_from_json_impl(j)) catch {
e => Err(e)
}
}
///|
fn entry_from_json_impl(j : Json) -> MemoryEntry raise MoomemError {
let obj = match j {
Object(o) => o
_ => raise MoomemError::StoreCorrupted("entry is not a JSON object")
}
let id = get_string_field(obj, "id")
let user_id = get_string_field(obj, "user_id")
let content = get_string_field(obj, "content")
let kind = match obj.get("kind") {
Some(k) => kind_from_json(k)
None => raise MoomemError::StoreCorrupted("entry missing field 'kind'")
}
let embedding = get_double_array_field(obj, "embedding")
let keywords = get_string_array_field(obj, "keywords")
let status = match obj.get("status") {
Some(s) => status_from_json(s)
None => raise MoomemError::StoreCorrupted("entry missing field 'status'")
}
let created_at = get_int64_field(obj, "created_at")
let superseded_by = match obj.get("superseded_by") {
Some(String(s)) => Some(s)
Some(Null) => None
Some(_) =>
raise MoomemError::StoreCorrupted(
"field 'superseded_by' is not a string or null",
)
None => None
}
let seq = get_int_field(obj, "seq")
let metadata : Map[String, Json] = Map([])
match obj.get("metadata") {
Some(Object(m)) => m.each((k, v) => metadata.set(k, v))
Some(Null) => ()
Some(_) =>
raise MoomemError::StoreCorrupted("field 'metadata' is not an object")
None => ()
}
MemoryEntry::{
id,
user_id,
content,
kind,
embedding,
keywords,
status,
created_at,
superseded_by,
seq,
metadata,
}
}
// ---------------------------------------------------------------------------
// 状态与类型的字符串编解码
// ---------------------------------------------------------------------------
///|
fn status_to_json(s : EntryStatus) -> Json {
match s {
Active => Json::string("active")
Superseded => Json::string("superseded")
Deleted => Json::string("deleted")
Unstructured => Json::string("unstructured")
}
}
///|
fn status_from_json(j : Json) -> EntryStatus raise MoomemError {
match j {
String("active") => Active
String("superseded") => Superseded
String("deleted") => Deleted
String("unstructured") => Unstructured
String(other) =>
raise MoomemError::StoreCorrupted("unknown status '\{other}'")
_ => raise MoomemError::StoreCorrupted("status is not a string")
}
}
///|
fn kind_to_json(k : EntryKind) -> Json {
match k {
Fact => Json::string("fact")
Preference => Json::string("preference")
Event => Json::string("event")
Unstructured => Json::string("unstructured")
}
}
///|
fn kind_from_json(j : Json) -> EntryKind raise MoomemError {
match j {
String("fact") => Fact
String("preference") => Preference
String("event") => Event
String("unstructured") => Unstructured
String(other) =>
raise MoomemError::StoreCorrupted("unknown kind '\{other}'")
_ => raise MoomemError::StoreCorrupted("kind is not a string")
}
}
// ---------------------------------------------------------------------------
// 字段读取辅助(类型不符一律 raise StoreCorrupted)
// ---------------------------------------------------------------------------
///|
fn get_string_field(
obj : Map[String, Json],
key : String,
) -> String raise MoomemError {
match obj.get(key) {
Some(String(s)) => s
Some(_) =>
raise MoomemError::StoreCorrupted("field '\{key}' is not a string")
None => raise MoomemError::StoreCorrupted("field '\{key}' is missing")
}
}
///|
fn get_int_field(
obj : Map[String, Json],
key : String,
) -> Int32 raise MoomemError {
match obj.get(key) {
Some(Number(n, ..)) => n.to_int()
Some(_) =>
raise MoomemError::StoreCorrupted("field '\{key}' is not a number")
None => raise MoomemError::StoreCorrupted("field '\{key}' is missing")
}
}
///|
fn get_int64_field(
obj : Map[String, Json],
key : String,
) -> Int64 raise MoomemError {
match obj.get(key) {
Some(Number(n, ..)) => n.to_int64()
Some(_) =>
raise MoomemError::StoreCorrupted("field '\{key}' is not a number")
None => raise MoomemError::StoreCorrupted("field '\{key}' is missing")
}
}
///|
fn get_double_array_field(
obj : Map[String, Json],
key : String,
) -> Array[Double] raise MoomemError {
match obj.get(key) {
Some(Array(items)) => {
let out : Array[Double] = []
for item in items {
match item {
Number(n, ..) => out.push(n)
_ =>
raise MoomemError::StoreCorrupted(
"field '\{key}' contains non-number item",
)
}
}
out
}
Some(_) =>
raise MoomemError::StoreCorrupted("field '\{key}' is not an array")
None => raise MoomemError::StoreCorrupted("field '\{key}' is missing")
}
}
///|
fn get_string_array_field(
obj : Map[String, Json],
key : String,
) -> Array[String] raise MoomemError {
match obj.get(key) {
Some(Array(items)) => {
let out : Array[String] = []
for item in items {
match item {
String(s) => out.push(s)
_ =>
raise MoomemError::StoreCorrupted(
"field '\{key}' contains non-string item",
)
}
}
out
}
Some(_) =>
raise MoomemError::StoreCorrupted("field '\{key}' is not an array")
None => raise MoomemError::StoreCorrupted("field '\{key}' is missing")
}
}
// ---------------------------------------------------------------------------
// 快照 header / 整体编解码
// ---------------------------------------------------------------------------
///|
/// 快照 header:{"v":1,"gen":..,"clock":..}
fn snapshot_header_to_json(v : Int32, gen : Int32, clock : Int64) -> Json {
let obj : Map[String, Json] = Map([])
obj.set("v", Json::number(v.to_double()))
obj.set("gen", Json::number(gen.to_double()))
obj.set("clock", Json::number(Int64::to_double(clock)))
Json::object(obj)
}
///|
/// 解析 header 行;校验版本号。返回 (v, gen, clock)。
fn parse_snapshot_header(
j : Json,
) -> Result[(Int32, Int32, Int64), MoomemError] {
try {
let obj = match j {
Object(o) => o
_ =>
raise MoomemError::StoreCorrupted(
"snapshot header is not a JSON object",
)
}
let v = get_int_field(obj, "v")
if v != SNAPSHOT_VERSION {
raise MoomemError::StoreCorrupted(
"snapshot version mismatch: expected \{SNAPSHOT_VERSION}, got \{v}",
)
}
let gen = get_int_field(obj, "gen")
let clock = get_int64_field(obj, "clock")
Ok((v, gen, clock))
} catch {
e => Err(e)
}
}
///|
/// 解析单行 JSON(空行 / 语法错误 → StoreCorrupted)。
fn parse_json_line(line : String) -> Result[Json, MoomemError] {
try {
let trimmed = line.trim()
if trimmed.length() == 0 {
raise MoomemError::StoreCorrupted("blank line in snapshot")
}
Ok(@json.parse(trimmed))
} catch {
e => Err(MoomemError::StoreCorrupted("invalid JSON line: \{e.to_string()}"))
}
}
///|
/// 组装完整快照文本:header 行 + 每条 entry 一行 JSON。
fn build_snapshot_text(
gen : Int32,
clock : Int64,
entries : Array[MemoryEntry],
) -> String {
let buf = StringBuilder()
buf.write_string(
snapshot_header_to_json(SNAPSHOT_VERSION, gen, clock).stringify(),
)
buf.write_char('\n')
for e in entries {
buf.write_string(entry_to_json(e).stringify())
buf.write_char('\n')
}
buf.to_string()
}
///|
/// 快照解析结果:header 代数与时钟 + 条目列表(按文件序) + 截断计数。
priv struct SnapshotParseResult {
gen : Int32
clock : Int64
entries : Array[MemoryEntry]
truncated_recovered : Int32
}
///|
/// 解析快照文本。
/// 规则:
/// - 首行必须是合法 header 且版本匹配,否则 StoreCorrupted;
/// - 尾部不完整行(半行损坏)丢弃并计入 truncated_recovered;
/// - 中部损坏行不属于尾部截断,报 StoreCorrupted。
fn parse_snapshot_text(
text : String,
) -> Result[SnapshotParseResult, MoomemError] {
Ok(parse_snapshot_text_impl(text)) catch {
e => Err(e)
}
}
///|
fn parse_snapshot_text_impl(
text : String,
) -> SnapshotParseResult raise MoomemError {
// 收集行并剥掉尾部空行
let lines : Array[String] = []
for raw in text.split("\n") {
lines.push(raw.to_owned())
}
while lines.length() > 0 && lines[lines.length() - 1].trim().length() == 0 {
ignore(lines.pop())
}
if lines.length() == 0 {
raise MoomemError::StoreCorrupted("empty snapshot")
}
let header_json = parse_json_line_impl(lines[0])
let obj = match header_json {
Object(o) => o
_ =>
raise MoomemError::StoreCorrupted("snapshot header is not a JSON object")
}
let v = get_int_field(obj, "v")
if v != SNAPSHOT_VERSION {
raise MoomemError::StoreCorrupted(
"snapshot version mismatch: expected \{SNAPSHOT_VERSION}, got \{v}",
)
}
let gen = get_int_field(obj, "gen")
let clock = get_int64_field(obj, "clock")
let entries : Array[MemoryEntry] = []
let mut truncated = 0
for i = 1; i < lines.length(); i = i + 1 {
let line = lines[i]
if line.trim().length() == 0 {
raise MoomemError::StoreCorrupted(
"blank entry line at \{i + 1} (not a tail truncation)",
)
}
let parsed_ok : Bool = try {
let j = parse_json_line_impl(line)
let entry = entry_from_json_impl(j)
entries.push(entry)
true
} catch {
_ => false
}
if !parsed_ok {
if i == lines.length() - 1 {
// 尾部半行损坏:丢弃并计数
truncated = truncated + 1
} else {
raise MoomemError::StoreCorrupted(
"corrupted entry at line \{i + 1} (not a tail truncation)",
)
}
}
}
SnapshotParseResult::{ gen, clock, entries, truncated_recovered: truncated, }
}
///|
fn parse_json_line_impl(line : String) -> Json raise MoomemError {
let trimmed = line.trim().to_owned()
if trimmed.length() == 0 {
raise MoomemError::StoreCorrupted("blank line in snapshot")
}
@json.parse(trimmed) catch {
e =>
raise MoomemError::StoreCorrupted("invalid JSON line: \{e.to_string()}")
}
}