# ---
# tags: cyber, nushell
# crystal-type: source
# crystal-domain: cyber
# ---
# trikernel.nu — compute tri-kernel focus distribution and write to frontmatter
#
# Three operators over the wiki-link graph:
# D (diffusion) — PageRank: where does probability flow?
# S (springs) — screened Laplacian: mean neighbor focus (structural equilibrium)
# H (heat) — 2-hop smoothed focus (multi-scale context at resolution τ)
#
# Composite: focus = norm(λd·D + λs·S + λh·H), λd + λs + λh = 1
#
# Fields written to frontmatter:
# diffusion: — D component (PageRank probability)
# springs: — S component (neighbor equilibrium)
# heat: — H component (2-hop smoothed)
# focus: — composite tri-kernel score (normalized, sums to 1)
# gravity: — raw inbound link count
# density: — outbound wiki-links per KB of content
#
# Usage:
# nu analizer/trikernel.nu ~/git/cyber # graph only
# nu analizer/trikernel.nu ~/git/cyber --subgraphs # include subgraph repos
# nu analizer/trikernel.nu ~/git/cyber --dry-run # compute and print, don't write
# nu analizer/trikernel.nu ~/git/cyber --iterations 30 # more iterations (default 20)
# nu analizer/trikernel.nu ~/git/cyber --force # bypass new moon check
#
# Schedule: weights are updated once per lunar cycle (new moon).
# The script checks the date and refuses to write unless --force is passed
# or today is within ±1 day of a new moon.
# new moon dates 2026-2027 (UTC)
const NEW_MOONS = [
"2026-01-18" "2026-02-17" "2026-03-19" "2026-04-17" "2026-05-16"
"2026-06-15" "2026-07-14" "2026-08-12" "2026-09-11" "2026-10-10"
"2026-11-09" "2026-12-08"
"2027-01-07" "2027-02-06" "2027-03-08" "2027-04-06" "2027-05-06"
"2027-06-04" "2027-07-04" "2027-08-02" "2027-09-01" "2027-10-01"
"2027-10-30" "2027-11-29" "2027-12-28"
]
def current-moon [] {
# returns the new moon date if today is within ±1 day, else null
let today = (date now | format date "%Y-%m-%d")
let today_ts = ($today | into datetime | into int)
let one_day = 86_400_000_000_000 # nanoseconds
let match = ($NEW_MOONS | where {|moon|
let moon_ts = ($moon | into datetime | into int)
let diff = (($today_ts - $moon_ts) | math abs)
$diff <= $one_day
})
if ($match | is-empty) { null } else { $match | first }
}
def is-new-moon [] {
(current-moon) != null
}
def last-moon-file [graph_path: string] {
$"($graph_path)/analizer/.last-moon"
}
def already-computed [graph_path: string] {
let marker = (last-moon-file $graph_path)
if not ($marker | path exists) { return false }
let recorded = (open --raw $marker | str trim)
let moon = (current-moon)
if $moon == null { return false }
$recorded == $moon
}
def main [
graph_path: string,
--subgraphs (-s),
--dry-run (-d),
--force (-f), # bypass new moon check
--iterations (-i): int = 20,
--alpha (-a): float = 0.85, # diffusion damping (teleport = 1-alpha)
--lambda-d: float = 0.5, # diffusion weight
--lambda-s: float = 0.3, # springs weight
--lambda-h: float = 0.2, # heat weight
--tau: float = 1.0, # heat kernel bandwidth
] {
# new moon guard — only write weights on new moon (±1 day), once per moon
if (not $dry_run) and (not $force) {
if not (is-new-moon) {
let today = (date now | format date "%Y-%m-%d")
let next = ($NEW_MOONS | where {|m| ($m | into datetime) > (date now)} | first)
print $"⏳ Not a new moon today \(($today)\). Next new moon: ($next)"
print " Weights are updated once per lunar cycle."
print " Use --dry-run to compute without writing, or --force to override."
return
}
if (already-computed $graph_path) {
let moon = (current-moon)
print $"🌑 New moon ($moon) already computed. Weights are up to date."
print " Use --force to recompute."
return
}
}
print "Scanning pages..."
# --- collect files ---
mut all_files = (glob $"($graph_path)/root/**/*.md" | sort)
if $subgraphs {
let git_root = ($graph_path | path dirname)
let repos = [hemera zheng nebu nox bbg cybernode mudra trident optica]
for repo in $repos {
let repo_path = $"($git_root)/($repo)"
if ($repo_path | path exists) {
let md = if ($"($repo_path)/root" | path exists) {
glob $"($repo_path)/root/**/*.md"
} else if ($"($repo_path)/graph" | path exists) {
glob $"($repo_path)/graph/**/*.md"
} else if ($"($repo_path)/pages" | path exists) {
glob $"($repo_path)/pages/**/*.md"
} else {
glob $"($repo_path)/**/*.md"
| where {|f| not ($f | str contains "/.git/")}
| where {|f| not ($f | str contains "/build/")}
| where {|f| not ($f | str contains "/target/")}
| where {|f| not ($f | str contains "/node_modules/")}
}
$all_files = ($all_files | append ($md | sort))
}
}
}
let git_root = ($graph_path | path dirname)
print $"Total files: ($all_files | length)"
# --- build page index, alias map, sizes ---
mut page_index = {}
mut alias_map = {}
mut page_files = {}
mut page_sizes = {}
for f in $all_files {
let rel = if ($f | str starts-with $graph_path) {
$f | str replace $"($graph_path)/" ""
} else {
$f | str replace $"($git_root)/" ""
}
let page_name = if ($rel | str starts-with "root/") {
$rel | str replace "root/" "" | str replace ".md" ""
} else {
$rel | str replace ".md" ""
}
let name_lower = ($page_name | str downcase)
let raw = (open --raw $f)
let size = ($raw | str length)
$page_index = ($page_index | merge {$name_lower: true})
$page_files = ($page_files | merge {$name_lower: $f})
$page_sizes = ($page_sizes | merge {$name_lower: $size})
if ($raw | str starts-with "---") {
let lines = ($raw | lines)
let fm_matches = ($lines | skip 1 | enumerate | where {|x| $x.item == "---"})
let fm_end = if ($fm_matches | length) > 0 { $fm_matches | first | get index } else { 999 }
if $fm_end < 999 {
let fm_lines = ($lines | skip 1 | first $fm_end)
let alias_lines = ($fm_lines | where {|l| $l | str starts-with "alias:"})
if ($alias_lines | length) > 0 {
let alias_line = ($alias_lines | first)
let aliases = ($alias_line | str replace "alias:" "" | split row "," | each {|a| $a | str trim | str downcase} | where {|a| ($a | str length) > 0})
for a in $aliases {
$alias_map = ($alias_map | merge {$a: $name_lower})
}
}
}
}
}
let alias_map = $alias_map
let page_index = $page_index
let page_files = $page_files
let page_sizes = $page_sizes
let all_names = ($page_index | columns)
let n = ($all_names | length)
let nf = ($n | into float)
print $"Pages: ($n), aliases: ($alias_map | columns | length)"
# --- build link graph ---
print "Building link graph..."
mut outbound = {}
mut inbound = {}
mut out_count = {}
for name in $all_names {
$outbound = ($outbound | merge {$name: []})
$inbound = ($inbound | merge {$name: []})
$out_count = ($out_count | merge {$name: 0})
}
for name in $all_names {
let f = ($page_files | get $name)
let raw = (open --raw $f)
let links = ($raw | parse --regex '\[\[([^\]]+)\]\]' | get capture0 | uniq)
let resolved = ($links | each {|l|
let lower = ($l | str downcase)
if ($alias_map | get -i $lower) != null { $alias_map | get $lower } else { $lower }
} | where {|t| ($page_index | get -i $t) != null})
$outbound = ($outbound | merge {$name: $resolved})
$out_count = ($out_count | merge {$name: ($resolved | length)})
for target in $resolved {
let current = ($inbound | get $target)
$inbound = ($inbound | merge {$target: ($current | append $name)})
}
}
let outbound = $outbound
let inbound = $inbound
let out_count = $out_count
# =================================================================
# OPERATOR 1: DIFFUSION (PageRank)
# =================================================================
print $"D: diffusion — ($iterations) iterations, alpha=($alpha)..."
let teleport = (1.0 - $alpha) / $nf
mut phi_d = {}
let init_val = 1.0 / $nf
for name in $all_names {
$phi_d = ($phi_d | merge {$name: $init_val})
}
mut dangling_list = []
for name in $all_names {
if ($out_count | get $name) == 0 {
$dangling_list = ($dangling_list | append $name)
}
}
let dangling = $dangling_list
print $" dangling nodes: ($dangling | length)"
for iter in 1..($iterations + 1) {
mut dangling_mass = 0.0
for name in $dangling {
$dangling_mass = $dangling_mass + ($phi_d | get $name)
}
let dangling_share = $alpha * $dangling_mass / $nf
mut phi_new = {}
for name in $all_names {
let sources = ($inbound | get $name)
mut link_sum = 0.0
for src in $sources {
let src_phi = ($phi_d | get $src)
let src_out = ($out_count | get $src)
if $src_out > 0 {
$link_sum = $link_sum + $src_phi / ($src_out | into float)
}
}
let val = $alpha * $link_sum + $teleport + $dangling_share
$phi_new = ($phi_new | merge {$name: $val})
}
# normalize
mut total = 0.0
for name in $all_names { $total = $total + ($phi_new | get $name) }
if $total > 0 {
for name in $all_names {
let v = ($phi_new | get $name)
$phi_new = ($phi_new | merge {$name: ($v / $total)})
}
}
mut delta = 0.0
for name in $all_names {
$delta = $delta + ((($phi_d | get $name) - ($phi_new | get $name)) | math abs)
}
if ($iter mod 5) == 0 or $iter == 1 {
print $" iteration ($iter): delta = ($delta | math round -p 8)"
}
$phi_d = $phi_new
if $delta < 1e-8 { print $" converged at iteration ($iter)"; break }
}
let phi_d = $phi_d
# =================================================================
# OPERATOR 2: SPRINGS (screened Laplacian equilibrium)
# For each page: average diffusion focus of all direct neighbors
# (both inbound and outbound). Approximates (L + μI)⁻¹ x₀
# =================================================================
print "S: springs — neighbor equilibrium..."
mut phi_s = {}
for name in $all_names {
let in_neighbors = ($inbound | get $name)
let out_neighbors = ($outbound | get $name)
# collect unique neighbors
mut neighbor_sum = 0.0
mut neighbor_count = 0
for nb in $in_neighbors {
$neighbor_sum = $neighbor_sum + ($phi_d | get $nb)
$neighbor_count = $neighbor_count + 1
}
for nb in $out_neighbors {
$neighbor_sum = $neighbor_sum + ($phi_d | get $nb)
$neighbor_count = $neighbor_count + 1
}
# screened: blend neighbor average with own diffusion (screening μ)
let mu = 0.1
let nb_avg = if $neighbor_count > 0 { $neighbor_sum / ($neighbor_count | into float) } else { 1.0 / $nf }
let val = (1.0 - $mu) * $nb_avg + $mu * ($phi_d | get $name)
$phi_s = ($phi_s | merge {$name: $val})
}
# normalize springs
mut s_total = 0.0
for name in $all_names { $s_total = $s_total + ($phi_s | get $name) }
if $s_total > 0 {
for name in $all_names {
let v = ($phi_s | get $name)
$phi_s = ($phi_s | merge {$name: ($v / $s_total)})
}
}
let phi_s = $phi_s
# =================================================================
# OPERATOR 3: HEAT (multi-scale smoothing, 2-hop neighborhood)
# Heat kernel e^{-τL} approximated as weighted 2-hop average.
# hop-1 neighbors get weight e^{-τ}, hop-2 get weight e^{-2τ}
# =================================================================
print $"H: heat — 2-hop smoothing, tau=($tau)..."
let w1 = ((-1.0 * $tau) | math exp) # weight for 1-hop
let w2 = ((-2.0 * $tau) | math exp) # weight for 2-hop
mut phi_h = {}
for name in $all_names {
let in_neighbors = ($inbound | get $name)
let out_neighbors = ($outbound | get $name)
# 1-hop: direct neighbors
mut sum_1hop = 0.0
mut count_1hop = 0
mut hop1_set = []
for nb in $in_neighbors {
$sum_1hop = $sum_1hop + ($phi_d | get $nb)
$count_1hop = $count_1hop + 1
$hop1_set = ($hop1_set | append $nb)
}
for nb in $out_neighbors {
$sum_1hop = $sum_1hop + ($phi_d | get $nb)
$count_1hop = $count_1hop + 1
$hop1_set = ($hop1_set | append $nb)
}
# 2-hop: neighbors of neighbors (sample — full would be too slow)
mut sum_2hop = 0.0
mut count_2hop = 0
for nb in $hop1_set {
let nb_in = ($inbound | get $nb)
for nb2 in $nb_in {
$sum_2hop = $sum_2hop + ($phi_d | get $nb2)
$count_2hop = $count_2hop + 1
}
}
let self_val = ($phi_d | get $name)
let hop1_avg = if $count_1hop > 0 { $sum_1hop / ($count_1hop | into float) } else { 0.0 }
let hop2_avg = if $count_2hop > 0 { $sum_2hop / ($count_2hop | into float) } else { 0.0 }
# heat kernel: self + weighted 1-hop + weighted 2-hop
let val = $self_val + $w1 * $hop1_avg + $w2 * $hop2_avg
$phi_h = ($phi_h | merge {$name: $val})
}
# normalize heat
mut h_total = 0.0
for name in $all_names { $h_total = $h_total + ($phi_h | get $name) }
if $h_total > 0 {
for name in $all_names {
let v = ($phi_h | get $name)
$phi_h = ($phi_h | merge {$name: ($v / $h_total)})
}
}
let phi_h = $phi_h
# =================================================================
# COMPOSITE: focus = norm(λd·D + λs·S + λh·H)
# =================================================================
print $"Composing: lambda_d=($lambda_d) lambda_s=($lambda_s) lambda_h=($lambda_h)"
mut phi_focus = {}
for name in $all_names {
let d = ($phi_d | get $name)
let s = ($phi_s | get $name)
let h = ($phi_h | get $name)
let val = $lambda_d * $d + $lambda_s * $s + $lambda_h * $h
$phi_focus = ($phi_focus | merge {$name: $val})
}
# normalize composite
mut f_total = 0.0
for name in $all_names { $f_total = $f_total + ($phi_focus | get $name) }
if $f_total > 0 {
for name in $all_names {
let v = ($phi_focus | get $name)
$phi_focus = ($phi_focus | merge {$name: ($v / $f_total)})
}
}
let phi_focus = $phi_focus
# =================================================================
# BUILD RESULTS
# =================================================================
let results = ($all_names | each {|name|
let grav = ($inbound | get $name | length)
let size_bytes = ($page_sizes | get $name)
let out = ($out_count | get $name)
let dens = if $size_bytes > 0 { ($out | into float) / ($size_bytes / 1024.0) } else { 0.0 }
{
name: $name
file: ($page_files | get $name)
diffusion: ($phi_d | get $name)
springs: ($phi_s | get $name)
heat: ($phi_h | get $name)
focus: ($phi_focus | get $name)
gravity: $grav
density: $dens
}
} | sort-by focus -r)
if $dry_run {
print "\n=== TOP 50 by focus ==="
let display = ($results | first 50 | each {|r| {
name: $r.name
D: ($r.diffusion * 1e6 | math round -p 0)
S: ($r.springs * 1e6 | math round -p 0)
H: ($r.heat * 1e6 | math round -p 0)
focus: ($r.focus * 1e6 | math round -p 0)
gravity: $r.gravity
density: ($r.density | math round -p 1)
}})
print ($display | table)
let total = ($results | get focus | math sum | math round -p 6)
print $"\nTotal focus: ($total) [should be 1.0]"
return
}
# =================================================================
# WRITE TO FRONTMATTER
# =================================================================
print "Writing tri-kernel results to frontmatter..."
let fields_to_clean = ["focus:" "diffusion:" "springs:" "heat:" "gravity:" "density:"]
mut written = 0
for row in $results {
let f = $row.file
let raw = (open --raw $f)
if not ($raw | str starts-with "---") { continue }
let lines = ($raw | lines)
let fm_matches = ($lines | skip 1 | enumerate | where {|x| $x.item == "---"})
if ($fm_matches | length) == 0 { continue }
let fm_end = ($fm_matches | first | get index)
let fm_lines = ($lines | skip 1 | first $fm_end)
let body_lines = ($lines | skip ($fm_end + 2))
# remove old tri-kernel fields
let tk_prefixes = ["focus:" "diffusion:" "springs:" "heat:" "gravity:" "density:"]
let clean_fm = ($fm_lines | where {|l| not ($tk_prefixes | any {|p| $l | str starts-with $p})})
let density_str = ($row.density | math round -p 2 | into string)
let new_fm = ($clean_fm
| append $"diffusion: ($row.diffusion | into string)"
| append $"springs: ($row.springs | into string)"
| append $"heat: ($row.heat | into string)"
| append $"focus: ($row.focus | into string)"
| append $"gravity: ($row.gravity)"
| append $"density: ($density_str)"
)
let new_content = (["---"] | append $new_fm | append ["---"] | append $body_lines | str join "\n")
$new_content | save -f $f
$written = $written + 1
}
# record completion marker
let moon = if (is-new-moon) { current-moon } else { date now | format date "%Y-%m-%d" }
$moon | save -f (last-moon-file $graph_path)
print $"🌑 Recorded moon marker: ($moon)"
print $"Written 6 fields to ($written) pages"
print "\nTop 10 by focus:"
print ($results | first 10 | each {|r| {
name: $r.name
D: ($r.diffusion * 1e6 | math round -p 0)
S: ($r.springs * 1e6 | math round -p 0)
H: ($r.heat * 1e6 | math round -p 0)
focus: ($r.focus * 1e6 | math round -p 0)
gravity: $r.gravity
density: ($r.density | math round -p 1)
}} | table)
}
analizer/trikernel.nu
π 0.0%