Skip to main content

cortiq_core/
knowledge.rs

1//! Format v2 "knowledge without forgetting" (spec §9.2–§9.5): a FROZEN
2//! genome, skill records v2 that own their tensors over it, and the
3//! backbone-gated router policy.
4//!
5//! Three `required_features` bits carry the semantics, each DERIVED by the
6//! writers from header content and checked both ways on open:
7//!
8//! * `GENOME` ⇔ `header.genome` — the reader recomputes [`trunk_hash`] from
9//!   the directory and refuses a file whose trunk changed without a new
10//!   genome;
11//! * `SKILLS_V2` ⇔ some `skills[i].kind` — every v2 record is validated
12//!   against the genome it is bound to and against the directory;
13//! * `ROUTER_V2` ⇔ `header.router` — only the declared `backbone_gated`
14//!   policy may auto-route; the legacy argmin-of-skills is not a fallback.
15//!
16//! Everything here is pure metadata logic (no I/O): `format.rs` calls
17//! [`validate_knowledge`] from `open()`, from every writer and from the
18//! tail append, so a file the writer produced is a file the reader accepts.
19
20use crate::format::{
21    CmfError, CmfHeader, SelectionDescriptor, SkillRecord, TensorEntry, TensorSpec, features,
22};
23use crate::hash::hash64;
24use crate::types::{LayerType, ModelArch, TensorDtype};
25use serde::{Deserialize, Serialize};
26use std::borrow::Cow;
27use std::collections::{BTreeSet, HashMap};
28
29/// `genome.status` values.
30pub const GENOME_STATUSES: &[&str] = &["pre_chat", "sealed", "candidate", "rejected"];
31/// `skills[i].status` values (v2 records).
32pub const SKILL_STATUSES: &[&str] = &["quarantine", "active", "stale_regate", "retired"];
33/// `segments[i].kind` values.
34pub const SEGMENT_KINDS: &[&str] = &["genome", "base", "skill"];
35/// The metric every router-v2 descriptor uses (unit-normalized φ).
36pub const METRIC_MSE_UNIT: &str = "mse_unit";
37/// Row id of the backbone class in router-v2 decisions. No skill may use it.
38pub const BACKBONE_CLASS_ID: &str = "@backbone";
39/// Ids a v2 skill may not take: the CLI and the route reports spell the
40/// backbone and the automatic decision with them (`--skill none`,
41/// `--route backbone|auto`, `target: "backbone"`), so a skill with such an
42/// id would be unreachable or indistinguishable from the backbone.
43pub const RESERVED_SKILL_IDS: &[&str] = &["backbone", "none", "auto"];
44
45/// Skill-record kinds (`SkillRecord.kind`).
46pub mod skill_kind {
47    /// Full-shape replacement of FFN tensors of some layers (the Patent-15
48    /// per-skill record), switched per request by the router.
49    pub const FFN_REPLACE: &str = "ffn_replace";
50    /// New MoE experts appended after the trunk's `E0` in the grown layers
51    /// (DTG-MA new edges / P3 growth — not a P15 record): the Embryo's
52    /// built-in resonance router picks them per token inside the layer, so
53    /// the record is part of the organism and is never switched per
54    /// request. Layout and rules: [`super::ExpertAppend`],
55    /// [`super::expert_append_layout`].
56    pub const EXPERT_APPEND: &str = "expert_append";
57    /// An explicit reference memory (spec §9.5.2): a key → card table
58    /// appended after the sealed genome under `skill.{id}.lookup.*`,
59    /// picked per request by the `backbone_gated` router like any v2
60    /// record and answered from the table — exact facts by construction,
61    /// no forgetting by construction (the network's computation and state
62    /// are untouched), O(1) per query. Layout and rules:
63    /// [`super::LookupInfo`], [`super::lookup_leaf`].
64    pub const LOOKUP: &str = "lookup";
65    /// Kinds this reader implements.
66    pub const IMPLEMENTED: &[&str] = &[FFN_REPLACE, EXPERT_APPEND, LOOKUP];
67    /// Kinds that are named (so files can be planned against them) but
68    /// REFUSED until a reader implements them: `anchor_sinks`, `mask_only`,
69    /// `fcd_vector` (FCD-JMLR Tucker v_t / P1 cl.7).
70    pub const RESERVED: &[&str] = &["anchor_sinks", "mask_only", "fcd_vector"];
71}
72
73/// `StateEffect.switch` values.
74pub mod state_switch {
75    /// Switching the skill mid-sequence leaves every recurrent / ring state
76    /// exact: no layer after the first replaced FFN carries sequence state.
77    pub const TRANSPARENT: &str = "transparent";
78    /// The skill may only change at a sequence start (state is reset).
79    pub const SEQUENCE_START: &str = "sequence_start";
80    /// Ordered from most to least permissive.
81    pub const ALL: &[&str] = &[TRANSPARENT, SEQUENCE_START];
82}
83
84// ───────────────────────── genome + lineage ─────────────────────────
85
86/// The frozen genome the file carries (`header.genome`, bit `GENOME`).
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct GenomeInfo {
89    /// Stable family id, e.g. `"embryo-o1-fam-a"`. Gradient training of a
90    /// sealed trunk is a NEW genome id (a new birth), not generation+1.
91    pub id: String,
92    /// 0 at birth.
93    pub generation: u32,
94    /// `pre_chat` | `sealed` | `candidate` | `rejected`.
95    pub status: String,
96    /// hex hash64 of the trunk AS ENCODED in this file ([`trunk_hash`]).
97    /// A writer fills an empty value; a non-empty one must match the
98    /// content or the writer refuses.
99    #[serde(default)]
100    pub trunk_hash: String,
101    /// `"f32"` | `"f16"` | `"q4tp"` | … (informational).
102    pub encoding: String,
103    /// trunk_hash of the f32 master (== `trunk_hash` when `encoding` is
104    /// `"f32"`; the writer fills it then). Skill records bind to THIS, so a
105    /// requant does not orphan them.
106    #[serde(default)]
107    pub master_trunk_hash: String,
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub parent: Option<GenomeParent>,
110    /// Measured reference battery `{name: {value, dataset_sha256}}`.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub reference: Option<serde_json::Value>,
113    /// `E0`: trunk experts per MoE layer of a resonance-routed genome
114    /// (`arch.moe.router_resonance`). The writer fills an empty value from
115    /// `arch.moe.num_experts` when it seals the genome; the reader refuses
116    /// a value that disagrees with the arch. `expert_append` records grow
117    /// every layer from here ([`expert_append_base`]). Absent on dense /
118    /// gated-MoE genomes and on files written before the field existed
119    /// ([`genome_moe_experts`] falls back to the arch).
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub moe_experts: Option<usize>,
122}
123
124impl GenomeInfo {
125    /// A genome at birth; the writer fills `trunk_hash` (and the master
126    /// hash for `f32`) and `moe_experts` from the content it writes.
127    pub fn birth(
128        id: impl Into<String>,
129        status: impl Into<String>,
130        encoding: impl Into<String>,
131    ) -> Self {
132        Self {
133            id: id.into(),
134            generation: 0,
135            status: status.into(),
136            trunk_hash: String::new(),
137            encoding: encoding.into(),
138            master_trunk_hash: String::new(),
139            parent: None,
140            reference: None,
141            moe_experts: None,
142        }
143    }
144}
145
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct GenomeParent {
148    pub id: String,
149    pub generation: u32,
150    pub master_trunk_hash: String,
151}
152
153/// One journal entry (`header.lineage`): `birth` | `skill_committed` |
154/// `skill_retired` | `requant` | `recalibrate` | `compact` | `rollback`.
155/// `seq` is strictly increasing.
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157pub struct LineageEvent {
158    pub seq: u32,
159    pub event: String,
160    /// RFC 3339 UTC.
161    pub ts: String,
162    #[serde(default)]
163    pub detail: serde_json::Value,
164}
165
166impl LineageEvent {
167    /// An event stamped with the current UTC time.
168    pub fn now(seq: u32, event: impl Into<String>, detail: serde_json::Value) -> Self {
169        Self {
170            seq,
171            event: event.into(),
172            ts: utc_now_rfc3339(),
173            detail,
174        }
175    }
176}
177
178/// Data-range ownership after tail appends (`header.segments`). Offsets
179/// are relative to `data_off`, like directory offsets. A full rewrite is a
180/// compaction and clears the table.
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182pub struct Segment {
183    /// `genome` | `base` | `skill`.
184    pub kind: String,
185    pub id: String,
186    pub data_start: u64,
187    pub data_end: u64,
188}
189
190// ───────────────────────── skill records v2 ─────────────────────────
191
192/// A base tensor a v2 skill replaces.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct SkillOverride {
195    /// Trunk tensor name (`X` of `skill.{id}.X`).
196    pub name: String,
197    /// hex hash64 of the base entry the skill was trained against (the f32
198    /// master's entry; comparable with this file only when
199    /// `genome.encoding == "f32"`).
200    pub base_hash: String,
201}
202
203/// The genome a v2 skill is bound to.
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct SkillBound {
206    pub genome_id: String,
207    pub generation: u32,
208    pub master_trunk_hash: String,
209}
210
211/// What switching this skill does to the O(1) sequence state.
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213pub struct StateEffect {
214    pub first_affected_layer: usize,
215    /// `transparent` | `sequence_start` ([`state_switch`]).
216    pub switch: String,
217    /// Extra per-sequence state the record adds (0 for `ffn_replace`).
218    pub state_bytes_added: u64,
219}
220
221/// Does this layer type carry sequence state (KV, ring, recurrent)?
222/// Exhaustive on purpose: a new operator must decide here.
223pub fn layer_type_is_stateful(t: LayerType) -> bool {
224    match t {
225        LayerType::FullAttention
226        | LayerType::SlidingAttention
227        | LayerType::LinearAttention
228        | LayerType::ShortConv
229        | LayerType::Kda
230        | LayerType::BoundedAttention => true,
231    }
232}
233
234/// The state effect of replacing the FFN of `layers`, computed by the
235/// WRITER from `arch.layer_types`: the FFN output of layer l feeds every
236/// layer j > l, so switching is `transparent` iff no layer after
237/// `min(layers)` carries sequence state (and the stack is not looped —
238/// a loop feeds layer l back into layer 0). The MTP head is not counted:
239/// its state only steers drafts that the main model verifies.
240pub fn ffn_replace_state_effect(arch: &ModelArch, layers: &[usize]) -> StateEffect {
241    let first = layers.iter().copied().min().unwrap_or(0);
242    let later_stateful = arch.num_loops > 1
243        || arch
244            .layer_types
245            .iter()
246            .enumerate()
247            .any(|(j, t)| j > first && layer_type_is_stateful(*t));
248    StateEffect {
249        first_affected_layer: first,
250        switch: if later_stateful {
251            state_switch::SEQUENCE_START
252        } else {
253            state_switch::TRANSPARENT
254        }
255        .into(),
256        state_bytes_added: 0,
257    }
258}
259
260// ───────────────────────── expert_append ─────────────────────────
261
262/// Parameters of a `kind = "expert_append"` record (`SkillRecord.experts`):
263/// `count` new experts in EACH of `SkillRecord.layers` (the grown MoE
264/// layers, any non-empty subset), appended after the experts already
265/// present in that layer. The per-layer index of the new experts is not
266/// stored — the reader computes it ([`expert_append_base`]).
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
268pub struct ExpertAppend {
269    /// New experts per grown layer (≥ 1).
270    pub count: usize,
271    /// The quantile (in `[0, 1]`) of the reconstruction errors of the tokens
272    /// each expert won on the growth corpus that the trainer wrote into its
273    /// `desc.shell` — provenance of the shell, not used by the reader.
274    pub shell_quantile: f32,
275    /// Rank of every new expert's `desc.u` — must equal the trunk's expert 0
276    /// rank in every grown layer ([`trunk_expert_rank`]); 0 when the trunk
277    /// carries no `desc.u` (then the record carries none either).
278    pub rank: usize,
279}
280
281/// Per-expert tensor leaves of an MoE layer
282/// (`model.layers.{l}.mlp.experts.{e}.{leaf}`), in the order a record lists
283/// them.
284pub mod expert_leaf {
285    pub const GATE: &str = "gate_proj.weight";
286    pub const UP: &str = "up_proj.weight";
287    pub const DOWN: &str = "down_proj.weight";
288    /// Resonance descriptor mean `[hidden]`.
289    pub const MU: &str = "desc.mu";
290    /// Resonance descriptor subspace, same shape as the trunk's expert 0
291    /// (`[rank, hidden]` as the Embryo exports it); absent when `rank == 0`.
292    pub const U: &str = "desc.u";
293    /// `[1]` f32 finite — the grown expert's balancing bias, frozen by
294    /// the trainer (0.0, or its source trunk expert's; the record's origin
295    /// says which as `bias_mode`).
296    pub const BIAS: &str = "desc.bias";
297    /// `[1]` f32 finite — reconstruction-error threshold outside which the
298    /// expert never wins (grown experts only; the trunk has no shell).
299    pub const SHELL: &str = "desc.shell";
300    /// Every leaf a grown expert carries (`U` only when `rank > 0`).
301    pub const ALL: &[&str] = &[GATE, UP, DOWN, MU, U, BIAS, SHELL];
302}
303
304/// `model.layers.{layer}.mlp.experts.{expert}.{leaf}` — a trunk expert
305/// tensor; a record's copy is `skill.{id}.` + this
306/// ([`expert_append_tensor_name`]).
307pub fn expert_tensor_name(layer: usize, expert: usize, leaf: &str) -> String {
308    format!("model.layers.{layer}.mlp.experts.{expert}.{leaf}")
309}
310
311/// `skill.{id}.model.layers.{layer}.mlp.experts.{expert}.{leaf}`.
312pub fn expert_append_tensor_name(id: &str, layer: usize, expert: usize, leaf: &str) -> String {
313    format!("skill.{id}.{}", expert_tensor_name(layer, expert, leaf))
314}
315
316/// The layers whose trunk carries expert tensors
317/// (`model.layers.{l}.mlp.experts.0.gate_proj.weight`), ascending — the
318/// layers an `expert_append` record may grow (the trainer's default: all).
319pub fn moe_layers(tensors: &[TensorEntry]) -> Vec<usize> {
320    let mut out: Vec<usize> = tensors
321        .iter()
322        .filter_map(|t| {
323            let l = layer_of(&t.name)?;
324            (t.name == expert_tensor_name(l, 0, expert_leaf::GATE)).then_some(l)
325        })
326        .collect();
327    out.sort_unstable();
328    out.dedup();
329    out
330}
331
332/// `E0` of a genome: `genome.moe_experts`, or — on a file written before
333/// the field existed — `arch.moe.num_experts` of a resonance-routed MoE
334/// (the value the writer would fill). Errors: no genome, or the genome is
335/// not a resonance-routed MoE (a gated MoE cannot take appended experts:
336/// its gate tensor fixes `E`).
337pub fn genome_moe_experts(header: &CmfHeader) -> Result<usize, CmfError> {
338    let g = header
339        .genome
340        .as_ref()
341        .ok_or_else(|| perr("expert_append: the file has no genome".into()))?;
342    let moe = header
343        .arch
344        .moe
345        .as_ref()
346        .filter(|m| m.router_resonance)
347        .ok_or_else(|| {
348            perr(format!(
349                "genome '{}': expert_append needs a resonance-routed MoE trunk \
350                 (arch.moe.router_resonance); a gated MoE fixes its expert count in the gate",
351                g.id
352            ))
353        })?;
354    Ok(g.moe_experts.unwrap_or(moe.num_experts))
355}
356
357/// Rank of the trunk's expert 0 descriptor subspace in `layer`
358/// (`experts.0.desc.u` elements / `hidden_size`), 0 when the trunk has no
359/// `desc.u` there. Errors: `layer` is not an MoE layer of the trunk, or it
360/// has no per-expert descriptors (`experts.0.desc.mu`) — the legacy
361/// stacked `mlp.desc.*` form cannot be grown per expert.
362pub fn trunk_expert_rank(
363    header: &CmfHeader,
364    tensors: &[TensorEntry],
365    layer: usize,
366) -> Result<usize, CmfError> {
367    let by_name: HashMap<&str, &TensorEntry> =
368        tensors.iter().map(|t| (t.name.as_str(), t)).collect();
369    trunk_expert_rank_in(header, &by_name, layer)
370}
371
372fn trunk_expert_rank_in(
373    header: &CmfHeader,
374    by_name: &HashMap<&str, &TensorEntry>,
375    layer: usize,
376) -> Result<usize, CmfError> {
377    let trunk = |leaf: &str| {
378        by_name
379            .get(expert_tensor_name(layer, 0, leaf).as_str())
380            .copied()
381            .filter(|t| is_trunk_tensor(&t.name))
382    };
383    if trunk(expert_leaf::GATE).is_none() {
384        return Err(perr(format!(
385            "layer {layer} is not an MoE layer of the trunk (no '{}')",
386            expert_tensor_name(layer, 0, expert_leaf::GATE)
387        )));
388    }
389    if trunk(expert_leaf::MU).is_none() {
390        return Err(perr(format!(
391            "layer {layer}: the trunk has no per-expert descriptors ('{}'); an \
392             expert_append record needs the per-expert form, not the stacked mlp.desc.*",
393            expert_tensor_name(layer, 0, expert_leaf::MU)
394        )));
395    }
396    let Some(u) = trunk(expert_leaf::U) else {
397        return Ok(0);
398    };
399    let hidden = header.arch.hidden_size.max(1);
400    let n = u.n_elems();
401    if n == 0 || n % hidden != 0 {
402        return Err(perr(format!(
403            "'{}': {n} elements is not a multiple of hidden {hidden}",
404            u.name
405        )));
406    }
407    Ok(n / hidden)
408}
409
410/// Grown experts of the `expert_append` records before `at` that grow
411/// `layer` (every status), and the id of the first retired one.
412fn expert_chain_before(header: &CmfHeader, at: usize, layer: usize) -> (usize, Option<&str>) {
413    let mut n = 0usize;
414    let mut retired = None;
415    for s in header.skills.iter().take(at) {
416        if s.kind.as_deref() != Some(skill_kind::EXPERT_APPEND) || !s.layers.contains(&layer) {
417            continue;
418        }
419        n += s.experts.as_ref().map(|e| e.count).unwrap_or(0);
420        if retired.is_none() && s.status.as_deref() == Some("retired") {
421            retired = Some(s.id.as_str());
422        }
423    }
424    (n, retired)
425}
426
427/// First expert index the `expert_append` record at position `at` of
428/// `header.skills` owns in `layer` — the per-layer chain rule:
429/// `E0 + Σ count` of the expert_append records BEFORE `at` that grow
430/// `layer`. Its experts are `base + k`, `k in 0..count`; a layer the record
431/// does not grow keeps its experts. `at == header.skills.len()` plans a
432/// record about to be appended.
433///
434/// Indices are fixed when a record is written and never move: a record's
435/// status (quarantine / active / stale_regate) does not change the chain.
436/// Retiring is allowed only from the TAIL of a layer's chain — a live
437/// record behind a retired one in the same layer is refused (here and by
438/// `open()`), so a retired record cannot shift the indices of a later one;
439/// once every later record of the layer is retired, the earlier one may be.
440pub fn expert_append_base(header: &CmfHeader, at: usize, layer: usize) -> Result<usize, CmfError> {
441    let e0 = genome_moe_experts(header)?;
442    let (n, retired) = expert_chain_before(header, at, layer);
443    if let Some(r) = retired {
444        return Err(perr(format!(
445            "expert_append chain at layer {layer} is broken: record '{r}' before position {at} \
446             is retired — retire records from the tail of the chain (the last appended first)"
447        )));
448    }
449    Ok(e0 + n)
450}
451
452/// One tensor an `expert_append` record carries.
453#[derive(Debug, Clone, PartialEq, Eq)]
454pub struct ExpertTensorSpec {
455    /// Full directory name `skill.{id}.model.layers.{layer}.mlp.experts.{expert}.{leaf}`.
456    pub name: String,
457    pub shape: Vec<usize>,
458    pub layer: usize,
459    /// Absolute expert index in the layer (`base + k`).
460    pub expert: usize,
461    /// One of [`expert_leaf`].
462    pub leaf: &'static str,
463}
464
465/// Every tensor (name and shape) the `expert_append` record `record` must
466/// carry when it sits at position `at` of `header.skills` — exactly these,
467/// no others: per grown layer `l` and `k in 0..count`, expert
468/// `e = expert_append_base(l) + k`: `gate_proj.weight`, `up_proj.weight`,
469/// `down_proj.weight` shaped like the trunk's expert 0 of `l`, `desc.mu
470/// [hidden]`, `desc.u` shaped like the trunk's expert 0 `desc.u` (absent
471/// when `rank == 0`), `desc.bias [1]` and `desc.shell [1]` (both f32).
472///
473/// Errors name the first rule broken: no genome / not a resonance MoE, no
474/// `experts`, `count == 0`, empty or duplicate `layers`, a layer that is
475/// not MoE or has no per-expert descriptors, `rank` != the trunk's, a
476/// broken chain. The trainer builds its `TensorSpec`s from this plan with
477/// `at = header.skills.len()`; the loader recovers the names of a mounted
478/// record with its position.
479pub fn expert_append_layout(
480    header: &CmfHeader,
481    tensors: &[TensorEntry],
482    at: usize,
483    record: &SkillRecord,
484) -> Result<Vec<ExpertTensorSpec>, CmfError> {
485    let by_name: HashMap<&str, &TensorEntry> =
486        tensors.iter().map(|t| (t.name.as_str(), t)).collect();
487    expert_append_layout_in(header, &by_name, at, record, false)
488}
489
490/// [`expert_append_layout`]; `retired_self` = the record is retired, so a
491/// retired predecessor does not break its chain (its indices were fixed
492/// when every predecessor was live).
493fn expert_append_layout_in(
494    header: &CmfHeader,
495    by_name: &HashMap<&str, &TensorEntry>,
496    at: usize,
497    record: &SkillRecord,
498    retired_self: bool,
499) -> Result<Vec<ExpertTensorSpec>, CmfError> {
500    let id = &record.id;
501    let e0 = genome_moe_experts(header).map_err(|e| perr(format!("skill '{id}': {e}")))?;
502    let ea = record
503        .experts
504        .as_ref()
505        .ok_or_else(|| perr(format!("skill '{id}': expert_append record has no `experts`")))?;
506    if ea.count == 0 {
507        return Err(perr(format!(
508            "skill '{id}': experts.count must be ≥ 1"
509        )));
510    }
511    if !(ea.shell_quantile.is_finite() && (0.0..=1.0).contains(&ea.shell_quantile)) {
512        return Err(perr(format!(
513            "skill '{id}': experts.shell_quantile {} must be finite and in [0, 1]",
514            ea.shell_quantile
515        )));
516    }
517    if record.layers.is_empty() {
518        return Err(perr(format!(
519            "skill '{id}': expert_append record grows no layers (`layers` is empty)"
520        )));
521    }
522    let distinct: BTreeSet<usize> = record.layers.iter().copied().collect();
523    if distinct.len() != record.layers.len() {
524        return Err(perr(format!(
525            "skill '{id}': duplicate layer in {:?}",
526            record.layers
527        )));
528    }
529    let hidden = header.arch.hidden_size;
530    let mut out = Vec::with_capacity(distinct.len() * ea.count * expert_leaf::ALL.len());
531    for &l in &distinct {
532        if l >= header.arch.num_layers {
533            return Err(perr(format!(
534                "skill '{id}': layer {l} of {} layers",
535                header.arch.num_layers
536            )));
537        }
538        let rank = trunk_expert_rank_in(header, by_name, l)
539            .map_err(|e| perr(format!("skill '{id}': {e}")))?;
540        if rank != ea.rank {
541            return Err(perr(format!(
542                "skill '{id}': experts.rank {} != the trunk expert 0's desc.u rank {rank} at \
543                 layer {l}",
544                ea.rank
545            )));
546        }
547        let (n_before, retired) = expert_chain_before(header, at, l);
548        if let (Some(r), false) = (retired, retired_self) {
549            return Err(perr(format!(
550                "skill '{id}': expert_append chain at layer {l} is broken — the earlier record \
551                 '{r}' is retired while this one is live; retire records from the tail of the \
552                 chain (the last appended first)"
553            )));
554        }
555        let base = e0 + n_before;
556        let trunk_shape = |leaf: &str| -> Vec<usize> {
557            by_name
558                .get(expert_tensor_name(l, 0, leaf).as_str())
559                .map(|t| t.shape.clone())
560                .unwrap_or_default()
561        };
562        for leaf in [expert_leaf::GATE, expert_leaf::UP, expert_leaf::DOWN] {
563            if trunk_shape(leaf).is_empty() {
564                return Err(perr(format!(
565                    "skill '{id}': the trunk has no '{}'",
566                    expert_tensor_name(l, 0, leaf)
567                )));
568            }
569        }
570        for k in 0..ea.count {
571            let e = base + k;
572            let mut push = |leaf: &'static str, shape: Vec<usize>| {
573                out.push(ExpertTensorSpec {
574                    name: expert_append_tensor_name(id, l, e, leaf),
575                    shape,
576                    layer: l,
577                    expert: e,
578                    leaf,
579                })
580            };
581            push(expert_leaf::GATE, trunk_shape(expert_leaf::GATE));
582            push(expert_leaf::UP, trunk_shape(expert_leaf::UP));
583            push(expert_leaf::DOWN, trunk_shape(expert_leaf::DOWN));
584            push(expert_leaf::MU, vec![hidden]);
585            if rank > 0 {
586                push(expert_leaf::U, trunk_shape(expert_leaf::U));
587            }
588            push(expert_leaf::BIAS, vec![1]);
589            push(expert_leaf::SHELL, vec![1]);
590        }
591    }
592    Ok(out)
593}
594
595/// The state effect of an `expert_append` record: the grown experts are
596/// part of the organism from the first grown layer on and are never
597/// switched, so `sequence_start` (the least permissive) and no state added.
598pub fn expert_append_state_effect(layers: &[usize]) -> StateEffect {
599    StateEffect {
600        first_affected_layer: layers.iter().copied().min().unwrap_or(0),
601        switch: state_switch::SEQUENCE_START.into(),
602        state_bytes_added: 0,
603    }
604}
605
606/// First f32 (LE) of a payload.
607pub fn f32_le_head(bytes: &[u8]) -> Option<f32> {
608    bytes
609        .get(..4)
610        .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
611}
612
613/// The value rules of `expert_append` descriptors, which the directory
614/// cannot express: every `desc.bias` is finite (the trainer freezes it —
615/// 0.0 or the source's balancing bias, `origin.bias_mode`; NaN/inf would
616/// buy or lose every token) and every `desc.shell` is finite (+inf would
617/// mean "no shell", NaN would never fire). `f32_of` yields the first f32
618/// of an entry's payload (`None` = not available → refused).
619/// Called by `open()` (mmap) and by every writer (the payload it writes)
620/// after [`validate_knowledge`], which fixed the dtype to f32.
621pub fn validate_expert_append_values(
622    header: &CmfHeader,
623    tensors: &[TensorEntry],
624    mut f32_of: impl FnMut(&TensorEntry) -> Option<f32>,
625) -> Result<(), CmfError> {
626    for s in header
627        .skills
628        .iter()
629        .filter(|s| s.kind.as_deref() == Some(skill_kind::EXPERT_APPEND))
630    {
631        let prefix = format!("skill.{}.", s.id);
632        for t in tensors.iter().filter(|t| t.name.starts_with(&prefix)) {
633            let bias = t.name.ends_with(".desc.bias");
634            let shell = t.name.ends_with(".desc.shell");
635            if !(bias || shell) {
636                continue;
637            }
638            let v = f32_of(t).ok_or_else(|| {
639                perr(format!(
640                    "skill '{}': payload of '{}' is not available for the value check",
641                    s.id, t.name
642                ))
643            })?;
644            if bias && !v.is_finite() {
645                return Err(perr(format!(
646                    "skill '{}': '{}' is {v} — a grown expert's desc.bias must be a finite f32",
647                    s.id, t.name
648                )));
649            }
650            if shell && !v.is_finite() {
651                return Err(perr(format!(
652                    "skill '{}': '{}' is {v} — desc.shell must be a finite f32 threshold",
653                    s.id, t.name
654                )));
655            }
656        }
657    }
658    Ok(())
659}
660
661// ───────────────────────── lookup ─────────────────────────
662
663/// The pre-release key normalisation (NFC → lowercase → every
664/// non-alphanumeric char → space): it split a word at a combining stress
665/// mark or a soft hyphen (`Рома́шка` → `рома шка`) and kept Latin
666/// diacritics (`Pínus` ≠ `Pinus`). No table was released under it; this
667/// reader refuses the id with a rebuild hint.
668pub const KEY_NORM_V1: &str = "cmf-key-v1";
669/// `cmf-key-v2`: [`normalize_key`] — the rule every table is built and
670/// read with. A new rule is a new id, never a silent change.
671pub const KEY_NORM_V2: &str = "cmf-key-v2";
672/// The key normalisation a `lookup` record declares (`LookupInfo.key_norm`):
673/// [`KEY_NORM_V2`], [`normalize_key`]. This reader knows exactly one.
674pub const KEY_NORM: &str = KEY_NORM_V2;
675/// Longest key the runtime can match, in words of the normalised form:
676/// the user message is searched as n-grams of `1..=LOOKUP_MAX_NGRAM`
677/// words (`cortiq_engine::lookup::MAX_NGRAM` is this value), so the
678/// builder drops a longer key as unreachable instead of storing it.
679pub const LOOKUP_MAX_NGRAM: usize = 4;
680
681/// Parameters of a `kind = "lookup"` record (`SkillRecord.lookup`, spec
682/// §9.5.2): `entries` cards, each with one text slot per language of
683/// `langs` (slot `entry · L + lang`), reached through `keys` normalised,
684/// hashed keys. The table itself is the four tensors of [`lookup_leaf`];
685/// a lookup record has no `overrides`, no `experts`, no `layers` and the
686/// transparent [`lookup_state_effect`] — it never touches the network.
687#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
688pub struct LookupInfo {
689    /// `E ≥ 1` cards.
690    pub entries: usize,
691    /// `K ≥ 1` keys; several keys may name one entry (synonyms, languages).
692    pub keys: usize,
693    /// [`KEY_NORM`] — how a key is normalised before it is hashed.
694    pub key_norm: String,
695    /// `L ≥ 1` languages in slot order, unique, e.g. `["ru", "en"]`.
696    pub langs: Vec<String>,
697    /// Names of the fields the cards carry (informational; unique).
698    #[serde(default)]
699    pub fields: Vec<String>,
700    /// How a request reaches the record ([`lookup_policy`]):
701    /// `router_and_key` — the φ router sends the request, the key is then
702    /// looked up in it (also when the field is absent); `key_first` — a
703    /// STRONG key in the message (≥ 2 words: an exact or stem n-gram, a
704    /// Latin binomial) sends the request to the record even when the
705    /// router picked the backbone, a one-word key still needs the router.
706    /// Additive: files written before 26.09.2026 carry no field.
707    #[serde(default, skip_serializing_if = "Option::is_none")]
708    pub policy: Option<String>,
709}
710
711/// Values of `LookupInfo.policy` (spec §9.5.2).
712pub mod lookup_policy {
713    /// The router decides; the table is consulted for what it sent.
714    pub const ROUTER_AND_KEY: &str = "router_and_key";
715    /// A strong key in the message takes the request past a backbone
716    /// decision of the router.
717    pub const KEY_FIRST: &str = "key_first";
718    /// Every known value.
719    pub const ALL: &[&str] = &[ROUTER_AND_KEY, KEY_FIRST];
720    /// The policy of a record without the field.
721    pub const DEFAULT: &str = ROUTER_AND_KEY;
722}
723
724/// `(skill id, value)` of every lookup record whose `lookup.policy` this
725/// reader does not know ([`lookup_policy::ALL`]). `open()` accepts such a
726/// file and warns; the runtime reads the record as
727/// [`lookup_policy::DEFAULT`] — the conservative reading, the one a reader
728/// from before the field existed applies (review KF-6).
729pub fn unknown_lookup_policies(header: &CmfHeader) -> Vec<(String, String)> {
730    header
731        .skills
732        .iter()
733        .filter(|s| s.kind.as_deref() == Some(skill_kind::LOOKUP))
734        .filter_map(|s| {
735            let p = s.lookup.as_ref()?.policy.as_ref()?;
736            (!lookup_policy::ALL.contains(&p.as_str())).then(|| (s.id.clone(), p.clone()))
737        })
738        .collect()
739}
740
741/// The writers' rule: every lookup record's `lookup.policy`, when present,
742/// is one this writer knows — `append_skill`, `update_header_append` and
743/// the full writers refuse anything else before a byte is written
744/// (`open()` only warns, [`unknown_lookup_policies`]).
745pub fn check_lookup_policies(header: &CmfHeader) -> Result<(), CmfError> {
746    match unknown_lookup_policies(header).first() {
747        None => Ok(()),
748        Some((id, p)) => Err(perr(format!(
749            "skill '{id}': lookup.policy '{p}' (expected {})",
750            lookup_policy::ALL.join(" | ")
751        ))),
752    }
753}
754
755impl LookupInfo {
756    /// The declared policy, [`lookup_policy::DEFAULT`] when absent.
757    pub fn policy_label(&self) -> &str {
758        self.policy.as_deref().unwrap_or(lookup_policy::DEFAULT)
759    }
760
761    /// `E · L` — slots of the text blob (`None` on overflow).
762    pub fn slots(&self) -> Option<usize> {
763        self.entries.checked_mul(self.langs.len())
764    }
765
766    /// Slot index of `entry` in language `lang` (`None` when out of range).
767    pub fn slot(&self, entry: usize, lang: usize) -> Option<usize> {
768        (entry < self.entries && lang < self.langs.len()).then(|| entry * self.langs.len() + lang)
769    }
770}
771
772/// Tensor leaves of a lookup record (`skill.{id}.` + leaf).
773pub mod lookup_leaf {
774    /// `[K]` u64 — hash64 of every normalised key, sorted ascending, unique
775    /// (the runtime binary-searches it).
776    pub const KEYS_HASH: &str = "lookup.keys.hash";
777    /// `[K]` u32 — the entry each key names (`< E`), parallel to `keys.hash`.
778    pub const KEYS_ENTRY: &str = "lookup.keys.entry";
779    /// `[E·L + 1]` u64 — byte offsets into `text`: slot `s` is
780    /// `text[off[s]..off[s+1]]`, monotone, all within the blob.
781    pub const ENTRIES_OFF: &str = "lookup.entries.off";
782    /// `[N]` u8 — UTF-8 blob; every slot is a JSON object
783    /// `{"card": "...", "fields": {field: text}}`.
784    pub const TEXT: &str = "lookup.text";
785    /// Every leaf a lookup record carries — exactly these.
786    pub const ALL: &[&str] = &[KEYS_HASH, KEYS_ENTRY, ENTRIES_OFF, TEXT];
787}
788
789/// `skill.{id}.{leaf}` for one of [`lookup_leaf`].
790pub fn lookup_tensor_name(id: &str, leaf: &str) -> String {
791    format!("skill.{id}.{leaf}")
792}
793
794/// `cmf-key-v2` ([`KEY_NORM`]): NFC → every combining mark and every
795/// invisible format character is dropped WITHOUT ending the word (a
796/// stress mark over a Cyrillic vowel has no precomposed form, so NFC
797/// leaves U+0301 standing: `Рома́шка` → `ромашка`; soft hyphen,
798/// zero-width space / joiner / non-joiner, word joiner, BOM: `ро­машка`
799/// → `ромашка`) → a precomposed LATIN letter loses its diacritics
800/// (`Pínus` → `pinus`, `école` → `ecole`; a Cyrillic letter keeps its
801/// mark — `й`, `ё`, `ў` are letters, not accents) → lowercase → every
802/// other char that is not alphanumeric (Unicode) becomes a space → runs
803/// of spaces collapse → trim. Shared by the builder (keys) and the
804/// runtime (n-grams of the user message), so
805/// `normalize_key("Пихта бальзамическая (Abies balsamea)")` ==
806/// `"пихта бальзамическая abies balsamea"`. Idempotent.
807pub fn normalize_key(s: &str) -> String {
808    use unicode_normalization::UnicodeNormalization;
809    use unicode_normalization::char::{decompose_canonical, is_combining_mark};
810    /// Invisible format characters that sit inside a word.
811    fn is_format(c: char) -> bool {
812        matches!(
813            c,
814            '\u{00AD}' | '\u{200B}'..='\u{200F}' | '\u{2060}'..='\u{2064}' | '\u{FEFF}'
815        )
816    }
817    /// Latin-1 Supplement, Latin Extended-A/B, Latin Extended Additional:
818    /// where the precomposed Latin letters with diacritics live.
819    fn is_latin_precomposed(c: char) -> bool {
820        matches!(c, '\u{00C0}'..='\u{024F}' | '\u{1E00}'..='\u{1EFF}')
821    }
822    fn emit(out: &mut String, gap: &mut bool, c: char) {
823        for l in c.to_lowercase() {
824            if is_combining_mark(l) {
825                continue;
826            }
827            if l.is_alphanumeric() {
828                if *gap && !out.is_empty() {
829                    out.push(' ');
830                }
831                *gap = false;
832                out.push(l);
833            } else {
834                *gap = true;
835            }
836        }
837    }
838    let mut out = String::with_capacity(s.len());
839    let mut gap = false;
840    for c in s.nfc() {
841        if is_combining_mark(c) || is_format(c) {
842            continue;
843        }
844        if is_latin_precomposed(c) {
845            decompose_canonical(c, |d| {
846                if !is_combining_mark(d) {
847                    emit(&mut out, &mut gap, d);
848                }
849            });
850        } else {
851            emit(&mut out, &mut gap, c);
852        }
853    }
854    out
855}
856
857/// hash64 of the UTF-8 bytes of an ALREADY normalised key (the runtime
858/// normalises the whole message once and hashes its n-grams).
859pub fn normalized_key_hash(norm: &str) -> u64 {
860    hash64(norm.as_bytes())
861}
862
863/// hash64 of [`normalize_key`]`(key)` — the value stored in `keys.hash`.
864pub fn key_hash(key: &str) -> u64 {
865    normalized_key_hash(&normalize_key(key))
866}
867
868/// The state effect of a lookup record: it never touches the network, so
869/// `first_affected_layer 0`, `transparent`, no state added — and the
870/// validator requires exactly this.
871pub fn lookup_state_effect() -> StateEffect {
872    StateEffect {
873        first_affected_layer: 0,
874        switch: state_switch::TRANSPARENT.into(),
875        state_bytes_added: 0,
876    }
877}
878
879/// Little-endian `u64` words of a raw payload (`u64` tensors).
880pub fn read_u64_le(bytes: &[u8]) -> Vec<u64> {
881    bytes
882        .chunks_exact(8)
883        .map(|c| u64::from_le_bytes(c.try_into().unwrap()))
884        .collect()
885}
886
887/// Little-endian `u32` words of a raw payload (`u32` tensors).
888pub fn read_u32_le(bytes: &[u8]) -> Vec<u32> {
889    bytes
890        .chunks_exact(4)
891        .map(|c| u32::from_le_bytes(c.try_into().unwrap()))
892        .collect()
893}
894
895/// The four tensors of the lookup record `id` from hashed keys and slot
896/// texts: `keys` = `(key_hash, entry)` in any order, `slots[entry · L +
897/// lang]` = the JSON object text of that card in that language (`E · L`
898/// of them, `L = info.langs.len()`). Sorts the keys by hash and refuses
899/// a duplicate hash (two keys that normalise alike — the builder drops
900/// and reports them BEFORE calling this), an entry `≥ E`, a key count or
901/// slot count that disagrees with `info`, a slot that is not a JSON
902/// object. What comes back passes the record validation by construction.
903pub fn lookup_tensors(
904    id: &str,
905    info: &LookupInfo,
906    keys: &[(u64, u32)],
907    slots: &[&str],
908) -> Result<Vec<TensorSpec>, CmfError> {
909    let n_slots = info
910        .slots()
911        .ok_or_else(|| perr(format!("lookup '{id}': entries × langs overflows")))?;
912    if info.entries == 0 || info.langs.is_empty() {
913        return Err(perr(format!(
914            "lookup '{id}': entries {} and langs {:?} must both be non-empty",
915            info.entries, info.langs
916        )));
917    }
918    if keys.is_empty() || keys.len() != info.keys {
919        return Err(perr(format!(
920            "lookup '{id}': {} keys given, lookup.keys says {} (≥ 1)",
921            keys.len(),
922            info.keys
923        )));
924    }
925    if slots.len() != n_slots {
926        return Err(perr(format!(
927            "lookup '{id}': {} slots given, entries {} × langs {} = {n_slots}",
928            slots.len(),
929            info.entries,
930            info.langs.len()
931        )));
932    }
933    let mut sorted: Vec<(u64, u32)> = keys.to_vec();
934    sorted.sort_unstable();
935    for w in sorted.windows(2) {
936        if w[0].0 == w[1].0 {
937            return Err(perr(format!(
938                "lookup '{id}': duplicate key hash {} (entries {} and {})",
939                hex64(w[0].0),
940                w[0].1,
941                w[1].1
942            )));
943        }
944    }
945    for &(h, e) in &sorted {
946        if e as usize >= info.entries {
947            return Err(perr(format!(
948                "lookup '{id}': key {} points at entry {e} of {}",
949                hex64(h),
950                info.entries
951            )));
952        }
953    }
954    let mut text = Vec::new();
955    let mut off = Vec::with_capacity(n_slots + 1);
956    off.push(0u64);
957    for (s, slot) in slots.iter().enumerate() {
958        let v: serde_json::Value = serde_json::from_str(slot)
959            .map_err(|e| perr(format!("lookup '{id}': slot {s} is not JSON: {e}")))?;
960        if !v.is_object() {
961            return Err(perr(format!(
962                "lookup '{id}': slot {s} is not a JSON object"
963            )));
964        }
965        text.extend_from_slice(slot.as_bytes());
966        off.push(text.len() as u64);
967    }
968    let u64s = |v: &[u64]| v.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>();
969    let hashes: Vec<u64> = sorted.iter().map(|k| k.0).collect();
970    let entries: Vec<u8> = sorted.iter().flat_map(|k| k.1.to_le_bytes()).collect();
971    Ok(vec![
972        TensorSpec {
973            name: lookup_tensor_name(id, lookup_leaf::KEYS_HASH),
974            dtype: TensorDtype::U64,
975            shape: vec![hashes.len()],
976            data: u64s(&hashes),
977        },
978        TensorSpec {
979            name: lookup_tensor_name(id, lookup_leaf::KEYS_ENTRY),
980            dtype: TensorDtype::U32,
981            shape: vec![sorted.len()],
982            data: entries,
983        },
984        TensorSpec {
985            name: lookup_tensor_name(id, lookup_leaf::ENTRIES_OFF),
986            dtype: TensorDtype::U64,
987            shape: vec![off.len()],
988            data: u64s(&off),
989        },
990        TensorSpec {
991            name: lookup_tensor_name(id, lookup_leaf::TEXT),
992            dtype: TensorDtype::U8,
993            shape: vec![text.len()],
994            data: text,
995        },
996    ])
997}
998
999/// The value rules of lookup tables, which the directory cannot express:
1000/// `keys.hash` strictly ascending (sorted, unique — the runtime
1001/// binary-searches it), every `keys.entry < E`, `entries.off` monotone
1002/// and within the blob, the blob valid UTF-8 with every slot boundary on
1003/// a character boundary, every slot shaped as a JSON object (first
1004/// non-blank byte `{`, last `}`). With `deep` every slot is also parsed
1005/// as JSON: what a WRITER does with the payload it is about to commit;
1006/// `open()` passes `false` — the runtime reads a slot lazily and reports
1007/// a slot that does not parse at that request, so parsing every card of
1008/// a table of hundreds of MB on every open of the file would only cost
1009/// time. `bytes_of` yields an entry's payload (`None` = not available →
1010/// refused). Called after [`validate_knowledge`], which fixed dtypes and
1011/// shapes. One pass over the table: O(K + N).
1012pub fn validate_lookup_values<'a>(
1013    header: &CmfHeader,
1014    tensors: &[TensorEntry],
1015    deep: bool,
1016    mut bytes_of: impl FnMut(&TensorEntry) -> Option<Cow<'a, [u8]>>,
1017) -> Result<(), CmfError> {
1018    for s in header
1019        .skills
1020        .iter()
1021        .filter(|s| s.kind.as_deref() == Some(skill_kind::LOOKUP))
1022    {
1023        let id = &s.id;
1024        let Some(info) = s.lookup.as_ref() else {
1025            continue; // refused by validate_knowledge
1026        };
1027        let mut get = |leaf: &str| -> Result<Cow<'a, [u8]>, CmfError> {
1028            let name = lookup_tensor_name(id, leaf);
1029            let t = tensors
1030                .iter()
1031                .find(|t| t.name == name)
1032                .ok_or_else(|| perr(format!("skill '{id}': missing tensor '{name}'")))?;
1033            bytes_of(t).ok_or_else(|| {
1034                perr(format!(
1035                    "skill '{id}': payload of '{name}' is not available for the value check"
1036                ))
1037            })
1038        };
1039        let hashes = read_u64_le(&get(lookup_leaf::KEYS_HASH)?);
1040        let entries = read_u32_le(&get(lookup_leaf::KEYS_ENTRY)?);
1041        let offs = read_u64_le(&get(lookup_leaf::ENTRIES_OFF)?);
1042        let text = get(lookup_leaf::TEXT)?;
1043        for (i, w) in hashes.windows(2).enumerate() {
1044            if w[1] == w[0] {
1045                return Err(perr(format!(
1046                    "skill '{id}': duplicate key hash {} in '{}' (keys {i} and {})",
1047                    hex64(w[0]),
1048                    lookup_leaf::KEYS_HASH,
1049                    i + 1
1050                )));
1051            }
1052            if w[1] < w[0] {
1053                return Err(perr(format!(
1054                    "skill '{id}': '{}' is not sorted ascending at key {} ({} after {}) — the \
1055                     runtime binary-searches it",
1056                    lookup_leaf::KEYS_HASH,
1057                    i + 1,
1058                    hex64(w[1]),
1059                    hex64(w[0])
1060                )));
1061            }
1062        }
1063        for (i, &e) in entries.iter().enumerate() {
1064            if e as usize >= info.entries {
1065                return Err(perr(format!(
1066                    "skill '{id}': key {i} points at entry {e} of {} (lookup.entries)",
1067                    info.entries
1068                )));
1069            }
1070        }
1071        for (i, w) in offs.windows(2).enumerate() {
1072            if w[1] < w[0] {
1073                return Err(perr(format!(
1074                    "skill '{id}': '{}' is not monotone at slot {i} ({} then {})",
1075                    lookup_leaf::ENTRIES_OFF,
1076                    w[0],
1077                    w[1]
1078                )));
1079            }
1080        }
1081        if let Some(&last) = offs.last() {
1082            if last > text.len() as u64 {
1083                return Err(perr(format!(
1084                    "skill '{id}': '{}'[{}] = {last} is beyond the text blob ({} bytes)",
1085                    lookup_leaf::ENTRIES_OFF,
1086                    offs.len() - 1,
1087                    text.len()
1088                )));
1089            }
1090        }
1091        let l = info.langs.len().max(1);
1092        let where_ = |slot: usize| {
1093            format!(
1094                "slot {slot} (entry {}, lang '{}')",
1095                slot / l,
1096                info.langs.get(slot % l).map(String::as_str).unwrap_or("?")
1097            )
1098        };
1099        // One UTF-8 pass over the whole blob; a failure is reported for
1100        // the slot that holds the offending byte.
1101        let text_str = std::str::from_utf8(&text).map_err(|e| {
1102            let at = e.valid_up_to() as u64;
1103            let slot = offs.partition_point(|&o| o <= at).saturating_sub(1);
1104            perr(format!("skill '{id}': {} is not UTF-8: {e}", where_(slot)))
1105        })?;
1106        for (slot, w) in offs.windows(2).enumerate() {
1107            let (a, b) = (w[0] as usize, w[1] as usize);
1108            if !text_str.is_char_boundary(a) || !text_str.is_char_boundary(b) {
1109                return Err(perr(format!(
1110                    "skill '{id}': {} does not start and end on a UTF-8 character boundary \
1111                     ({a}..{b})",
1112                    where_(slot)
1113                )));
1114            }
1115            let txt = &text_str[a..b];
1116            let shaped = txt.trim_start().as_bytes().first() == Some(&b'{')
1117                && txt.trim_end().as_bytes().last() == Some(&b'}');
1118            if !shaped {
1119                return Err(perr(format!(
1120                    "skill '{id}': {} is not a JSON object",
1121                    where_(slot)
1122                )));
1123            }
1124            if deep {
1125                let v: serde_json::Value = serde_json::from_str(txt).map_err(|e| {
1126                    perr(format!(
1127                        "skill '{id}': {} is not a JSON object: {e}",
1128                        where_(slot)
1129                    ))
1130                })?;
1131                if !v.is_object() {
1132                    return Err(perr(format!(
1133                        "skill '{id}': {} is not a JSON object",
1134                        where_(slot)
1135                    )));
1136                }
1137            }
1138        }
1139    }
1140    Ok(())
1141}
1142
1143impl SkillRecord {
1144    /// A v2 record (`kind` set) — validated by the SKILLS_V2 rules.
1145    pub fn is_v2(&self) -> bool {
1146        self.kind.is_some()
1147    }
1148
1149    /// May the router-v2 policy auto-route to this skill? Requires a v2
1150    /// record with a selection descriptor, `status == "active"` and a
1151    /// measured gate (`gate.status == "measured"`).
1152    pub fn is_auto_routable(&self) -> bool {
1153        self.is_v2()
1154            && self.selection.is_some()
1155            && self.status.as_deref() == Some("active")
1156            && self
1157                .gate
1158                .as_ref()
1159                .and_then(|g| g.get("status"))
1160                .and_then(|s| s.as_str())
1161                == Some("measured")
1162    }
1163
1164    fn has_v2_fields(&self) -> bool {
1165        !self.overrides.is_empty()
1166            || self.bound.is_some()
1167            || self.state_effect.is_some()
1168            || self.status.is_some()
1169            || self.gate.is_some()
1170            || self.prompt_contract.is_some()
1171            || self.origin.is_some()
1172            || self.experts.is_some()
1173            || self.lookup.is_some()
1174    }
1175}
1176
1177// ───────────────────────── router policy v2 ─────────────────────────
1178
1179/// `header.router` (bit `ROUTER_V2`): the per-request backbone-gated
1180/// policy. The backbone is the default; a skill runs only when the
1181/// calibrated decision says so (`cortiq-engine` `router::decide_backbone_gated`).
1182#[derive(Debug, Clone, Serialize, Deserialize)]
1183pub struct RouterPolicy {
1184    /// 2.
1185    pub version: u32,
1186    /// `"backbone_gated"` (the only value).
1187    pub policy: String,
1188    /// `"request"` (the only value today).
1189    pub granularity: String,
1190    /// How φ is computed — identical in trainer and runtime.
1191    pub phi: PhiSpec,
1192    /// Descriptor of the BACKBONE class (general prompts), same schema and
1193    /// metric as the skills'.
1194    pub base: SelectionDescriptor,
1195    /// A skill must beat the backbone by this much in unit error E.
1196    pub margin: f32,
1197    /// hex hash64 over the base descriptor + every calibrated skill
1198    /// descriptor (`cortiq-engine` `router::skills_hash`). A mismatch means
1199    /// the calibration is stale → the backbone runs.
1200    pub skills_hash: String,
1201    /// `{in_scope_recall, false_accept, false_accept_upper95, n_in,
1202    /// n_general, general_sha256, in_sha256}`.
1203    #[serde(default, skip_serializing_if = "Option::is_none")]
1204    pub measured: Option<serde_json::Value>,
1205}
1206
1207/// Canonical φ(q) of a user message q: `ids = prefix_ids ++ encode(q) ++
1208/// suffix_ids` (encode without BOS or special handling), run the backbone
1209/// through `layer`, take the hidden AFTER that layer, mean over the
1210/// positions of `encode(q)` only (`span_mean`), unit-normalize (`unit`).
1211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1212pub struct PhiSpec {
1213    pub layer: usize,
1214    /// `"span_mean"`.
1215    pub pool: String,
1216    /// `"unit"`.
1217    pub norm: String,
1218    #[serde(default)]
1219    pub prefix_ids: Vec<u32>,
1220    #[serde(default)]
1221    pub suffix_ids: Vec<u32>,
1222}
1223
1224// ───────────────────────── trunk hash ─────────────────────────
1225
1226/// Is this directory entry part of the trunk (not a skill or router
1227/// tensor)?
1228pub fn is_trunk_tensor(name: &str) -> bool {
1229    !(name.starts_with("skill.") || name.starts_with("route."))
1230}
1231
1232/// hash64 of the genome's trunk. Input, in order:
1233///
1234/// 1. `b"cmf-trunk-v1\0"`;
1235/// 2. every directory entry outside `skill.` / `route.`, sorted by name
1236///    (bytewise): `name 0x00 dtype-u8 0x00 ndim-u8 shape(u64 LE each) 0x00
1237///    hash(u64 LE)`;
1238/// 3. `0x1e` + canonical JSON of the `arch` object ([`canonical_json`]:
1239///    object keys sorted, no whitespace);
1240/// 4. `0x1e` + hash64 of the vocab section (u64 LE, 0 when absent);
1241/// 5. `0x1e` + canonical JSON of `tokenizer_config` (`null` when absent).
1242///
1243/// The JSON parts are taken AS WRITTEN: this function serializes the
1244/// records exactly as the header writer does and canonicalizes the parsed
1245/// result, and `open()` canonicalizes the raw header JSON of the file
1246/// ([`trunk_hash_json`]) — the two agree byte for byte, and an arch field
1247/// a newer writer added (additive evolution) is covered instead of silently
1248/// dropped by an older struct.
1249///
1250/// Directory-level: payload bytes enter through the entry hashes, so
1251/// `open()` recomputes it cheaply and `verify` (which re-hashes payloads)
1252/// catches a payload changed under an unchanged directory.
1253///
1254/// This form covers a file WITHOUT mask / sparse-index sections; a file
1255/// that carries them is hashed by [`trunk_hash_exec`] (the masks are part
1256/// of how the trunk executes: `run`/`serve` apply the catalog's fallback
1257/// mask to every request, the backbone's included).
1258pub fn trunk_hash(header: &CmfHeader, tensors: &[TensorEntry], vocab: Option<&[u8]>) -> u64 {
1259    trunk_hash_exec(header, tensors, vocab, ExecHashes::default())
1260}
1261
1262/// hash64 of the execution-relevant sections outside the directory: the
1263/// DTG-MA mask catalog and the sparse index derived from it (`None` =
1264/// the section is absent).
1265#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1266pub struct ExecHashes {
1267    pub masks: Option<u64>,
1268    pub index: Option<u64>,
1269}
1270
1271impl ExecHashes {
1272    /// From the raw section bytes (empty or absent section → `None`).
1273    pub fn of(masks: Option<&[u8]>, index: Option<&[u8]>) -> Self {
1274        let h = |b: Option<&[u8]>| b.filter(|b| !b.is_empty()).map(hash64);
1275        Self {
1276            masks: h(masks),
1277            index: h(index),
1278        }
1279    }
1280
1281    fn is_empty(&self) -> bool {
1282        self.masks.is_none() && self.index.is_none()
1283    }
1284}
1285
1286/// [`trunk_hash`] with the mask / sparse-index sections (NF-1): when the
1287/// file carries either, `0x1e b"exec" 0x00` + per section (tag u8 + hash64
1288/// LE) is appended to the hash input. A file with neither hashes exactly
1289/// as [`trunk_hash`] (so genomes born without masks keep their hash), and
1290/// a mask catalog added to a genome later changes the trunk hash — the
1291/// reader refuses it unless it is a new genome.
1292pub fn trunk_hash_exec(
1293    header: &CmfHeader,
1294    tensors: &[TensorEntry],
1295    vocab: Option<&[u8]>,
1296    exec: ExecHashes,
1297) -> u64 {
1298    let as_written = |bytes: Vec<u8>| -> serde_json::Value {
1299        serde_json::from_slice(&bytes).expect("serialized JSON parses")
1300    };
1301    let arch = as_written(serde_json::to_vec(&header.arch).expect("ModelArch serializes"));
1302    let tok = as_written(serde_json::to_vec(&header.tokenizer_config).expect("bundle serializes"));
1303    trunk_hash_json_exec(&arch, &tok, tensors, vocab, exec)
1304}
1305
1306/// [`trunk_hash`] over the raw `arch` / `tokenizer_config` JSON values of a
1307/// header (`Value::Null` for an absent bundle) — the reader's form.
1308pub fn trunk_hash_json(
1309    arch: &serde_json::Value,
1310    tokenizer_config: &serde_json::Value,
1311    tensors: &[TensorEntry],
1312    vocab: Option<&[u8]>,
1313) -> u64 {
1314    trunk_hash_json_exec(arch, tokenizer_config, tensors, vocab, ExecHashes::default())
1315}
1316
1317/// [`trunk_hash_json`] with the mask / sparse-index sections
1318/// ([`trunk_hash_exec`]).
1319pub fn trunk_hash_json_exec(
1320    arch: &serde_json::Value,
1321    tokenizer_config: &serde_json::Value,
1322    tensors: &[TensorEntry],
1323    vocab: Option<&[u8]>,
1324    exec: ExecHashes,
1325) -> u64 {
1326    let mut trunk: Vec<&TensorEntry> = tensors
1327        .iter()
1328        .filter(|t| is_trunk_tensor(&t.name))
1329        .collect();
1330    trunk.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
1331    let mut buf = Vec::with_capacity(trunk.len() * 96 + 4096);
1332    buf.extend_from_slice(b"cmf-trunk-v1\0");
1333    for t in trunk {
1334        buf.extend_from_slice(t.name.as_bytes());
1335        buf.push(0);
1336        buf.push(t.dtype.id());
1337        buf.push(0);
1338        buf.push(t.shape.len() as u8);
1339        for &d in &t.shape {
1340            buf.extend_from_slice(&(d as u64).to_le_bytes());
1341        }
1342        buf.push(0);
1343        buf.extend_from_slice(&t.hash.to_le_bytes());
1344    }
1345    buf.push(0x1e);
1346    buf.extend(canonical_json(arch));
1347    buf.push(0x1e);
1348    buf.extend_from_slice(&vocab.map(hash64).unwrap_or(0).to_le_bytes());
1349    buf.push(0x1e);
1350    buf.extend(canonical_json(tokenizer_config));
1351    if !exec.is_empty() {
1352        buf.push(0x1e);
1353        buf.extend_from_slice(b"exec\0");
1354        for h in [exec.masks, exec.index] {
1355            match h {
1356                Some(v) => {
1357                    buf.push(1);
1358                    buf.extend_from_slice(&v.to_le_bytes());
1359                }
1360                None => buf.push(0),
1361            }
1362        }
1363    }
1364    hash64(&buf)
1365}
1366
1367/// Canonical JSON bytes of a value: object keys sorted bytewise at every
1368/// depth, no whitespace. Independent of map insertion order and of
1369/// serde_json's `preserve_order` feature (which the engine enables and a
1370/// core-only build does not).
1371pub fn canonical_json(v: &serde_json::Value) -> Vec<u8> {
1372    fn walk(v: &serde_json::Value, out: &mut Vec<u8>) {
1373        match v {
1374            serde_json::Value::Object(m) => {
1375                let mut keys: Vec<&String> = m.keys().collect();
1376                keys.sort_unstable_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1377                out.push(b'{');
1378                for (i, k) in keys.into_iter().enumerate() {
1379                    if i > 0 {
1380                        out.push(b',');
1381                    }
1382                    out.extend(serde_json::to_vec(k).expect("string serializes"));
1383                    out.push(b':');
1384                    walk(&m[k], out);
1385                }
1386                out.push(b'}');
1387            }
1388            serde_json::Value::Array(a) => {
1389                out.push(b'[');
1390                for (i, x) in a.iter().enumerate() {
1391                    if i > 0 {
1392                        out.push(b',');
1393                    }
1394                    walk(x, out);
1395                }
1396                out.push(b']');
1397            }
1398            other => out.extend(serde_json::to_vec(other).expect("scalar serializes")),
1399        }
1400    }
1401    let mut out = Vec::new();
1402    walk(v, &mut out);
1403    out
1404}
1405
1406/// `{:016x}` of a hash64.
1407pub fn hex64(h: u64) -> String {
1408    format!("{h:016x}")
1409}
1410
1411/// Parse a hex hash64 (as written by [`hex64`]).
1412pub fn parse_hex64(s: &str) -> Option<u64> {
1413    if s.is_empty() || s.len() > 16 {
1414        return None;
1415    }
1416    u64::from_str_radix(s, 16).ok()
1417}
1418
1419/// Writer side of the genome contract: fill an empty `trunk_hash` (and an
1420/// empty master hash when `encoding == "f32"`) from the content being
1421/// written; refuse a non-empty one that disagrees — a trunk that changed
1422/// must become a new genome (or a `requant` with a rewritten hash), never
1423/// the old one with different bytes.
1424pub(crate) fn seal_genome(
1425    header: &mut CmfHeader,
1426    tensors: &[TensorEntry],
1427    vocab: Option<&[u8]>,
1428    exec: ExecHashes,
1429) -> Result<(), CmfError> {
1430    let computed = trunk_hash_exec(header, tensors, vocab, exec);
1431    // E0 of a resonance-routed MoE genome, filled once (the arch is hashed;
1432    // the reader refuses a disagreeing value).
1433    let e0 = header
1434        .arch
1435        .moe
1436        .as_ref()
1437        .filter(|m| m.router_resonance)
1438        .map(|m| m.num_experts);
1439    let Some(g) = header.genome.as_mut() else {
1440        return Ok(());
1441    };
1442    if g.moe_experts.is_none() {
1443        g.moe_experts = e0;
1444    }
1445    if g.trunk_hash.is_empty() {
1446        g.trunk_hash = hex64(computed);
1447    } else {
1448        let stored = parse_hex64(&g.trunk_hash).ok_or_else(|| {
1449            CmfError::Parse(format!(
1450                "genome.trunk_hash '{}' is not a hex hash64",
1451                g.trunk_hash
1452            ))
1453        })?;
1454        if stored != computed {
1455            return Err(CmfError::Parse(format!(
1456                "refusing to write genome '{}': genome.trunk_hash {} != trunk content {} — \
1457                 the trunk changed without a new genome (a new id/generation with a lineage \
1458                 event, or a requant that rewrites trunk_hash/encoding)",
1459                g.id,
1460                g.trunk_hash,
1461                hex64(computed)
1462            )));
1463        }
1464    }
1465    if g.master_trunk_hash.is_empty() && g.encoding == "f32" {
1466        g.master_trunk_hash = g.trunk_hash.clone();
1467    }
1468    Ok(())
1469}
1470
1471// ───────────────────────── validation ─────────────────────────
1472
1473/// Knowledge bits (all three are derived from content).
1474pub const KNOWLEDGE_BITS: u32 = features::GENOME | features::SKILLS_V2 | features::ROUTER_V2;
1475
1476/// The knowledge bits this header's content requires.
1477pub fn knowledge_bits(header: &CmfHeader) -> u32 {
1478    let mut bits = 0;
1479    if header.genome.is_some() {
1480        bits |= features::GENOME;
1481    }
1482    if header.skills.iter().any(|s| s.kind.is_some()) {
1483        bits |= features::SKILLS_V2;
1484    }
1485    if header.router.is_some() {
1486        bits |= features::ROUTER_V2;
1487    }
1488    bits
1489}
1490
1491fn perr(msg: String) -> CmfError {
1492    CmfError::Parse(msg)
1493}
1494
1495/// `model.layers.{i}.` → i.
1496fn layer_of(name: &str) -> Option<usize> {
1497    let rest = name.strip_prefix("model.layers.")?;
1498    let (li, _) = rest.split_once('.')?;
1499    li.parse().ok()
1500}
1501
1502/// Every knowledge rule (§9.2–§9.5) over a header + directory: the bits ⇔
1503/// content in both directions, the genome's trunk hash, the lineage order,
1504/// segment bounds, every v2 skill record and the router policy. Called by
1505/// `open()` (with the envelope's bits) and by every writer before a byte
1506/// is written. Files carrying none of the new bits and no v2 content pass
1507/// untouched.
1508///
1509/// `raw_trunk_hash`: the trunk hash `open()` computed from the file's raw
1510/// header JSON ([`trunk_hash_json_exec`]); `None` = compute it from
1511/// `header` (writers, whose header is exactly what they write). `exec`:
1512/// the mask / sparse-index sections the file carries ([`ExecHashes`]).
1513pub fn validate_knowledge(
1514    header: &CmfHeader,
1515    tensors: &[TensorEntry],
1516    vocab: Option<&[u8]>,
1517    required_features: u32,
1518    data_len: Option<u64>,
1519    raw_trunk_hash: Option<u64>,
1520    exec: ExecHashes,
1521) -> Result<(), CmfError> {
1522    // 1. bit ⇔ content, both ways (like BOUNDED_STATE).
1523    let content = knowledge_bits(header);
1524    for (bit, name, what) in [
1525        (features::GENOME, "GENOME", "genome record"),
1526        (features::SKILLS_V2, "SKILLS_V2", "v2 skill record (kind)"),
1527        (features::ROUTER_V2, "ROUTER_V2", "router policy"),
1528    ] {
1529        let declared = content & bit != 0;
1530        let set = required_features & bit != 0;
1531        if declared != set {
1532            return Err(perr(format!(
1533                "{what} ({}) and {name} feature bit ({}) disagree",
1534                if declared { "present" } else { "absent" },
1535                if set { "set" } else { "clear" }
1536            )));
1537        }
1538    }
1539    // v2 fields on a record without `kind` would be invisible to the bit.
1540    for s in &header.skills {
1541        if s.kind.is_none() && s.has_v2_fields() {
1542            return Err(perr(format!(
1543                "skill '{}': v2 fields (overrides/bound/state_effect/status/gate/\
1544                 prompt_contract/origin/experts/lookup) on a record without `kind`",
1545                s.id
1546            )));
1547        }
1548    }
1549    let new_file = content != 0 || !header.segments.is_empty() || !header.lineage.is_empty();
1550    if !new_file {
1551        return Ok(());
1552    }
1553
1554    // 2. genome.
1555    if let Some(g) = &header.genome {
1556        if header.shard.is_some() {
1557            return Err(perr(
1558                "a genome file cannot be sharded (the trunk hash covers one directory)".into(),
1559            ));
1560        }
1561        if g.id.is_empty() {
1562            return Err(perr("genome.id is empty".into()));
1563        }
1564        if !GENOME_STATUSES.contains(&g.status.as_str()) {
1565            return Err(perr(format!(
1566                "genome.status '{}' is not one of {GENOME_STATUSES:?}",
1567                g.status
1568            )));
1569        }
1570        if g.encoding.is_empty() {
1571            return Err(perr("genome.encoding is empty".into()));
1572        }
1573        // The trunk's tokenizer is part of the genome: without an embedded
1574        // VOCAB the hash covers "no vocab" and a reader would pick up
1575        // whatever tokenizer.json lies beside the file (NF-5).
1576        if vocab.is_none_or(|v| v.is_empty()) {
1577            return Err(perr(format!(
1578                "genome '{}': a genome file must embed its tokenizer (VOCAB section) — the \
1579                 trunk hash binds it; a sidecar tokenizer.json is not part of the genome",
1580                g.id
1581            )));
1582        }
1583        // O(1) attention of a genome is set by its arch (hashed); a
1584        // provenance hint would switch the trunk's operator without a new
1585        // genome (NF-6).
1586        if header
1587            .provenance
1588            .as_ref()
1589            .and_then(|p| p.get("o1_attn"))
1590            .is_some()
1591        {
1592            return Err(perr(format!(
1593                "genome '{}': provenance.o1_attn is refused on a genome file — the trunk's \
1594                 attention operator is part of the genome (arch), not a provenance hint",
1595                g.id
1596            )));
1597        }
1598        let stored = parse_hex64(&g.trunk_hash).ok_or_else(|| {
1599            perr(format!(
1600                "genome.trunk_hash '{}' is not a hex hash64",
1601                g.trunk_hash
1602            ))
1603        })?;
1604        let master = parse_hex64(&g.master_trunk_hash).ok_or_else(|| {
1605            perr(format!(
1606                "genome.master_trunk_hash '{}' is not a hex hash64",
1607                g.master_trunk_hash
1608            ))
1609        })?;
1610        if g.encoding == "f32" && master != stored {
1611            return Err(perr(format!(
1612                "genome.encoding is f32 but master_trunk_hash {} != trunk_hash {}",
1613                g.master_trunk_hash, g.trunk_hash
1614            )));
1615        }
1616        // E0 is the arch's expert count (the arch is hashed; the field is
1617        // the growth chain's origin and may not drift from it).
1618        if let Some(n) = g.moe_experts {
1619            match &header.arch.moe {
1620                Some(m) if m.num_experts == n => {}
1621                Some(m) => {
1622                    return Err(perr(format!(
1623                        "genome '{}': moe_experts {n} != arch.moe.num_experts {}",
1624                        g.id, m.num_experts
1625                    )));
1626                }
1627                None => {
1628                    return Err(perr(format!(
1629                        "genome '{}': moe_experts {n} on an arch without a MoE block",
1630                        g.id
1631                    )));
1632                }
1633            }
1634        }
1635        let computed =
1636            raw_trunk_hash.unwrap_or_else(|| trunk_hash_exec(header, tensors, vocab, exec));
1637        if computed != stored {
1638            return Err(perr(format!(
1639                "trunk_hash mismatch: the genome's bytes changed without a new genome \
1640                 (lineage) — genome '{}' gen {} declares {}, the directory hashes to {}",
1641                g.id,
1642                g.generation,
1643                g.trunk_hash,
1644                hex64(computed)
1645            )));
1646        }
1647    }
1648
1649    // 3. lineage order.
1650    for w in header.lineage.windows(2) {
1651        if w[1].seq <= w[0].seq {
1652            return Err(perr(format!(
1653                "lineage seq must strictly increase ({} then {})",
1654                w[0].seq, w[1].seq
1655            )));
1656        }
1657    }
1658    for e in &header.lineage {
1659        if e.event.is_empty() {
1660            return Err(perr(format!("lineage event #{} has an empty name", e.seq)));
1661        }
1662    }
1663
1664    // 4. segments.
1665    for s in &header.segments {
1666        if !SEGMENT_KINDS.contains(&s.kind.as_str()) {
1667            return Err(perr(format!(
1668                "segment kind '{}' is not one of {SEGMENT_KINDS:?}",
1669                s.kind
1670            )));
1671        }
1672        let over = data_len.is_some_and(|dl| s.data_end > dl);
1673        if s.data_start > s.data_end || over {
1674            return Err(perr(format!(
1675                "segment {}:{} [{}, {}) is outside the data section",
1676                s.kind, s.id, s.data_start, s.data_end
1677            )));
1678        }
1679    }
1680
1681    // 5. skill records.
1682    let by_name: HashMap<&str, &TensorEntry> =
1683        tensors.iter().map(|t| (t.name.as_str(), t)).collect();
1684    let mut ids = BTreeSet::new();
1685    for (at, s) in header.skills.iter().enumerate() {
1686        if !ids.insert(s.id.as_str()) {
1687            return Err(perr(format!("duplicate skill id '{}'", s.id)));
1688        }
1689        if s.id == BACKBONE_CLASS_ID {
1690            return Err(perr(format!(
1691                "skill id '{BACKBONE_CLASS_ID}' is reserved for the backbone class"
1692            )));
1693        }
1694        if s.kind.is_some() {
1695            validate_skill_v2(header, at, s, tensors, &by_name)?;
1696        }
1697    }
1698    // every skill tensor belongs to a record
1699    for t in tensors {
1700        if let Some(rest) = t.name.strip_prefix("skill.") {
1701            let owned = header.skills.iter().any(|s| {
1702                rest.strip_prefix(s.id.as_str())
1703                    .is_some_and(|r| r.starts_with('.'))
1704            });
1705            if !owned {
1706                return Err(perr(format!(
1707                    "tensor '{}' belongs to no skill record",
1708                    t.name
1709                )));
1710            }
1711        }
1712    }
1713
1714    // 6. router policy.
1715    if let Some(r) = &header.router {
1716        validate_router(header, r)?;
1717    }
1718    Ok(())
1719}
1720
1721fn validate_skill_v2(
1722    header: &CmfHeader,
1723    at: usize,
1724    s: &SkillRecord,
1725    tensors: &[TensorEntry],
1726    by_name: &HashMap<&str, &TensorEntry>,
1727) -> Result<(), CmfError> {
1728    let id = &s.id;
1729    if id.is_empty() || id.contains('.') {
1730        return Err(perr(format!(
1731            "skill '{id}': a v2 skill id must be non-empty and contain no '.' \
1732             (it prefixes its tensors: skill.{{id}}.X)"
1733        )));
1734    }
1735    if RESERVED_SKILL_IDS.contains(&id.as_str()) {
1736        return Err(perr(format!(
1737            "skill '{id}': the id is reserved ({RESERVED_SKILL_IDS:?} name the backbone / the \
1738             automatic decision in --skill, --route and route reports)"
1739        )));
1740    }
1741    let kind = s.kind.as_deref().unwrap_or_default();
1742    if skill_kind::RESERVED.contains(&kind) {
1743        return Err(perr(format!(
1744            "skill '{id}': kind '{kind}' is reserved and not implemented by this reader"
1745        )));
1746    }
1747    if !skill_kind::IMPLEMENTED.contains(&kind) {
1748        return Err(perr(format!("skill '{id}': unknown kind '{kind}'")));
1749    }
1750    if let Some(st) = &s.status {
1751        if !SKILL_STATUSES.contains(&st.as_str()) {
1752            return Err(perr(format!(
1753                "skill '{id}': status '{st}' is not one of {SKILL_STATUSES:?}"
1754            )));
1755        }
1756    }
1757    // Bound to exactly this genome (the f32 master's hash).
1758    let bound = s
1759        .bound
1760        .as_ref()
1761        .ok_or_else(|| perr(format!("skill '{id}': v2 record has no `bound`")))?;
1762    let g = header.genome.as_ref().ok_or_else(|| {
1763        perr(format!(
1764            "skill '{id}': v2 record in a file without a genome"
1765        ))
1766    })?;
1767    if bound.genome_id != g.id {
1768        return Err(perr(format!(
1769            "skill '{id}': bound.genome_id '{}' != genome.id '{}'",
1770            bound.genome_id, g.id
1771        )));
1772    }
1773    if bound.generation != g.generation {
1774        return Err(perr(format!(
1775            "skill '{id}': bound.generation {} != genome.generation {}",
1776            bound.generation, g.generation
1777        )));
1778    }
1779    let bm = parse_hex64(&bound.master_trunk_hash);
1780    if bm.is_none() || bm != parse_hex64(&g.master_trunk_hash) {
1781        return Err(perr(format!(
1782            "skill '{id}': bound.master_trunk_hash {} != genome.master_trunk_hash {}",
1783            bound.master_trunk_hash, g.master_trunk_hash
1784        )));
1785    }
1786    match kind {
1787        skill_kind::EXPERT_APPEND => validate_expert_append(header, at, s, tensors, by_name),
1788        skill_kind::LOOKUP => validate_lookup(s, tensors, by_name),
1789        _ => validate_ffn_replace(header, s, tensors, by_name),
1790    }
1791}
1792
1793/// `lookup` (spec §9.5.2): `lookup` present and consistent, exactly the
1794/// four tensors of [`lookup_leaf`] with their dtypes and shapes, no
1795/// overrides / experts / layers, the transparent state effect. Values
1796/// (sorted unique hashes, entries in range, monotone offsets, JSON
1797/// slots) are checked by [`validate_lookup_values`] where the payload is
1798/// at hand.
1799fn validate_lookup(
1800    s: &SkillRecord,
1801    tensors: &[TensorEntry],
1802    by_name: &HashMap<&str, &TensorEntry>,
1803) -> Result<(), CmfError> {
1804    let id = &s.id;
1805    let info = s
1806        .lookup
1807        .as_ref()
1808        .ok_or_else(|| perr(format!("skill '{id}': lookup record has no `lookup`")))?;
1809    if s.experts.is_some() {
1810        return Err(perr(format!(
1811            "skill '{id}': `experts` belongs to an expert_append record, not to lookup"
1812        )));
1813    }
1814    if !s.overrides.is_empty() {
1815        return Err(perr(format!(
1816            "skill '{id}': a lookup record replaces nothing — `overrides` must be empty"
1817        )));
1818    }
1819    if !s.layers.is_empty() {
1820        return Err(perr(format!(
1821            "skill '{id}': a lookup record touches no layer — `layers` must be empty (is {:?})",
1822            s.layers
1823        )));
1824    }
1825    if info.key_norm == KEY_NORM_V1 {
1826        return Err(perr(format!(
1827            "skill '{id}': lookup.key_norm '{KEY_NORM_V1}' is the pre-release rule — rebuild \
1828             the table with `cortiq lookup-build` (this reader knows {KEY_NORM})"
1829        )));
1830    }
1831    if info.key_norm != KEY_NORM {
1832        return Err(perr(format!(
1833            "skill '{id}': lookup.key_norm '{}' (this reader knows {KEY_NORM})",
1834            info.key_norm
1835        )));
1836    }
1837    // `lookup.policy` is NOT checked here — this rule set is also what
1838    // `open()` applies, and a value this reader does not know (a newer
1839    // writer's policy) must not make the whole genome unreadable: the
1840    // runtime reads it as `router_and_key` and `open()` warns
1841    // ([`unknown_lookup_policies`]); the writers refuse it
1842    // ([`check_lookup_policies`]).
1843    if info.entries == 0 {
1844        return Err(perr(format!("skill '{id}': lookup.entries must be ≥ 1")));
1845    }
1846    if info.keys == 0 {
1847        return Err(perr(format!("skill '{id}': lookup.keys must be ≥ 1")));
1848    }
1849    if info.langs.is_empty() {
1850        return Err(perr(format!("skill '{id}': lookup.langs is empty")));
1851    }
1852    for (what, list) in [("langs", &info.langs), ("fields", &info.fields)] {
1853        let mut seen = BTreeSet::new();
1854        for x in list {
1855            if x.is_empty() {
1856                return Err(perr(format!(
1857                    "skill '{id}': lookup.{what} has an empty name"
1858                )));
1859            }
1860            if !seen.insert(x.as_str()) {
1861                return Err(perr(format!(
1862                    "skill '{id}': duplicate '{x}' in lookup.{what}"
1863                )));
1864            }
1865        }
1866    }
1867    let n_off = info
1868        .slots()
1869        .and_then(|n| n.checked_add(1))
1870        .ok_or_else(|| perr(format!("skill '{id}': lookup.entries × langs overflows")))?;
1871    let prefix = format!("skill.{id}.");
1872    for t in tensors.iter().filter(|t| t.name.starts_with(&prefix)) {
1873        if !lookup_leaf::ALL.contains(&&t.name[prefix.len()..]) {
1874            return Err(perr(format!(
1875                "skill '{id}': tensor '{}' is not part of a lookup record (expected exactly \
1876                 {prefix}{{{}}})",
1877                t.name,
1878                lookup_leaf::ALL.join(", ")
1879            )));
1880        }
1881    }
1882    let want = [
1883        (lookup_leaf::KEYS_HASH, TensorDtype::U64, Some((info.keys, "lookup.keys"))),
1884        (lookup_leaf::KEYS_ENTRY, TensorDtype::U32, Some((info.keys, "lookup.keys"))),
1885        (
1886            lookup_leaf::ENTRIES_OFF,
1887            TensorDtype::U64,
1888            Some((n_off, "lookup.entries × langs + 1")),
1889        ),
1890        (lookup_leaf::TEXT, TensorDtype::U8, None),
1891    ];
1892    for (leaf, dtype, len) in want {
1893        let name = lookup_tensor_name(id, leaf);
1894        let Some(t) = by_name.get(name.as_str()) else {
1895            return Err(perr(format!("skill '{id}': missing tensor '{name}'")));
1896        };
1897        if t.dtype != dtype {
1898            return Err(perr(format!(
1899                "skill '{id}': '{name}' must be {} (is {})",
1900                dtype.name(),
1901                t.dtype.name()
1902            )));
1903        }
1904        if t.shape.len() != 1 {
1905            return Err(perr(format!(
1906                "skill '{id}': '{name}' must be 1-D (shape {:?})",
1907                t.shape
1908            )));
1909        }
1910        if let Some((n, why)) = len {
1911            if t.shape[0] != n {
1912                return Err(perr(format!(
1913                    "skill '{id}': '{name}' shape {:?} != [{n}] ({why})",
1914                    t.shape
1915                )));
1916            }
1917        }
1918    }
1919    let se = s
1920        .state_effect
1921        .as_ref()
1922        .ok_or_else(|| perr(format!("skill '{id}': v2 record has no `state_effect`")))?;
1923    let want = lookup_state_effect();
1924    if *se != want {
1925        return Err(perr(format!(
1926            "skill '{id}': state_effect {{first_affected_layer {}, switch '{}', \
1927             state_bytes_added {}}} — a lookup record never touches the network's state: \
1928             first_affected_layer 0, switch '{}', state_bytes_added 0",
1929            se.first_affected_layer, se.switch, se.state_bytes_added, want.switch
1930        )));
1931    }
1932    Ok(())
1933}
1934
1935/// `expert_append` (spec §9.5.1): exactly the tensors of
1936/// [`expert_append_layout`] with its shapes, f32 `desc.bias` / `desc.shell`,
1937/// no overrides, no per-request selection, the least permissive state
1938/// effect. Values (finite bias, finite shell) are checked by
1939/// [`validate_expert_append_values`] where the payload is at hand.
1940fn validate_expert_append(
1941    header: &CmfHeader,
1942    at: usize,
1943    s: &SkillRecord,
1944    tensors: &[TensorEntry],
1945    by_name: &HashMap<&str, &TensorEntry>,
1946) -> Result<(), CmfError> {
1947    let id = &s.id;
1948    if s.lookup.is_some() {
1949        return Err(perr(format!(
1950            "skill '{id}': `lookup` belongs to a lookup record, not to expert_append"
1951        )));
1952    }
1953    if !s.overrides.is_empty() {
1954        return Err(perr(format!(
1955            "skill '{id}': an expert_append record replaces nothing — `overrides` must be empty"
1956        )));
1957    }
1958    if s.selection.is_some() {
1959        return Err(perr(format!(
1960            "skill '{id}': an expert_append record carries no `selection` — its experts are \
1961             chosen per token by the layer's resonance router, never per request"
1962        )));
1963    }
1964    let retired = s.status.as_deref() == Some("retired");
1965    let plan = expert_append_layout_in(header, by_name, at, s, retired)?;
1966    let expected: HashMap<&str, &ExpertTensorSpec> =
1967        plan.iter().map(|p| (p.name.as_str(), p)).collect();
1968    let prefix = format!("skill.{id}.");
1969    for t in tensors.iter().filter(|t| t.name.starts_with(&prefix)) {
1970        let Some(p) = expected.get(t.name.as_str()) else {
1971            return Err(perr(format!(
1972                "skill '{id}': tensor '{}' is not part of the expert_append layout (layers {:?}, \
1973                 count {})",
1974                t.name,
1975                s.layers,
1976                s.experts.as_ref().map(|e| e.count).unwrap_or(0)
1977            )));
1978        };
1979        if t.shape != p.shape {
1980            return Err(perr(format!(
1981                "skill '{id}': '{}' shape {:?} != {:?} (the trunk's expert 0 of layer {})",
1982                t.name, t.shape, p.shape, p.layer
1983            )));
1984        }
1985        if matches!(p.leaf, expert_leaf::BIAS | expert_leaf::SHELL) && t.dtype != TensorDtype::F32
1986        {
1987            return Err(perr(format!(
1988                "skill '{id}': '{}' must be f32 (is {:?})",
1989                t.name, t.dtype
1990            )));
1991        }
1992    }
1993    for p in &plan {
1994        if !by_name.contains_key(p.name.as_str()) {
1995            return Err(perr(format!(
1996                "skill '{id}': missing tensor '{}' (layer {}, expert {})",
1997                p.name, p.layer, p.expert
1998            )));
1999        }
2000    }
2001    let se = s
2002        .state_effect
2003        .as_ref()
2004        .ok_or_else(|| perr(format!("skill '{id}': v2 record has no `state_effect`")))?;
2005    let computed = expert_append_state_effect(&s.layers);
2006    if se.first_affected_layer != computed.first_affected_layer {
2007        return Err(perr(format!(
2008            "skill '{id}': state_effect.first_affected_layer {} != min(layers) {}",
2009            se.first_affected_layer, computed.first_affected_layer
2010        )));
2011    }
2012    if se.switch != computed.switch {
2013        return Err(perr(format!(
2014            "skill '{id}': state_effect.switch '{}' — grown experts are part of the organism, \
2015             the record's switch is '{}'",
2016            se.switch, computed.switch
2017        )));
2018    }
2019    if se.state_bytes_added != 0 {
2020        return Err(perr(format!(
2021            "skill '{id}': state_effect.state_bytes_added {} — an expert_append record adds \
2022             no per-sequence state",
2023            se.state_bytes_added
2024        )));
2025    }
2026    Ok(())
2027}
2028
2029/// `ffn_replace`: tensors ⇔ overrides, same shape as the trunk entry.
2030fn validate_ffn_replace(
2031    header: &CmfHeader,
2032    s: &SkillRecord,
2033    tensors: &[TensorEntry],
2034    by_name: &HashMap<&str, &TensorEntry>,
2035) -> Result<(), CmfError> {
2036    let id = &s.id;
2037    let g = header.genome.as_ref().expect("checked by the caller");
2038    if s.experts.is_some() {
2039        return Err(perr(format!(
2040            "skill '{id}': `experts` belongs to an expert_append record, not to ffn_replace"
2041        )));
2042    }
2043    if s.lookup.is_some() {
2044        return Err(perr(format!(
2045            "skill '{id}': `lookup` belongs to a lookup record, not to ffn_replace"
2046        )));
2047    }
2048    let prefix = format!("skill.{id}.");
2049    let mut seen = BTreeSet::new();
2050    for o in &s.overrides {
2051        if !seen.insert(o.name.as_str()) {
2052            return Err(perr(format!(
2053                "skill '{id}': duplicate override '{}'",
2054                o.name
2055            )));
2056        }
2057    }
2058    let mut layers_named = BTreeSet::new();
2059    let mut n_tensors = 0usize;
2060    for t in tensors.iter().filter(|t| t.name.starts_with(&prefix)) {
2061        n_tensors += 1;
2062        let x = &t.name[prefix.len()..];
2063        let Some(o) = s.overrides.iter().find(|o| o.name == x) else {
2064            return Err(perr(format!(
2065                "skill '{id}': tensor '{}' has no override entry",
2066                t.name
2067            )));
2068        };
2069        let li =
2070            layer_of(x).filter(|&l| x[format!("model.layers.{l}.").len()..].starts_with("mlp."));
2071        let Some(li) = li else {
2072            return Err(perr(format!(
2073                "skill '{id}': ffn_replace tensor '{}' is not an FFN tensor (model.layers.N.mlp.*)",
2074                t.name
2075            )));
2076        };
2077        if li >= header.arch.num_layers {
2078            return Err(perr(format!(
2079                "skill '{id}': '{}' names layer {li} of {}",
2080                t.name, header.arch.num_layers
2081            )));
2082        }
2083        layers_named.insert(li);
2084        let Some(base) = by_name.get(x).copied().filter(|b| is_trunk_tensor(&b.name)) else {
2085            return Err(perr(format!(
2086                "skill '{id}': override '{x}' replaces no trunk tensor"
2087            )));
2088        };
2089        if base.shape != t.shape {
2090            return Err(perr(format!(
2091                "skill '{id}': '{}' shape {:?} != trunk '{x}' shape {:?}",
2092                t.name, t.shape, base.shape
2093            )));
2094        }
2095        let bh = parse_hex64(&o.base_hash).ok_or_else(|| {
2096            perr(format!(
2097                "skill '{id}': override '{x}' base_hash '{}' is not a hex hash64",
2098                o.base_hash
2099            ))
2100        })?;
2101        if g.encoding == "f32" && bh != base.hash {
2102            return Err(perr(format!(
2103                "skill '{id}': override '{x}' base_hash {} != trunk entry hash {} \
2104                 (the skill was trained against other trunk bytes)",
2105                o.base_hash,
2106                hex64(base.hash)
2107            )));
2108        }
2109    }
2110    for o in &s.overrides {
2111        if by_name
2112            .get(format!("{prefix}{}", o.name).as_str())
2113            .is_none()
2114        {
2115            return Err(perr(format!(
2116                "skill '{id}': override '{}' has no tensor '{prefix}{}'",
2117                o.name, o.name
2118            )));
2119        }
2120    }
2121    if n_tensors == 0 {
2122        return Err(perr(format!(
2123            "skill '{id}': ffn_replace record carries no tensors"
2124        )));
2125    }
2126    let declared: BTreeSet<usize> = s.layers.iter().copied().collect();
2127    if declared != layers_named || declared.len() != s.layers.len() {
2128        return Err(perr(format!(
2129            "skill '{id}': layers {:?} != layers named by its tensors {:?}",
2130            s.layers, layers_named
2131        )));
2132    }
2133    let se = s
2134        .state_effect
2135        .as_ref()
2136        .ok_or_else(|| perr(format!("skill '{id}': v2 record has no `state_effect`")))?;
2137    let first = *layers_named.iter().next().unwrap();
2138    if se.first_affected_layer != first {
2139        return Err(perr(format!(
2140            "skill '{id}': state_effect.first_affected_layer {} != min(layers) {first}",
2141            se.first_affected_layer
2142        )));
2143    }
2144    let rank = |sw: &str| state_switch::ALL.iter().position(|v| *v == sw);
2145    let Some(claimed) = rank(&se.switch) else {
2146        return Err(perr(format!(
2147            "skill '{id}': state_effect.switch '{}' is not one of {:?}",
2148            se.switch,
2149            state_switch::ALL
2150        )));
2151    };
2152    let computed = ffn_replace_state_effect(&header.arch, &s.layers);
2153    if claimed < rank(&computed.switch).unwrap() {
2154        return Err(perr(format!(
2155            "skill '{id}': state_effect.switch '{}' is more permissive than the layer schedule \
2156             allows ('{}')",
2157            se.switch, computed.switch
2158        )));
2159    }
2160    Ok(())
2161}
2162
2163fn validate_router(header: &CmfHeader, r: &RouterPolicy) -> Result<(), CmfError> {
2164    if r.version != 2 {
2165        return Err(perr(format!(
2166            "router.version {} (this reader knows 2)",
2167            r.version
2168        )));
2169    }
2170    if r.policy != "backbone_gated" {
2171        return Err(perr(format!(
2172            "router.policy '{}' (known: backbone_gated)",
2173            r.policy
2174        )));
2175    }
2176    if r.granularity != "request" {
2177        return Err(perr(format!(
2178            "router.granularity '{}' (known: request)",
2179            r.granularity
2180        )));
2181    }
2182    if r.phi.pool != "span_mean" || r.phi.norm != "unit" {
2183        return Err(perr(format!(
2184            "router.phi pool/norm '{}'/'{}' (known: span_mean/unit)",
2185            r.phi.pool, r.phi.norm
2186        )));
2187    }
2188    if r.phi.layer >= header.arch.num_layers {
2189        return Err(perr(format!(
2190            "router.phi.layer {} of {} layers",
2191            r.phi.layer, header.arch.num_layers
2192        )));
2193    }
2194    if !(r.margin.is_finite() && r.margin >= 0.0) {
2195        return Err(perr(format!(
2196            "router.margin {} must be finite and ≥ 0",
2197            r.margin
2198        )));
2199    }
2200    if parse_hex64(&r.skills_hash).is_none() {
2201        return Err(perr(format!(
2202            "router.skills_hash '{}' is not a hex hash64",
2203            r.skills_hash
2204        )));
2205    }
2206    let check_desc = |who: &str, d: &SelectionDescriptor| -> Result<(), CmfError> {
2207        if d.metric != METRIC_MSE_UNIT {
2208            return Err(perr(format!(
2209                "{who}: selection.metric '{}' (router v2 requires {METRIC_MSE_UNIT})",
2210                d.metric
2211            )));
2212        }
2213        if d.phi_layer != r.phi.layer {
2214            return Err(perr(format!(
2215                "{who}: selection.phi_layer {} != router.phi.layer {}",
2216                d.phi_layer, r.phi.layer
2217            )));
2218        }
2219        Ok(())
2220    };
2221    check_desc("router.base", &r.base)?;
2222    for s in header.skills.iter().filter(|s| s.is_v2()) {
2223        if let Some(sel) = &s.selection {
2224            check_desc(&format!("skill '{}'", s.id), sel)?;
2225            if let Some(&lmin) = s.layers.iter().min() {
2226                if r.phi.layer >= lmin {
2227                    return Err(perr(format!(
2228                        "skill '{}': router.phi.layer {} must be < min(layers) {lmin} \
2229                         (φ must not depend on the active skill)",
2230                        s.id, r.phi.layer
2231                    )));
2232                }
2233            }
2234        }
2235    }
2236    Ok(())
2237}
2238
2239// ───────────────────────── time ─────────────────────────
2240
2241/// Current UTC time as RFC 3339 (`YYYY-MM-DDTHH:MM:SSZ`), no dependency.
2242pub fn utc_now_rfc3339() -> String {
2243    let secs = std::time::SystemTime::now()
2244        .duration_since(std::time::UNIX_EPOCH)
2245        .map(|d| d.as_secs() as i64)
2246        .unwrap_or(0);
2247    let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));
2248    // civil-from-days (H. Hinnant)
2249    let z = days + 719_468;
2250    let era = z.div_euclid(146_097);
2251    let doe = z - era * 146_097;
2252    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
2253    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2254    let mp = (5 * doy + 2) / 153;
2255    let d = doy - (153 * mp + 2) / 5 + 1;
2256    let m = if mp < 10 { mp + 3 } else { mp - 9 };
2257    let y = yoe + era * 400 + i64::from(m <= 2);
2258    format!(
2259        "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
2260        sod / 3600,
2261        (sod / 60) % 60,
2262        sod % 60
2263    )
2264}