///|
/// Stable, reviewable identity for one validated behavior tree asset.
pub(all) struct AssetIdentity {
root : String
node_count : Int
blackboard_entries : Int
canonical_dsl : String
fingerprint : String
} derive(Debug, Eq)
///|
pub fn AssetIdentity::summary(self : AssetIdentity) -> String {
"root=" +
self.root +
", nodes=" +
self.node_count.to_string() +
", blackboard=" +
self.blackboard_entries.to_string() +
", fingerprint=" +
self.fingerprint
}
///|
pub fn AssetIdentity::markdown(self : AssetIdentity) -> String {
"# MoonBTKit Asset Identity\n\n" +
"- " +
self.summary() +
"\n\n## Canonical DSL\n\n```text\n" +
self.canonical_dsl +
"\n```"
}
///|
/// Normalize comments, whitespace, blackboard order, and declaration order.
/// Child order remains unchanged because it affects behavior tree semantics.
pub fn canonicalize_dsl(source : String) -> Result[String, BtError] {
match parse_dsl(source) {
Ok(doc) => Ok(canonicalize_document(doc))
Err(err) => Err(err)
}
}
///|
/// Produce a deterministic, non-cryptographic fingerprint for cache keys,
/// version comparisons, and regression asset identity.
pub fn asset_fingerprint(source : String) -> Result[String, BtError] {
match canonicalize_dsl(source) {
Ok(canonical) =>
Ok("asset-v2-" + stable_digest("moonbtkit-asset-v2\n" + canonical))
Err(err) => Err(err)
}
}
///|
pub fn inspect_asset(source : String) -> Result[AssetIdentity, BtError] {
match parse_dsl(source) {
Ok(doc) => {
let canonical = canonicalize_document(doc)
Ok({
root: doc.tree.root,
node_count: doc.tree.node_count(),
blackboard_entries: doc.blackboard.len(),
canonical_dsl: canonical,
fingerprint: "asset-v2-" +
stable_digest("moonbtkit-asset-v2\n" + canonical),
})
}
Err(err) => Err(err)
}
}
///|
fn canonicalize_document(doc : DslDocument) -> String {
let lines = Array::new()
lines.push("root " + doc.tree.root)
let board_lines = named_blackboard_lines(doc.blackboard)
sort_named_lines(board_lines)
let mut b = 0
while b < board_lines.length() {
lines.push(board_lines[b].1)
b = b + 1
}
let node_lines = named_node_lines(doc.tree)
sort_named_lines(node_lines)
let mut n = 0
while n < node_lines.length() {
lines.push(node_lines[n].1)
n = n + 1
}
join_strings(lines, "\n")
}
///|
fn named_blackboard_lines(board : Blackboard) -> Array[(String, String)] {
let values = board.snapshot()
let serialized = serialize_blackboard(board)
let out = Array::new()
let mut i = 0
while i < values.length() {
out.push((values[i].0, serialized[i]))
i = i + 1
}
out
}
///|
fn named_node_lines(tree : BehaviorTree) -> Array[(String, String)] {
let serialized = serialize_tree(tree)
let out = Array::new()
let mut i = 0
while i < tree.nodes.length() {
out.push((tree.nodes[i].id, serialized[i + 1]))
i = i + 1
}
out
}
///|
fn sort_named_lines(values : Array[(String, String)]) -> Unit {
let mut i = 0
while i < values.length() {
let mut best = i
let mut j = i + 1
while j < values.length() {
if string_less(values[j].0, values[best].0) {
best = j
}
j = j + 1
}
if best != i {
let temp = values[i]
values[i] = values[best]
values[best] = temp
}
i = i + 1
}
}
///|
fn string_less(left : String, right : String) -> Bool {
let limit = if left.length() < right.length() {
left.length()
} else {
right.length()
}
let mut i = 0
while i < limit {
if left[i] < right[i] {
return true
}
if left[i] > right[i] {
return false
}
i = i + 1
}
left.length() < right.length()
}