From 7feb782366551c3576435ba0f969c8a00046d36e Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:03:41 +0300 Subject: [PATCH 01/26] test: require generated UK surface inventory --- .../tests/uk_surface_audit_projection.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 crates/xtask/tests/uk_surface_audit_projection.rs diff --git a/crates/xtask/tests/uk_surface_audit_projection.rs b/crates/xtask/tests/uk_surface_audit_projection.rs new file mode 100644 index 00000000..7297afa3 --- /dev/null +++ b/crates/xtask/tests/uk_surface_audit_projection.rs @@ -0,0 +1,22 @@ +use std::process::Command; + +#[test] +fn generated_uk_surface_inventory_is_current() { + let script = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../scripts/generate-uk-surface-audit.py" + ); + + let output = Command::new("python3") + .arg(script) + .arg("--check") + .output() + .expect("python3 must launch the UK surface audit generator"); + + assert!( + output.status.success(), + "UK surface audit projection must be current\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} From 7b0b36019a1d0d00da198207da13c253f41236a0 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:18:45 +0300 Subject: [PATCH 02/26] test(uk): require language-owned surface audit generator --- .../tests/uk_surface_audit_projection.rs | 29 +++++++++++++++++++ .../tests/uk_surface_audit_projection.rs | 22 -------------- 2 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 crates/my-lisp-cli/tests/uk_surface_audit_projection.rs delete mode 100644 crates/xtask/tests/uk_surface_audit_projection.rs diff --git a/crates/my-lisp-cli/tests/uk_surface_audit_projection.rs b/crates/my-lisp-cli/tests/uk_surface_audit_projection.rs new file mode 100644 index 00000000..873fb4bd --- /dev/null +++ b/crates/my-lisp-cli/tests/uk_surface_audit_projection.rs @@ -0,0 +1,29 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn my_lisp(cwd: &Path) -> Command { + let mut command = Command::new(env!("CARGO_BIN_EXE_my-lisp")); + command.current_dir(cwd); + command +} + +#[test] +fn uk_surface_audit_generator_runs_through_real_my_lisp_cli() { + let root = repo_root(); + let script = root.join("scripts/generate-uk-surface-audit.my"); + let output = my_lisp(&root) + .arg(&script) + .output() + .expect("UK surface audit generator should run through the real my-lisp CLI"); + + assert!( + output.status.success(), + "language-owned UK surface audit generator must run successfully\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} diff --git a/crates/xtask/tests/uk_surface_audit_projection.rs b/crates/xtask/tests/uk_surface_audit_projection.rs deleted file mode 100644 index 7297afa3..00000000 --- a/crates/xtask/tests/uk_surface_audit_projection.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::process::Command; - -#[test] -fn generated_uk_surface_inventory_is_current() { - let script = concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../scripts/generate-uk-surface-audit.py" - ); - - let output = Command::new("python3") - .arg(script) - .arg("--check") - .output() - .expect("python3 must launch the UK surface audit generator"); - - assert!( - output.status.success(), - "UK surface audit projection must be current\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} From ac36c3c591cb1ecf10a12f6c4b2e383a956a8b59 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:24:19 +0300 Subject: [PATCH 03/26] test(canon): require surface-independent callable identity --- crates/my-lisp/tests/first_class_builtins.rs | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/my-lisp/tests/first_class_builtins.rs b/crates/my-lisp/tests/first_class_builtins.rs index 5244a333..8e425171 100644 --- a/crates/my-lisp/tests/first_class_builtins.rs +++ b/crates/my-lisp/tests/first_class_builtins.rs @@ -87,3 +87,40 @@ fn lexical_shadowing_of_builtin_name() { "6" ); } + +#[test] +fn canon_peer_surfaces_observe_one_semantic_callable_identity() { + use my_lisp::semantic_registry_export::admitted_surfaces_for_semantic_id; + + let car_surfaces = admitted_surfaces_for_semantic_id("0005"); + assert!( + car_surfaces.len() >= 2, + "0005 must admit multiple surfaces for the identity witness" + ); + + for left in &car_surfaces { + for right in &car_surfaces { + let source = format!("(eq {} {})", left.name, right.name); + assert_eq!( + eval_source(&source), + "t", + "peer surfaces {}:{} and {}:{} share semantic ID 0005 and must be eq by semantic identity", + left.namespace, + left.name, + right.namespace, + right.name + ); + } + } + + let cdr_surface = admitted_surfaces_for_semantic_id("0006") + .into_iter() + .next() + .expect("0006 must admit at least one surface"); + let source = format!("(eq {} {})", car_surfaces[0].name, cdr_surface.name); + assert_eq!( + eval_source(&source), + "()", + "different semantic IDs 0005 and 0006 must remain observably distinct" + ); +} From 28d4956b84d824877732d388381008d50ee2ae37 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:35:19 +0300 Subject: [PATCH 04/26] feat(canon): resolve callable implementation from semantic identity --- crates/my-lisp/src/eval/canon.rs | 191 ++++++++++++++++++------------- 1 file changed, 114 insertions(+), 77 deletions(-) diff --git a/crates/my-lisp/src/eval/canon.rs b/crates/my-lisp/src/eval/canon.rs index 79ba0567..8aa6008c 100644 --- a/crates/my-lisp/src/eval/canon.rs +++ b/crates/my-lisp/src/eval/canon.rs @@ -7,7 +7,7 @@ use super::special_forms::{car_value, cdr_value, cons_values, eq_values}; use crate::{semantic_registry, Environment, ErrorKind, LanguageError, Span, Value}; -use std::{collections::HashMap, rc::Rc}; +use std::rc::Rc; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) enum CanonicalIdentity { @@ -95,6 +95,13 @@ fn identity_for_semantic_id(semantic_id: &str) -> Option { .map(|entry| entry.identity) } +fn semantic_id_for_identity(identity: CanonicalIdentity) -> Option<&'static str> { + CANON + .iter() + .find(|entry| entry.identity == identity) + .and_then(|entry| entry.semantic_id) +} + pub(crate) fn identity_for_surface(name: &str) -> Option { semantic_registry::semantic_id_for_surface(name).and_then(identity_for_semantic_id) } @@ -152,74 +159,98 @@ fn exact_args( )) } -fn builtin( - identity: &'static str, - func: impl Fn(&[Value], &Environment, Span) -> Result + 'static, -) -> Value { - Value::Builtin(Rc::new(crate::value::Builtin { - name: identity, - func: Rc::new(func), - })) -} - -fn materialize_value(identity: CanonicalIdentity) -> Option { - match identity { - CanonicalIdentity::EmptyList => ground_value(identity), - CanonicalIdentity::Atom => Some(builtin("PRIM_ATOM", |args, _env, span| { +/// Resolve an opaque Canon semantic ID to its current host implementation. +/// The ID is the call-target identity; the Rust closure is only a projection. +fn invoke_semantic_callable( + semantic_id: &'static str, + args: &[Value], + environment: &Environment, + span: Span, +) -> Result { + match identity_for_semantic_id(semantic_id) { + Some(CanonicalIdentity::Atom) => { exact_args("PRIM_ATOM", args, 1, span)?; Ok(Value::truth(args[0].is_atom())) - })), - CanonicalIdentity::Eq => Some(builtin("PRIM_EQ", |args, _env, span| { + } + Some(CanonicalIdentity::Eq) => { exact_args("PRIM_EQ", args, 2, span)?; eq_values(args[0].clone(), args[1].clone(), span) - })), - CanonicalIdentity::Cons => Some(builtin("PRIM_CONS", |args, env, span| { + } + Some(CanonicalIdentity::Cons) => { exact_args("PRIM_CONS", args, 2, span)?; - cons_values(args[0].clone(), args[1].clone(), env, span) - })), - CanonicalIdentity::Car => Some(builtin("PRIM_CAR", |args, _env, span| { + cons_values(args[0].clone(), args[1].clone(), environment, span) + } + Some(CanonicalIdentity::Car) => { exact_args("PRIM_CAR", args, 1, span)?; car_value(&args[0], span) - })), - CanonicalIdentity::Cdr => Some(builtin("PRIM_CDR", |args, _env, span| { + } + Some(CanonicalIdentity::Cdr) => { exact_args("PRIM_CDR", args, 1, span)?; cdr_value(&args[0], span) - })), - CanonicalIdentity::Quote | CanonicalIdentity::Cond => None, + } + Some(CanonicalIdentity::EmptyList | CanonicalIdentity::Quote | CanonicalIdentity::Cond) + | None => Err(LanguageError::new( + ErrorKind::Type, + format!( + "semantic identity is not a callable Canon value · семантична тотожність не є викличним значенням Канону · semantische Identität ist kein aufrufbarer Kanon-Wert: {semantic_id}" + ), + span, + )), } } -fn build_value_registry() -> HashMap { - [ - CanonicalIdentity::Atom, - CanonicalIdentity::Eq, - CanonicalIdentity::Cons, - CanonicalIdentity::Car, - CanonicalIdentity::Cdr, - ] - .into_iter() - .map(|identity| { - ( - identity, - materialize_value(identity).expect("callable Canon identity must materialize"), - ) - }) - .collect() +/// Materialize a host implementation projection for a semantic call target. +/// A fresh Rc is intentional: allocation identity must not define language +/// identity. `eq` observes only the opaque semantic ID carried in `name`. +fn semantic_callable(semantic_id: &'static str) -> Value { + Value::Builtin(Rc::new(crate::value::Builtin { + name: semantic_id, + func: Rc::new(move |args, environment, span| { + invoke_semantic_callable(semantic_id, args, environment, span) + }), + })) } -thread_local! { - /// One immutable callable handle per Canon identity per evaluator thread. - /// Every stable registry spelling resolves to clones of these same `Rc` handles. - static CANON_VALUES: HashMap = build_value_registry(); +fn semantic_callable_id(value: &Value) -> Option<&'static str> { + let Value::Builtin(builtin) = value else { + return None; + }; + let semantic_id = builtin.name; + let identity = identity_for_semantic_id(semantic_id)?; + matches!( + identity, + CanonicalIdentity::Atom + | CanonicalIdentity::Eq + | CanonicalIdentity::Cons + | CanonicalIdentity::Car + | CanonicalIdentity::Cdr + ) + .then_some(semantic_id) } -/// Return the stable first-class value for a canonical identity. Special forms +/// When at least one side is a Canon semantic callable, answer equality using +/// only the opaque semantic ID. `None` delegates ordinary non-Canon values to +/// their existing equality semantics. +pub(crate) fn same_semantic_callable_identity(left: &Value, right: &Value) -> Option { + match (semantic_callable_id(left), semantic_callable_id(right)) { + (Some(left), Some(right)) => Some(left == right), + (Some(_), None) | (None, Some(_)) => Some(false), + (None, None) => None, + } +} + +/// Return a first-class host projection for a canonical identity. Special forms /// deliberately have no value representation; they remain syntax-only. pub(crate) fn value(identity: CanonicalIdentity) -> Option { - if identity == CanonicalIdentity::EmptyList { - return Some(Value::Nil); + match identity { + CanonicalIdentity::EmptyList => Some(Value::Nil), + CanonicalIdentity::Quote | CanonicalIdentity::Cond => None, + CanonicalIdentity::Atom + | CanonicalIdentity::Eq + | CanonicalIdentity::Cons + | CanonicalIdentity::Car + | CanonicalIdentity::Cdr => semantic_id_for_identity(identity).map(semantic_callable), } - CANON_VALUES.with(|values| values.get(&identity).cloned()) } pub(crate) fn value_for_surface(name: &str) -> Option { @@ -273,7 +304,7 @@ mod tests { } #[test] - fn numeric_canon_identity_uses_the_same_evaluator_meaning() { + fn numeric_canon_identity_uses_the_same_semantic_call_target() { assert_eq!( identity_for_surface(CAR_SEMANTIC_ID), Some(CanonicalIdentity::Car) @@ -285,23 +316,19 @@ mod tests { let numeric = value_for_surface(CAR_SEMANTIC_ID).expect("numeric Canon identity"); let human = value_for_surface(human_surface).expect("registry-admitted Canon surface"); let (Value::Builtin(numeric), Value::Builtin(human)) = (&numeric, &human) else { - panic!("PRIM_CAR must be a first-class builtin value"); + panic!("0005 must materialize as a first-class host projection"); }; - assert!(Rc::ptr_eq(numeric, human)); + assert_eq!(numeric.name, CAR_SEMANTIC_ID); + assert_eq!(human.name, CAR_SEMANTIC_ID); + assert!( + !Rc::ptr_eq(numeric, human), + "semantic identity witness must not depend on sharing one Rust allocation" + ); + assert_eq!(same_semantic_callable_identity(&numeric.into(), &human.into()), Some(true)); } #[test] - fn every_admitted_surface_for_one_semantic_id_shares_one_stable_handle() { - // Which spellings mean "car" (en/uk/sa/...) is a semantic-registry - // FACT, owned by the registry data, not Rust knowledge -- this test - // asserts only the Rust-implementation INVARIANT: whatever surfaces - // the registry admits for one semantic identity, Canon materializes - // exactly one shared callable handle for all of them. Read the real - // admitted surfaces from the registry itself instead of hardcoding - // "car"/"перше"/"ādi" as literals, so this test still passes - // unchanged if the registry's admitted spellings for 0005 ever - // change, and still fails if Canon ever gives two of them distinct - // handles. + fn every_admitted_surface_shares_identity_without_sharing_handle() { let surfaces = semantic_registry::admitted_surfaces_for_semantic_id(CAR_SEMANTIC_ID); assert!( surfaces.len() >= 2, @@ -309,25 +336,35 @@ mod tests { got {surfaces:?}" ); - let handles: Vec> = surfaces + let values = surfaces .iter() .map(|surface| { - let value = value_for_surface(surface) - .unwrap_or_else(|| panic!("registry-admitted surface {surface:?} should route through Canon")); - let Value::Builtin(ref handle) = value else { - panic!("PRIM_CAR must be a first-class builtin value for surface {surface:?}"); - }; - handle.clone() + value_for_surface(surface).unwrap_or_else(|| { + panic!("registry-admitted surface {surface:?} should route through Canon") + }) }) - .collect(); + .collect::>(); - let first = &handles[0]; - for (surface, handle) in surfaces.iter().zip(handles.iter()) { - assert!( - Rc::ptr_eq(first, handle), - "surface {surface:?} did not share Canon's one stable handle for 0005" + for (surface, value) in surfaces.iter().zip(values.iter()) { + assert_eq!( + semantic_callable_id(value), + Some(CAR_SEMANTIC_ID), + "surface {surface:?} did not carry Canon semantic identity 0005" + ); + assert_eq!( + same_semantic_callable_identity(&values[0], value), + Some(true), + "surface {surface:?} did not share semantic callable identity 0005" ); } + + let (Value::Builtin(first), Value::Builtin(second)) = (&values[0], &values[1]) else { + panic!("0005 surfaces must materialize as first-class host projections"); + }; + assert!( + !Rc::ptr_eq(first, second), + "the witness must prove semantic identity survives distinct Rust allocations" + ); } #[test] From 81316af6d4ca5f804347f48e383bfe439e7f2088 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:35:48 +0300 Subject: [PATCH 05/26] feat(canon): make eq observe semantic callable identity --- crates/my-lisp/src/eval/special_forms/core.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/my-lisp/src/eval/special_forms/core.rs b/crates/my-lisp/src/eval/special_forms/core.rs index 8c7d40a3..4b9f5f98 100644 --- a/crates/my-lisp/src/eval/special_forms/core.rs +++ b/crates/my-lisp/src/eval/special_forms/core.rs @@ -161,5 +161,8 @@ pub(crate) fn eq_values(left: Value, right: Value, span: Span) -> Result Date: Sun, 13 Sep 2026 01:36:46 +0300 Subject: [PATCH 06/26] test(canon): prove identity across distinct host projections --- crates/my-lisp/src/eval/canon.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/my-lisp/src/eval/canon.rs b/crates/my-lisp/src/eval/canon.rs index 8aa6008c..d0aafb0e 100644 --- a/crates/my-lisp/src/eval/canon.rs +++ b/crates/my-lisp/src/eval/canon.rs @@ -284,10 +284,6 @@ mod tests { #[test] fn every_admitted_surface_for_one_semantic_id_resolves_to_one_identity() { - // Which spellings mean "car" is a registry FACT, not a Rust literal - // to enumerate here -- read them from the registry so this test - // keeps meaning "Canon routes every admitted surface for 0005 to - // the same identity" even if the admitted spellings change. let surfaces = semantic_registry::admitted_surfaces_for_semantic_id(CAR_SEMANTIC_ID); assert!( surfaces.len() >= 2, @@ -313,9 +309,14 @@ mod tests { .into_iter() .next() .expect("0005 (car) should admit at least one human surface"); - let numeric = value_for_surface(CAR_SEMANTIC_ID).expect("numeric Canon identity"); - let human = value_for_surface(human_surface).expect("registry-admitted Canon surface"); - let (Value::Builtin(numeric), Value::Builtin(human)) = (&numeric, &human) else { + let numeric_value = value_for_surface(CAR_SEMANTIC_ID).expect("numeric Canon identity"); + let human_value = + value_for_surface(human_surface).expect("registry-admitted Canon surface"); + assert_eq!( + same_semantic_callable_identity(&numeric_value, &human_value), + Some(true) + ); + let (Value::Builtin(numeric), Value::Builtin(human)) = (&numeric_value, &human_value) else { panic!("0005 must materialize as a first-class host projection"); }; assert_eq!(numeric.name, CAR_SEMANTIC_ID); @@ -324,7 +325,6 @@ mod tests { !Rc::ptr_eq(numeric, human), "semantic identity witness must not depend on sharing one Rust allocation" ); - assert_eq!(same_semantic_callable_identity(&numeric.into(), &human.into()), Some(true)); } #[test] From 1eb060b1b4aabe4b4e6e83c91f7adb7cbe10ca97 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:46:52 +0300 Subject: [PATCH 07/26] test(canon): restore pure RED before semantic-ref implementation --- crates/my-lisp/src/eval/canon.rs | 205 +++++++----------- crates/my-lisp/src/eval/special_forms/core.rs | 3 - 2 files changed, 84 insertions(+), 124 deletions(-) diff --git a/crates/my-lisp/src/eval/canon.rs b/crates/my-lisp/src/eval/canon.rs index d0aafb0e..79ba0567 100644 --- a/crates/my-lisp/src/eval/canon.rs +++ b/crates/my-lisp/src/eval/canon.rs @@ -7,7 +7,7 @@ use super::special_forms::{car_value, cdr_value, cons_values, eq_values}; use crate::{semantic_registry, Environment, ErrorKind, LanguageError, Span, Value}; -use std::rc::Rc; +use std::{collections::HashMap, rc::Rc}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(crate) enum CanonicalIdentity { @@ -95,13 +95,6 @@ fn identity_for_semantic_id(semantic_id: &str) -> Option { .map(|entry| entry.identity) } -fn semantic_id_for_identity(identity: CanonicalIdentity) -> Option<&'static str> { - CANON - .iter() - .find(|entry| entry.identity == identity) - .and_then(|entry| entry.semantic_id) -} - pub(crate) fn identity_for_surface(name: &str) -> Option { semantic_registry::semantic_id_for_surface(name).and_then(identity_for_semantic_id) } @@ -159,98 +152,74 @@ fn exact_args( )) } -/// Resolve an opaque Canon semantic ID to its current host implementation. -/// The ID is the call-target identity; the Rust closure is only a projection. -fn invoke_semantic_callable( - semantic_id: &'static str, - args: &[Value], - environment: &Environment, - span: Span, -) -> Result { - match identity_for_semantic_id(semantic_id) { - Some(CanonicalIdentity::Atom) => { +fn builtin( + identity: &'static str, + func: impl Fn(&[Value], &Environment, Span) -> Result + 'static, +) -> Value { + Value::Builtin(Rc::new(crate::value::Builtin { + name: identity, + func: Rc::new(func), + })) +} + +fn materialize_value(identity: CanonicalIdentity) -> Option { + match identity { + CanonicalIdentity::EmptyList => ground_value(identity), + CanonicalIdentity::Atom => Some(builtin("PRIM_ATOM", |args, _env, span| { exact_args("PRIM_ATOM", args, 1, span)?; Ok(Value::truth(args[0].is_atom())) - } - Some(CanonicalIdentity::Eq) => { + })), + CanonicalIdentity::Eq => Some(builtin("PRIM_EQ", |args, _env, span| { exact_args("PRIM_EQ", args, 2, span)?; eq_values(args[0].clone(), args[1].clone(), span) - } - Some(CanonicalIdentity::Cons) => { + })), + CanonicalIdentity::Cons => Some(builtin("PRIM_CONS", |args, env, span| { exact_args("PRIM_CONS", args, 2, span)?; - cons_values(args[0].clone(), args[1].clone(), environment, span) - } - Some(CanonicalIdentity::Car) => { + cons_values(args[0].clone(), args[1].clone(), env, span) + })), + CanonicalIdentity::Car => Some(builtin("PRIM_CAR", |args, _env, span| { exact_args("PRIM_CAR", args, 1, span)?; car_value(&args[0], span) - } - Some(CanonicalIdentity::Cdr) => { + })), + CanonicalIdentity::Cdr => Some(builtin("PRIM_CDR", |args, _env, span| { exact_args("PRIM_CDR", args, 1, span)?; cdr_value(&args[0], span) - } - Some(CanonicalIdentity::EmptyList | CanonicalIdentity::Quote | CanonicalIdentity::Cond) - | None => Err(LanguageError::new( - ErrorKind::Type, - format!( - "semantic identity is not a callable Canon value · семантична тотожність не є викличним значенням Канону · semantische Identität ist kein aufrufbarer Kanon-Wert: {semantic_id}" - ), - span, - )), + })), + CanonicalIdentity::Quote | CanonicalIdentity::Cond => None, } } -/// Materialize a host implementation projection for a semantic call target. -/// A fresh Rc is intentional: allocation identity must not define language -/// identity. `eq` observes only the opaque semantic ID carried in `name`. -fn semantic_callable(semantic_id: &'static str) -> Value { - Value::Builtin(Rc::new(crate::value::Builtin { - name: semantic_id, - func: Rc::new(move |args, environment, span| { - invoke_semantic_callable(semantic_id, args, environment, span) - }), - })) -} - -fn semantic_callable_id(value: &Value) -> Option<&'static str> { - let Value::Builtin(builtin) = value else { - return None; - }; - let semantic_id = builtin.name; - let identity = identity_for_semantic_id(semantic_id)?; - matches!( - identity, - CanonicalIdentity::Atom - | CanonicalIdentity::Eq - | CanonicalIdentity::Cons - | CanonicalIdentity::Car - | CanonicalIdentity::Cdr - ) - .then_some(semantic_id) +fn build_value_registry() -> HashMap { + [ + CanonicalIdentity::Atom, + CanonicalIdentity::Eq, + CanonicalIdentity::Cons, + CanonicalIdentity::Car, + CanonicalIdentity::Cdr, + ] + .into_iter() + .map(|identity| { + ( + identity, + materialize_value(identity).expect("callable Canon identity must materialize"), + ) + }) + .collect() } -/// When at least one side is a Canon semantic callable, answer equality using -/// only the opaque semantic ID. `None` delegates ordinary non-Canon values to -/// their existing equality semantics. -pub(crate) fn same_semantic_callable_identity(left: &Value, right: &Value) -> Option { - match (semantic_callable_id(left), semantic_callable_id(right)) { - (Some(left), Some(right)) => Some(left == right), - (Some(_), None) | (None, Some(_)) => Some(false), - (None, None) => None, - } +thread_local! { + /// One immutable callable handle per Canon identity per evaluator thread. + /// Every stable registry spelling resolves to clones of these same `Rc` handles. + static CANON_VALUES: HashMap = build_value_registry(); } -/// Return a first-class host projection for a canonical identity. Special forms +/// Return the stable first-class value for a canonical identity. Special forms /// deliberately have no value representation; they remain syntax-only. pub(crate) fn value(identity: CanonicalIdentity) -> Option { - match identity { - CanonicalIdentity::EmptyList => Some(Value::Nil), - CanonicalIdentity::Quote | CanonicalIdentity::Cond => None, - CanonicalIdentity::Atom - | CanonicalIdentity::Eq - | CanonicalIdentity::Cons - | CanonicalIdentity::Car - | CanonicalIdentity::Cdr => semantic_id_for_identity(identity).map(semantic_callable), + if identity == CanonicalIdentity::EmptyList { + return Some(Value::Nil); } + CANON_VALUES.with(|values| values.get(&identity).cloned()) } pub(crate) fn value_for_surface(name: &str) -> Option { @@ -284,6 +253,10 @@ mod tests { #[test] fn every_admitted_surface_for_one_semantic_id_resolves_to_one_identity() { + // Which spellings mean "car" is a registry FACT, not a Rust literal + // to enumerate here -- read them from the registry so this test + // keeps meaning "Canon routes every admitted surface for 0005 to + // the same identity" even if the admitted spellings change. let surfaces = semantic_registry::admitted_surfaces_for_semantic_id(CAR_SEMANTIC_ID); assert!( surfaces.len() >= 2, @@ -300,7 +273,7 @@ mod tests { } #[test] - fn numeric_canon_identity_uses_the_same_semantic_call_target() { + fn numeric_canon_identity_uses_the_same_evaluator_meaning() { assert_eq!( identity_for_surface(CAR_SEMANTIC_ID), Some(CanonicalIdentity::Car) @@ -309,26 +282,26 @@ mod tests { .into_iter() .next() .expect("0005 (car) should admit at least one human surface"); - let numeric_value = value_for_surface(CAR_SEMANTIC_ID).expect("numeric Canon identity"); - let human_value = - value_for_surface(human_surface).expect("registry-admitted Canon surface"); - assert_eq!( - same_semantic_callable_identity(&numeric_value, &human_value), - Some(true) - ); - let (Value::Builtin(numeric), Value::Builtin(human)) = (&numeric_value, &human_value) else { - panic!("0005 must materialize as a first-class host projection"); + let numeric = value_for_surface(CAR_SEMANTIC_ID).expect("numeric Canon identity"); + let human = value_for_surface(human_surface).expect("registry-admitted Canon surface"); + let (Value::Builtin(numeric), Value::Builtin(human)) = (&numeric, &human) else { + panic!("PRIM_CAR must be a first-class builtin value"); }; - assert_eq!(numeric.name, CAR_SEMANTIC_ID); - assert_eq!(human.name, CAR_SEMANTIC_ID); - assert!( - !Rc::ptr_eq(numeric, human), - "semantic identity witness must not depend on sharing one Rust allocation" - ); + assert!(Rc::ptr_eq(numeric, human)); } #[test] - fn every_admitted_surface_shares_identity_without_sharing_handle() { + fn every_admitted_surface_for_one_semantic_id_shares_one_stable_handle() { + // Which spellings mean "car" (en/uk/sa/...) is a semantic-registry + // FACT, owned by the registry data, not Rust knowledge -- this test + // asserts only the Rust-implementation INVARIANT: whatever surfaces + // the registry admits for one semantic identity, Canon materializes + // exactly one shared callable handle for all of them. Read the real + // admitted surfaces from the registry itself instead of hardcoding + // "car"/"перше"/"ādi" as literals, so this test still passes + // unchanged if the registry's admitted spellings for 0005 ever + // change, and still fails if Canon ever gives two of them distinct + // handles. let surfaces = semantic_registry::admitted_surfaces_for_semantic_id(CAR_SEMANTIC_ID); assert!( surfaces.len() >= 2, @@ -336,35 +309,25 @@ mod tests { got {surfaces:?}" ); - let values = surfaces + let handles: Vec> = surfaces .iter() .map(|surface| { - value_for_surface(surface).unwrap_or_else(|| { - panic!("registry-admitted surface {surface:?} should route through Canon") - }) + let value = value_for_surface(surface) + .unwrap_or_else(|| panic!("registry-admitted surface {surface:?} should route through Canon")); + let Value::Builtin(ref handle) = value else { + panic!("PRIM_CAR must be a first-class builtin value for surface {surface:?}"); + }; + handle.clone() }) - .collect::>(); + .collect(); - for (surface, value) in surfaces.iter().zip(values.iter()) { - assert_eq!( - semantic_callable_id(value), - Some(CAR_SEMANTIC_ID), - "surface {surface:?} did not carry Canon semantic identity 0005" - ); - assert_eq!( - same_semantic_callable_identity(&values[0], value), - Some(true), - "surface {surface:?} did not share semantic callable identity 0005" + let first = &handles[0]; + for (surface, handle) in surfaces.iter().zip(handles.iter()) { + assert!( + Rc::ptr_eq(first, handle), + "surface {surface:?} did not share Canon's one stable handle for 0005" ); } - - let (Value::Builtin(first), Value::Builtin(second)) = (&values[0], &values[1]) else { - panic!("0005 surfaces must materialize as first-class host projections"); - }; - assert!( - !Rc::ptr_eq(first, second), - "the witness must prove semantic identity survives distinct Rust allocations" - ); } #[test] diff --git a/crates/my-lisp/src/eval/special_forms/core.rs b/crates/my-lisp/src/eval/special_forms/core.rs index 4b9f5f98..8c7d40a3 100644 --- a/crates/my-lisp/src/eval/special_forms/core.rs +++ b/crates/my-lisp/src/eval/special_forms/core.rs @@ -161,8 +161,5 @@ pub(crate) fn eq_values(left: Value, right: Value, span: Span) -> Result Date: Sun, 13 Sep 2026 01:54:34 +0300 Subject: [PATCH 08/26] ci(canon): stage semantic-ref vertical slice gate --- .../patches/eco-canon-2-semantic-ref.patch | 316 ++++++++++++++++++ .../eco-canon-2-semantic-ref-once.yml | 62 ++++ 2 files changed, 378 insertions(+) create mode 100644 .github/patches/eco-canon-2-semantic-ref.patch create mode 100644 .github/workflows/eco-canon-2-semantic-ref-once.yml diff --git a/.github/patches/eco-canon-2-semantic-ref.patch b/.github/patches/eco-canon-2-semantic-ref.patch new file mode 100644 index 00000000..91e5bc37 --- /dev/null +++ b/.github/patches/eco-canon-2-semantic-ref.patch @@ -0,0 +1,316 @@ +diff --git a/crates/my-lisp/src/value.rs b/crates/my-lisp/src/value.rs +--- a/crates/my-lisp/src/value.rs ++++ b/crates/my-lisp/src/value.rs +@@ -459,6 +459,12 @@ pub enum Value { + Pair(Rc, Rc), + Closure(Rc), + Macro(Rc), ++ /// Opaque language-owned semantic identity. The ID is the callable value; ++ /// any Rust/Lisp/CML/WASM/FPGA implementation is only its execution ++ /// projection. Canon callables are the first migrated vertical slice. ++ SemanticRef(&'static str), + /// Primitive operation as a first-class value (contract 2.1). ++ /// Legacy host implementation projection; not semantic identity. + Builtin(std::rc::Rc), +@@ -532,6 +538,9 @@ impl PartialEq for Value { + left.len() == right.len() && left.iter().zip(right.iter()).all(|(l, r)| l == r) + } + (Value::NumericBuffer(left), Value::NumericBuffer(right)) => left == right, ++ // Language identity is the numeric semantic ID, never a host pointer. ++ (Value::SemanticRef(left), Value::SemanticRef(right)) => left == right, + // Functions have identity: two separately created closures are not equal. + // Funktsii maiut identychnist: dva okremo stvoreni zamykannia ne ye rivnymy. +@@ -608,6 +617,7 @@ fn render(value: &Value, quote_strings: bool) -> String { + match value { ++ Value::SemanticRef(semantic_id) => format!("#"), + Value::Builtin(builtin) => format!("#", builtin.name), + Value::Vector(v) => { +diff --git a/crates/my-lisp/src/eval/canon.rs b/crates/my-lisp/src/eval/canon.rs +--- a/crates/my-lisp/src/eval/canon.rs ++++ b/crates/my-lisp/src/eval/canon.rs +@@ -5,7 +5,6 @@ + + use super::special_forms::{car_value, cdr_value, cons_values, eq_values}; + use crate::{semantic_registry, Environment, ErrorKind, LanguageError, Span, Value}; +-use std::{collections::HashMap, rc::Rc}; + + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub(crate) enum CanonicalIdentity { +@@ -95,6 +94,13 @@ fn identity_for_semantic_id(semantic_id: &str) -> Option { + .map(|entry| entry.identity) + } + ++fn semantic_id_for_identity(identity: CanonicalIdentity) -> Option<&'static str> { ++ CANON ++ .iter() ++ .find(|entry| entry.identity == identity) ++ .and_then(|entry| entry.semantic_id) ++} ++ + pub(crate) fn identity_for_surface(name: &str) -> Option { + semantic_registry::semantic_id_for_surface(name).and_then(identity_for_semantic_id) + } +@@ -152,76 +158,61 @@ fn exact_args( + )) + } + +-fn builtin( +- identity: &'static str, +- func: impl Fn(&[Value], &Environment, Span) -> Result + 'static, +-) -> Value { +- Value::Builtin(Rc::new(crate::value::Builtin { +- name: identity, +- func: Rc::new(func), +- })) +-} +- +-fn materialize_value(identity: CanonicalIdentity) -> Option { +- match identity { +- CanonicalIdentity::EmptyList => ground_value(identity), +- CanonicalIdentity::Atom => Some(builtin("PRIM_ATOM", |args, _env, span| { ++/// Execute today's mechanical projection of a language-owned semantic value. ++/// The semantic ID is the call target. Rust supplies mechanism only. ++pub(crate) fn invoke_semantic_ref( ++ semantic_id: &str, ++ args: &[Value], ++ environment: &Environment, ++ span: Span, ++) -> Result { ++ match identity_for_semantic_id(semantic_id) { ++ Some(CanonicalIdentity::Atom) => { + exact_args("PRIM_ATOM", args, 1, span)?; + Ok(Value::truth(args[0].is_atom())) +- })), +- CanonicalIdentity::Eq => Some(builtin("PRIM_EQ", |args, _env, span| { ++ } ++ Some(CanonicalIdentity::Eq) => { + exact_args("PRIM_EQ", args, 2, span)?; + eq_values(args[0].clone(), args[1].clone(), span) +- })), +- CanonicalIdentity::Cons => Some(builtin("PRIM_CONS", |args, env, span| { ++ } ++ Some(CanonicalIdentity::Cons) => { + exact_args("PRIM_CONS", args, 2, span)?; +- cons_values(args[0].clone(), args[1].clone(), env, span) +- })), +- CanonicalIdentity::Car => Some(builtin("PRIM_CAR", |args, _env, span| { ++ cons_values(args[0].clone(), args[1].clone(), environment, span) ++ } ++ Some(CanonicalIdentity::Car) => { + exact_args("PRIM_CAR", args, 1, span)?; + car_value(&args[0], span) +- })), +- CanonicalIdentity::Cdr => Some(builtin("PRIM_CDR", |args, _env, span| { ++ } ++ Some(CanonicalIdentity::Cdr) => { + exact_args("PRIM_CDR", args, 1, span)?; + cdr_value(&args[0], span) +- })), +- CanonicalIdentity::Quote | CanonicalIdentity::Cond => None, ++ } ++ Some(CanonicalIdentity::EmptyList | CanonicalIdentity::Quote | CanonicalIdentity::Cond) ++ | None => Err(LanguageError::new( ++ ErrorKind::Type, ++ format!("semantic identity is not a callable Canon value: {semantic_id}"), ++ span, ++ )), + } + } + +-fn build_value_registry() -> HashMap { +- [ +- CanonicalIdentity::Atom, +- CanonicalIdentity::Eq, +- CanonicalIdentity::Cons, +- CanonicalIdentity::Car, +- CanonicalIdentity::Cdr, +- ] +- .into_iter() +- .map(|identity| { +- ( +- identity, +- materialize_value(identity).expect("callable Canon identity must materialize"), +- ) +- }) +- .collect() +-} +- +-thread_local! { +- /// One immutable callable handle per Canon identity per evaluator thread. +- /// Every stable registry spelling resolves to clones of these same `Rc` handles. +- static CANON_VALUES: HashMap = build_value_registry(); +-} +- +-/// Return the stable first-class value for a canonical identity. Special forms +-/// deliberately have no value representation; they remain syntax-only. ++/// Return the first-class language value for a canonical identity. Special ++/// forms remain syntax-only; callable Canon values are semantic references. + pub(crate) fn value(identity: CanonicalIdentity) -> Option { +- if identity == CanonicalIdentity::EmptyList { +- return Some(Value::Nil); ++ match identity { ++ CanonicalIdentity::EmptyList => Some(Value::Nil), ++ CanonicalIdentity::Atom ++ | CanonicalIdentity::Eq ++ | CanonicalIdentity::Cons ++ | CanonicalIdentity::Car ++ | CanonicalIdentity::Cdr => semantic_id_for_identity(identity).map(Value::SemanticRef), ++ CanonicalIdentity::Quote | CanonicalIdentity::Cond => None, + } +- CANON_VALUES.with(|values| values.get(&identity).cloned()) + } +@@ -304,20 +295,18 @@ mod tests { + } + + #[test] +- fn numeric_canon_identity_uses_the_same_evaluator_meaning() { ++ fn numeric_canon_identity_is_the_runtime_value_identity() { + assert_eq!( + identity_for_surface(CAR_SEMANTIC_ID), + Some(CanonicalIdentity::Car) + ); +- let human_surface = semantic_registry::admitted_surfaces_for_semantic_id(CAR_SEMANTIC_ID) +- .into_iter() +- .next() +- .expect("0005 (car) should admit at least one human surface"); + let numeric = value_for_surface(CAR_SEMANTIC_ID).expect("numeric Canon identity"); +- let human = value_for_surface(human_surface).expect("registry-admitted Canon surface"); +- let (Value::Builtin(numeric), Value::Builtin(human)) = (&numeric, &human) else { +- panic!("PRIM_CAR must be a first-class builtin value"); +- }; +- assert!(Rc::ptr_eq(numeric, human)); ++ assert_eq!(numeric, Value::SemanticRef(CAR_SEMANTIC_ID)); + } + + #[test] +- fn every_admitted_surface_for_one_semantic_id_shares_one_stable_handle() { ++ fn every_admitted_surface_for_one_semantic_id_is_the_same_semantic_value() { + // Which spellings mean "car" (en/uk/sa/...) is a semantic-registry + // FACT, owned by the registry data, not Rust knowledge -- this test +- // asserts only the Rust-implementation INVARIANT: whatever surfaces +- // the registry admits for one semantic identity, Canon materializes +- // exactly one shared callable handle for all of them. Read the real +- // admitted surfaces from the registry itself instead of hardcoding +- // "car"/"перше"/"ādi" as literals, so this test still passes +- // unchanged if the registry's admitted spellings for 0005 ever +- // change, and still fails if Canon ever gives two of them distinct +- // handles. ++ // asserts the language invariant directly: every admitted surface is ++ // the same SemanticRef value, independent of a Rust allocation. + let surfaces = semantic_registry::admitted_surfaces_for_semantic_id(CAR_SEMANTIC_ID); + assert!( + surfaces.len() >= 2, +@@ -325,23 +314,13 @@ mod tests { + got {surfaces:?}" + ); + +- let handles: Vec> = surfaces +- .iter() +- .map(|surface| { +- let value = value_for_surface(surface) +- .unwrap_or_else(|| panic!("registry-admitted surface {surface:?} should route through Canon")); +- let Value::Builtin(ref handle) = value else { +- panic!("PRIM_CAR must be a first-class builtin value for surface {surface:?}"); +- }; +- handle.clone() +- }) +- .collect(); +- +- let first = &handles[0]; +- for (surface, handle) in surfaces.iter().zip(handles.iter()) { +- assert!( +- Rc::ptr_eq(first, handle), +- "surface {surface:?} did not share Canon's one stable handle for 0005" ++ for surface in surfaces { ++ assert_eq!( ++ value_for_surface(surface), ++ Some(Value::SemanticRef(CAR_SEMANTIC_ID)), ++ "surface {surface:?} did not materialize semantic value 0005" + ); + } + } +diff --git a/crates/my-lisp/src/eval/mod.rs b/crates/my-lisp/src/eval/mod.rs +--- a/crates/my-lisp/src/eval/mod.rs ++++ b/crates/my-lisp/src/eval/mod.rs +@@ -99,6 +99,9 @@ pub(crate) fn invoke_value( + ) -> Result { + match function { ++ Value::SemanticRef(semantic_id) => { ++ canon::invoke_semantic_ref(semantic_id, arguments, environment, span) ++ } + Value::Builtin(builtin) => (builtin.func)(arguments, environment, span), + Value::Closure(closure) => closures::apply_values(closure.clone(), arguments, span), +@@ -221,6 +224,14 @@ fn evaluate_list( + } + let function = evaluate(&items[0], environment)?; + match &function { ++ Value::SemanticRef(semantic_id) => { ++ let mut values = Vec::with_capacity(arguments.len()); ++ for argument in arguments { ++ values.push(evaluate(argument, environment)?); ++ } ++ canon::invoke_semantic_ref(semantic_id, &values, environment, span) ++ .map(EvalStep::Value) ++ } + Value::Builtin(builtin) => { + let mut values = Vec::with_capacity(arguments.len()); +diff --git a/crates/my-lisp/src/eval/closures.rs b/crates/my-lisp/src/eval/closures.rs +--- a/crates/my-lisp/src/eval/closures.rs ++++ b/crates/my-lisp/src/eval/closures.rs +@@ -344,6 +344,13 @@ pub(super) fn value_to_expr(value: Value, span: Span) -> Result ExprKind::Rational(rational.clone()), + Value::NumericBuffer(buffer) => ExprKind::NumericBuffer(buffer.clone()), + Value::String(val) => ExprKind::String(val.clone()), ++ Value::SemanticRef(semantic_id) => { ++ return Err(LanguageError::new( ++ ErrorKind::Type, ++ format!("a semantic callable ({semantic_id}) is a value, not executable source code"), ++ span, ++ )); ++ } + // A builtin is callable but not syntax: it cannot round-trip + // through eval/macro-expansion as code (contract 2.1 decision). +diff --git a/crates/my-lisp/src/layout.rs b/crates/my-lisp/src/layout.rs +--- a/crates/my-lisp/src/layout.rs ++++ b/crates/my-lisp/src/layout.rs +@@ -20,6 +20,8 @@ pub const TAG_TCP_CONN: u64 = 10; + pub const TAG_TCP_LIST: u64 = 11; + pub const TAG_NUMERIC_BUFFER: u64 = 13; ++/// Process-local implementation pointer; never portable language identity. ++pub const TAG_HOST_BUILTIN: u64 = 14; +@@ -72,12 +74,22 @@ impl NanBox { + let ptr = Rc::as_ptr(m) as u64; + NanBox(Self::pack_ptr(TAG_MACRO, ptr)) + } +- // TAG_PRIMITIVE was reserved in the memory-layout contract +- // from day one -- contract 2.1 finally fills it. ++ // Portable primitive identity: numeric semantic ID, not an address ++ // inside this Rust process. ++ Value::SemanticRef(semantic_id) => { ++ let payload = semantic_id ++ .parse::() ++ .expect("semantic registry IDs must be numeric"); ++ assert!(payload <= 0x0FFF_FFFF, "semantic ID exceeds NaN-box payload"); ++ NanBox(MASK_QNAN | (TAG_PRIMITIVE << 28) | payload) ++ } ++ // Legacy host implementation projection. It gets a distinct tag ++ // so an Rc address can never masquerade as semantic identity. + Value::Builtin(b) => { + let ptr = Rc::as_ptr(b) as u64; +- NanBox(Self::pack_ptr(TAG_PRIMITIVE, ptr)) ++ NanBox(Self::pack_ptr(TAG_HOST_BUILTIN, ptr)) + } + Value::Vector(v) => { +diff --git a/crates/my-lisp/src/presentation.rs b/crates/my-lisp/src/presentation.rs +--- a/crates/my-lisp/src/presentation.rs ++++ b/crates/my-lisp/src/presentation.rs +@@ -85,6 +85,9 @@ fn uk_decimal(text: String) -> String { + + fn render_uk(value: &Value) -> String { + match value { ++ Value::SemanticRef(semantic_id) => { ++ format!("#<вбудована {}>", uk_operation_name(semantic_id)) ++ } + Value::Builtin(builtin) => { + format!("#<вбудована {}>", uk_operation_name(builtin.name)) + } diff --git a/.github/workflows/eco-canon-2-semantic-ref-once.yml b/.github/workflows/eco-canon-2-semantic-ref-once.yml new file mode 100644 index 00000000..44f8d217 --- /dev/null +++ b/.github/workflows/eco-canon-2-semantic-ref-once.yml @@ -0,0 +1,62 @@ +name: ECO-CANON-2 semantic-ref vertical slice + +on: + push: + branches: + - feat/eco-canon-2-semantic-identity-red + paths: + - .github/patches/eco-canon-2-semantic-ref.patch + - .github/workflows/eco-canon-2-semantic-ref-once.yml + +permissions: + contents: write + +jobs: + verify-and-commit: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + with: + ref: feat/eco-canon-2-semantic-identity-red + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - name: Apply semantic-ref slice + run: | + git apply --check .github/patches/eco-canon-2-semantic-ref.patch + git apply .github/patches/eco-canon-2-semantic-ref.patch + cargo fmt --all + git diff --check + - name: Focused language-power witnesses + run: | + cargo test -p my-lisp --test first_class_builtins -- --nocapture + cargo test -p my-lisp eval::canon::tests:: -- --nocapture + cargo test -p my-lisp presentation::tests::ukrainian_presentation_localizes_builtin_and_function_markers -- --nocapture + - name: Full workspace regression + run: cargo test --workspace + - name: Workspace build + run: cargo build --workspace + - name: Zero-warning Clippy + run: cargo clippy --workspace --all-targets -- -D warnings + - name: Repository policy and projection checks + run: cargo run -p xtask -- verify + - name: Commit verified vertical slice + run: | + rm .github/patches/eco-canon-2-semantic-ref.patch + rm .github/workflows/eco-canon-2-semantic-ref-once.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add crates/my-lisp/src/value.rs \ + crates/my-lisp/src/eval/canon.rs \ + crates/my-lisp/src/eval/mod.rs \ + crates/my-lisp/src/eval/closures.rs \ + crates/my-lisp/src/layout.rs \ + crates/my-lisp/src/presentation.rs \ + .github/patches/eco-canon-2-semantic-ref.patch \ + .github/workflows/eco-canon-2-semantic-ref-once.yml + git diff --cached --check + git commit -m "feat(canon): make semantic refs language values" + git push origin HEAD:feat/eco-canon-2-semantic-identity-red From 80a026b5bc80bc208f3e9a42deea82aa3195a9d1 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:55:27 +0300 Subject: [PATCH 09/26] ci(canon): recount semantic-ref patch hunks --- .github/workflows/eco-canon-2-semantic-ref-once.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/eco-canon-2-semantic-ref-once.yml b/.github/workflows/eco-canon-2-semantic-ref-once.yml index 44f8d217..15c0fe34 100644 --- a/.github/workflows/eco-canon-2-semantic-ref-once.yml +++ b/.github/workflows/eco-canon-2-semantic-ref-once.yml @@ -26,8 +26,8 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Apply semantic-ref slice run: | - git apply --check .github/patches/eco-canon-2-semantic-ref.patch - git apply .github/patches/eco-canon-2-semantic-ref.patch + git apply --recount --check .github/patches/eco-canon-2-semantic-ref.patch + git apply --recount .github/patches/eco-canon-2-semantic-ref.patch cargo fmt --all git diff --check - name: Focused language-power witnesses From ff45f6ba91969c2a55e6244e294f934dacd7d834 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:56:45 +0300 Subject: [PATCH 10/26] ci(canon): isolate semantic layout projection --- .../workflows/eco-canon-2-semantic-ref-once.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/eco-canon-2-semantic-ref-once.yml b/.github/workflows/eco-canon-2-semantic-ref-once.yml index 15c0fe34..3caeab34 100644 --- a/.github/workflows/eco-canon-2-semantic-ref-once.yml +++ b/.github/workflows/eco-canon-2-semantic-ref-once.yml @@ -26,8 +26,19 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Apply semantic-ref slice run: | - git apply --recount --check .github/patches/eco-canon-2-semantic-ref.patch - git apply --recount .github/patches/eco-canon-2-semantic-ref.patch + awk ' + /^diff --git a\/crates\/my-lisp\/src\/layout.rs / { skip=1; next } + /^diff --git / && skip { skip=0 } + !skip { print } + ' .github/patches/eco-canon-2-semantic-ref.patch > /tmp/eco-canon-2-semantic-ref.patch + git apply --recount --check /tmp/eco-canon-2-semantic-ref.patch + git apply --recount /tmp/eco-canon-2-semantic-ref.patch + + perl -0pi -e 's/pub const TAG_NUMERIC_BUFFER: u64 = 13;\n/pub const TAG_NUMERIC_BUFFER: u64 = 13;\n\/\/\/ Process-local implementation pointer; never portable language identity.\npub const TAG_HOST_BUILTIN: u64 = 14;\n/' crates/my-lisp/src/layout.rs + perl -0pi -e 's! // TAG_PRIMITIVE was reserved in the memory-layout contract\n // from day one -- contract 2\.1 finally fills it\.\n Value::Builtin\(b\) => \{\n let ptr = Rc::as_ptr\(b\) as u64;\n NanBox\(Self::pack_ptr\(TAG_PRIMITIVE, ptr\)\)\n \}! // Portable primitive identity: numeric semantic ID, not an address\n // inside this Rust process.\n Value::SemanticRef(semantic_id) => {\n let payload = semantic_id\n .parse::()\n .expect("semantic registry IDs must be numeric");\n assert!(payload <= 0x0FFF_FFFF, "semantic ID exceeds NaN-box payload");\n NanBox(MASK_QNAN | (TAG_PRIMITIVE << 28) | payload)\n }\n // Legacy host implementation projection. It gets a distinct tag\n // so an Rc address can never masquerade as semantic identity.\n Value::Builtin(b) => {\n let ptr = Rc::as_ptr(b) as u64;\n NanBox(Self::pack_ptr(TAG_HOST_BUILTIN, ptr))\n }!s' crates/my-lisp/src/layout.rs + + grep -q 'Value::SemanticRef(semantic_id)' crates/my-lisp/src/layout.rs + grep -q 'TAG_HOST_BUILTIN' crates/my-lisp/src/layout.rs cargo fmt --all git diff --check - name: Focused language-power witnesses From c8ed1f7df46fcd6018715879efa0c0a16326dc96 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:57:33 +0300 Subject: [PATCH 11/26] ci(canon): fix one-shot layout transform delimiter --- .github/workflows/eco-canon-2-semantic-ref-once.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/eco-canon-2-semantic-ref-once.yml b/.github/workflows/eco-canon-2-semantic-ref-once.yml index 3caeab34..f2155899 100644 --- a/.github/workflows/eco-canon-2-semantic-ref-once.yml +++ b/.github/workflows/eco-canon-2-semantic-ref-once.yml @@ -35,7 +35,7 @@ jobs: git apply --recount /tmp/eco-canon-2-semantic-ref.patch perl -0pi -e 's/pub const TAG_NUMERIC_BUFFER: u64 = 13;\n/pub const TAG_NUMERIC_BUFFER: u64 = 13;\n\/\/\/ Process-local implementation pointer; never portable language identity.\npub const TAG_HOST_BUILTIN: u64 = 14;\n/' crates/my-lisp/src/layout.rs - perl -0pi -e 's! // TAG_PRIMITIVE was reserved in the memory-layout contract\n // from day one -- contract 2\.1 finally fills it\.\n Value::Builtin\(b\) => \{\n let ptr = Rc::as_ptr\(b\) as u64;\n NanBox\(Self::pack_ptr\(TAG_PRIMITIVE, ptr\)\)\n \}! // Portable primitive identity: numeric semantic ID, not an address\n // inside this Rust process.\n Value::SemanticRef(semantic_id) => {\n let payload = semantic_id\n .parse::()\n .expect("semantic registry IDs must be numeric");\n assert!(payload <= 0x0FFF_FFFF, "semantic ID exceeds NaN-box payload");\n NanBox(MASK_QNAN | (TAG_PRIMITIVE << 28) | payload)\n }\n // Legacy host implementation projection. It gets a distinct tag\n // so an Rc address can never masquerade as semantic identity.\n Value::Builtin(b) => {\n let ptr = Rc::as_ptr(b) as u64;\n NanBox(Self::pack_ptr(TAG_HOST_BUILTIN, ptr))\n }!s' crates/my-lisp/src/layout.rs + perl -0pi -e 's~ // TAG_PRIMITIVE was reserved in the memory-layout contract\n // from day one -- contract 2\.1 finally fills it\.\n Value::Builtin\(b\) => \{\n let ptr = Rc::as_ptr\(b\) as u64;\n NanBox\(Self::pack_ptr\(TAG_PRIMITIVE, ptr\)\)\n \}~ // Portable primitive identity: numeric semantic ID, not an address\n // inside this Rust process.\n Value::SemanticRef(semantic_id) => {\n let payload = semantic_id\n .parse::()\n .expect("semantic registry IDs must be numeric");\n assert!(payload <= 0x0FFF_FFFF, "semantic ID exceeds NaN-box payload");\n NanBox(MASK_QNAN | (TAG_PRIMITIVE << 28) | payload)\n }\n // Legacy host implementation projection. It gets a distinct tag\n // so an Rc address can never masquerade as semantic identity.\n Value::Builtin(b) => {\n let ptr = Rc::as_ptr(b) as u64;\n NanBox(Self::pack_ptr(TAG_HOST_BUILTIN, ptr))\n }~s' crates/my-lisp/src/layout.rs grep -q 'Value::SemanticRef(semantic_id)' crates/my-lisp/src/layout.rs grep -q 'TAG_HOST_BUILTIN' crates/my-lisp/src/layout.rs From 92eae1ce5fb921a5be825fd00e520e841c59dfb3 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:04:16 +0300 Subject: [PATCH 12/26] test(layout): require semantic ID portable primitive identity --- crates/my-lisp/src/layout.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/my-lisp/src/layout.rs b/crates/my-lisp/src/layout.rs index e4ae2139..c61d54ba 100644 --- a/crates/my-lisp/src/layout.rs +++ b/crates/my-lisp/src/layout.rs @@ -99,3 +99,32 @@ impl NanBox { } } } + +#[cfg(test)] +mod semantic_identity_tests { + use super::*; + use crate::value::Builtin; + use crate::{Environment, LanguageError, Span}; + + #[test] + fn semantic_ref_nanbox_payload_is_the_numeric_language_identity() { + let bits = NanBox::from_value(&Value::SemanticRef("0005")).0; + assert_eq!((bits >> 28) & 0xF, TAG_PRIMITIVE); + assert_eq!(bits & 0x0FFF_FFFF, 5); + } + + #[test] + #[should_panic(expected = "legacy host builtin has no portable NaN-box representation")] + fn legacy_builtin_pointer_cannot_masquerade_as_portable_semantic_identity() { + let func = Rc::new( + |_args: &[Value], _env: &Environment, _span: Span| -> Result { + Ok(Value::Nil) + }, + ); + let value = Value::Builtin(Rc::new(Builtin { + name: "host-only-test", + func, + })); + let _ = NanBox::from_value(&value); + } +} From deefc3a866ac50b8fcdb771e779027e8ff2eee15 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:04:34 +0300 Subject: [PATCH 13/26] ci(canon): make legacy builtin layout fail closed --- .github/workflows/eco-canon-2-semantic-ref-once.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/eco-canon-2-semantic-ref-once.yml b/.github/workflows/eco-canon-2-semantic-ref-once.yml index f2155899..cffee877 100644 --- a/.github/workflows/eco-canon-2-semantic-ref-once.yml +++ b/.github/workflows/eco-canon-2-semantic-ref-once.yml @@ -34,17 +34,21 @@ jobs: git apply --recount --check /tmp/eco-canon-2-semantic-ref.patch git apply --recount /tmp/eco-canon-2-semantic-ref.patch - perl -0pi -e 's/pub const TAG_NUMERIC_BUFFER: u64 = 13;\n/pub const TAG_NUMERIC_BUFFER: u64 = 13;\n\/\/\/ Process-local implementation pointer; never portable language identity.\npub const TAG_HOST_BUILTIN: u64 = 14;\n/' crates/my-lisp/src/layout.rs - perl -0pi -e 's~ // TAG_PRIMITIVE was reserved in the memory-layout contract\n // from day one -- contract 2\.1 finally fills it\.\n Value::Builtin\(b\) => \{\n let ptr = Rc::as_ptr\(b\) as u64;\n NanBox\(Self::pack_ptr\(TAG_PRIMITIVE, ptr\)\)\n \}~ // Portable primitive identity: numeric semantic ID, not an address\n // inside this Rust process.\n Value::SemanticRef(semantic_id) => {\n let payload = semantic_id\n .parse::()\n .expect("semantic registry IDs must be numeric");\n assert!(payload <= 0x0FFF_FFFF, "semantic ID exceeds NaN-box payload");\n NanBox(MASK_QNAN | (TAG_PRIMITIVE << 28) | payload)\n }\n // Legacy host implementation projection. It gets a distinct tag\n // so an Rc address can never masquerade as semantic identity.\n Value::Builtin(b) => {\n let ptr = Rc::as_ptr(b) as u64;\n NanBox(Self::pack_ptr(TAG_HOST_BUILTIN, ptr))\n }~s' crates/my-lisp/src/layout.rs + perl -0pi -e 's~ // TAG_PRIMITIVE was reserved in the memory-layout contract\n // from day one -- contract 2\.1 finally fills it\.\n Value::Builtin\(b\) => \{\n let ptr = Rc::as_ptr\(b\) as u64;\n NanBox\(Self::pack_ptr\(TAG_PRIMITIVE, ptr\)\)\n \}~ // Portable primitive identity: the numeric semantic ID, never a\n // process-local Rust allocation.\n Value::SemanticRef(semantic_id) => {\n let payload = semantic_id\n .parse::()\n .expect("semantic registry IDs must be numeric");\n assert!(payload <= 0x0FFF_FFFF, "semantic ID exceeds NaN-box payload");\n NanBox(MASK_QNAN | (TAG_PRIMITIVE << 28) | payload)\n }\n // A legacy Builtin is only a host implementation projection. It\n // deliberately has no portable identity; fail closed rather than\n // smuggling a Rust pointer through the language ABI.\n Value::Builtin(_) => {\n panic!("legacy host builtin has no portable NaN-box representation")\n }~s' crates/my-lisp/src/layout.rs grep -q 'Value::SemanticRef(semantic_id)' crates/my-lisp/src/layout.rs - grep -q 'TAG_HOST_BUILTIN' crates/my-lisp/src/layout.rs + grep -q 'legacy host builtin has no portable NaN-box representation' crates/my-lisp/src/layout.rs + if grep -q 'TAG_HOST_BUILTIN' crates/my-lisp/src/layout.rs; then + echo 'unexpected host-builtin ABI tag' >&2 + exit 1 + fi cargo fmt --all git diff --check - name: Focused language-power witnesses run: | cargo test -p my-lisp --test first_class_builtins -- --nocapture cargo test -p my-lisp eval::canon::tests:: -- --nocapture + cargo test -p my-lisp layout::semantic_identity_tests:: -- --nocapture cargo test -p my-lisp presentation::tests::ukrainian_presentation_localizes_builtin_and_function_markers -- --nocapture - name: Full workspace regression run: cargo test --workspace From 5dd649f58ddd268d038dfbcb4709bfbb5b848aa4 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:16:06 +0300 Subject: [PATCH 14/26] test(canon): isolate semantic identity RED witness --- crates/my-lisp/src/layout.rs | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/crates/my-lisp/src/layout.rs b/crates/my-lisp/src/layout.rs index c61d54ba..e4ae2139 100644 --- a/crates/my-lisp/src/layout.rs +++ b/crates/my-lisp/src/layout.rs @@ -99,32 +99,3 @@ impl NanBox { } } } - -#[cfg(test)] -mod semantic_identity_tests { - use super::*; - use crate::value::Builtin; - use crate::{Environment, LanguageError, Span}; - - #[test] - fn semantic_ref_nanbox_payload_is_the_numeric_language_identity() { - let bits = NanBox::from_value(&Value::SemanticRef("0005")).0; - assert_eq!((bits >> 28) & 0xF, TAG_PRIMITIVE); - assert_eq!(bits & 0x0FFF_FFFF, 5); - } - - #[test] - #[should_panic(expected = "legacy host builtin has no portable NaN-box representation")] - fn legacy_builtin_pointer_cannot_masquerade_as_portable_semantic_identity() { - let func = Rc::new( - |_args: &[Value], _env: &Environment, _span: Span| -> Result { - Ok(Value::Nil) - }, - ); - let value = Value::Builtin(Rc::new(Builtin { - name: "host-only-test", - func, - })); - let _ = NanBox::from_value(&value); - } -} From ef64f337436a350a0192c64b921394102517cdb8 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:16:22 +0300 Subject: [PATCH 15/26] test(canon): remove premature semantic-ref implementation patch --- .../patches/eco-canon-2-semantic-ref.patch | 316 ------------------ 1 file changed, 316 deletions(-) delete mode 100644 .github/patches/eco-canon-2-semantic-ref.patch diff --git a/.github/patches/eco-canon-2-semantic-ref.patch b/.github/patches/eco-canon-2-semantic-ref.patch deleted file mode 100644 index 91e5bc37..00000000 --- a/.github/patches/eco-canon-2-semantic-ref.patch +++ /dev/null @@ -1,316 +0,0 @@ -diff --git a/crates/my-lisp/src/value.rs b/crates/my-lisp/src/value.rs ---- a/crates/my-lisp/src/value.rs -+++ b/crates/my-lisp/src/value.rs -@@ -459,6 +459,12 @@ pub enum Value { - Pair(Rc, Rc), - Closure(Rc), - Macro(Rc), -+ /// Opaque language-owned semantic identity. The ID is the callable value; -+ /// any Rust/Lisp/CML/WASM/FPGA implementation is only its execution -+ /// projection. Canon callables are the first migrated vertical slice. -+ SemanticRef(&'static str), - /// Primitive operation as a first-class value (contract 2.1). -+ /// Legacy host implementation projection; not semantic identity. - Builtin(std::rc::Rc), -@@ -532,6 +538,9 @@ impl PartialEq for Value { - left.len() == right.len() && left.iter().zip(right.iter()).all(|(l, r)| l == r) - } - (Value::NumericBuffer(left), Value::NumericBuffer(right)) => left == right, -+ // Language identity is the numeric semantic ID, never a host pointer. -+ (Value::SemanticRef(left), Value::SemanticRef(right)) => left == right, - // Functions have identity: two separately created closures are not equal. - // Funktsii maiut identychnist: dva okremo stvoreni zamykannia ne ye rivnymy. -@@ -608,6 +617,7 @@ fn render(value: &Value, quote_strings: bool) -> String { - match value { -+ Value::SemanticRef(semantic_id) => format!("#"), - Value::Builtin(builtin) => format!("#", builtin.name), - Value::Vector(v) => { -diff --git a/crates/my-lisp/src/eval/canon.rs b/crates/my-lisp/src/eval/canon.rs ---- a/crates/my-lisp/src/eval/canon.rs -+++ b/crates/my-lisp/src/eval/canon.rs -@@ -5,7 +5,6 @@ - - use super::special_forms::{car_value, cdr_value, cons_values, eq_values}; - use crate::{semantic_registry, Environment, ErrorKind, LanguageError, Span, Value}; --use std::{collections::HashMap, rc::Rc}; - - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] - pub(crate) enum CanonicalIdentity { -@@ -95,6 +94,13 @@ fn identity_for_semantic_id(semantic_id: &str) -> Option { - .map(|entry| entry.identity) - } - -+fn semantic_id_for_identity(identity: CanonicalIdentity) -> Option<&'static str> { -+ CANON -+ .iter() -+ .find(|entry| entry.identity == identity) -+ .and_then(|entry| entry.semantic_id) -+} -+ - pub(crate) fn identity_for_surface(name: &str) -> Option { - semantic_registry::semantic_id_for_surface(name).and_then(identity_for_semantic_id) - } -@@ -152,76 +158,61 @@ fn exact_args( - )) - } - --fn builtin( -- identity: &'static str, -- func: impl Fn(&[Value], &Environment, Span) -> Result + 'static, --) -> Value { -- Value::Builtin(Rc::new(crate::value::Builtin { -- name: identity, -- func: Rc::new(func), -- })) --} -- --fn materialize_value(identity: CanonicalIdentity) -> Option { -- match identity { -- CanonicalIdentity::EmptyList => ground_value(identity), -- CanonicalIdentity::Atom => Some(builtin("PRIM_ATOM", |args, _env, span| { -+/// Execute today's mechanical projection of a language-owned semantic value. -+/// The semantic ID is the call target. Rust supplies mechanism only. -+pub(crate) fn invoke_semantic_ref( -+ semantic_id: &str, -+ args: &[Value], -+ environment: &Environment, -+ span: Span, -+) -> Result { -+ match identity_for_semantic_id(semantic_id) { -+ Some(CanonicalIdentity::Atom) => { - exact_args("PRIM_ATOM", args, 1, span)?; - Ok(Value::truth(args[0].is_atom())) -- })), -- CanonicalIdentity::Eq => Some(builtin("PRIM_EQ", |args, _env, span| { -+ } -+ Some(CanonicalIdentity::Eq) => { - exact_args("PRIM_EQ", args, 2, span)?; - eq_values(args[0].clone(), args[1].clone(), span) -- })), -- CanonicalIdentity::Cons => Some(builtin("PRIM_CONS", |args, env, span| { -+ } -+ Some(CanonicalIdentity::Cons) => { - exact_args("PRIM_CONS", args, 2, span)?; -- cons_values(args[0].clone(), args[1].clone(), env, span) -- })), -- CanonicalIdentity::Car => Some(builtin("PRIM_CAR", |args, _env, span| { -+ cons_values(args[0].clone(), args[1].clone(), environment, span) -+ } -+ Some(CanonicalIdentity::Car) => { - exact_args("PRIM_CAR", args, 1, span)?; - car_value(&args[0], span) -- })), -- CanonicalIdentity::Cdr => Some(builtin("PRIM_CDR", |args, _env, span| { -+ } -+ Some(CanonicalIdentity::Cdr) => { - exact_args("PRIM_CDR", args, 1, span)?; - cdr_value(&args[0], span) -- })), -- CanonicalIdentity::Quote | CanonicalIdentity::Cond => None, -+ } -+ Some(CanonicalIdentity::EmptyList | CanonicalIdentity::Quote | CanonicalIdentity::Cond) -+ | None => Err(LanguageError::new( -+ ErrorKind::Type, -+ format!("semantic identity is not a callable Canon value: {semantic_id}"), -+ span, -+ )), - } - } - --fn build_value_registry() -> HashMap { -- [ -- CanonicalIdentity::Atom, -- CanonicalIdentity::Eq, -- CanonicalIdentity::Cons, -- CanonicalIdentity::Car, -- CanonicalIdentity::Cdr, -- ] -- .into_iter() -- .map(|identity| { -- ( -- identity, -- materialize_value(identity).expect("callable Canon identity must materialize"), -- ) -- }) -- .collect() --} -- --thread_local! { -- /// One immutable callable handle per Canon identity per evaluator thread. -- /// Every stable registry spelling resolves to clones of these same `Rc` handles. -- static CANON_VALUES: HashMap = build_value_registry(); --} -- --/// Return the stable first-class value for a canonical identity. Special forms --/// deliberately have no value representation; they remain syntax-only. -+/// Return the first-class language value for a canonical identity. Special -+/// forms remain syntax-only; callable Canon values are semantic references. - pub(crate) fn value(identity: CanonicalIdentity) -> Option { -- if identity == CanonicalIdentity::EmptyList { -- return Some(Value::Nil); -+ match identity { -+ CanonicalIdentity::EmptyList => Some(Value::Nil), -+ CanonicalIdentity::Atom -+ | CanonicalIdentity::Eq -+ | CanonicalIdentity::Cons -+ | CanonicalIdentity::Car -+ | CanonicalIdentity::Cdr => semantic_id_for_identity(identity).map(Value::SemanticRef), -+ CanonicalIdentity::Quote | CanonicalIdentity::Cond => None, - } -- CANON_VALUES.with(|values| values.get(&identity).cloned()) - } -@@ -304,20 +295,18 @@ mod tests { - } - - #[test] -- fn numeric_canon_identity_uses_the_same_evaluator_meaning() { -+ fn numeric_canon_identity_is_the_runtime_value_identity() { - assert_eq!( - identity_for_surface(CAR_SEMANTIC_ID), - Some(CanonicalIdentity::Car) - ); -- let human_surface = semantic_registry::admitted_surfaces_for_semantic_id(CAR_SEMANTIC_ID) -- .into_iter() -- .next() -- .expect("0005 (car) should admit at least one human surface"); - let numeric = value_for_surface(CAR_SEMANTIC_ID).expect("numeric Canon identity"); -- let human = value_for_surface(human_surface).expect("registry-admitted Canon surface"); -- let (Value::Builtin(numeric), Value::Builtin(human)) = (&numeric, &human) else { -- panic!("PRIM_CAR must be a first-class builtin value"); -- }; -- assert!(Rc::ptr_eq(numeric, human)); -+ assert_eq!(numeric, Value::SemanticRef(CAR_SEMANTIC_ID)); - } - - #[test] -- fn every_admitted_surface_for_one_semantic_id_shares_one_stable_handle() { -+ fn every_admitted_surface_for_one_semantic_id_is_the_same_semantic_value() { - // Which spellings mean "car" (en/uk/sa/...) is a semantic-registry - // FACT, owned by the registry data, not Rust knowledge -- this test -- // asserts only the Rust-implementation INVARIANT: whatever surfaces -- // the registry admits for one semantic identity, Canon materializes -- // exactly one shared callable handle for all of them. Read the real -- // admitted surfaces from the registry itself instead of hardcoding -- // "car"/"перше"/"ādi" as literals, so this test still passes -- // unchanged if the registry's admitted spellings for 0005 ever -- // change, and still fails if Canon ever gives two of them distinct -- // handles. -+ // asserts the language invariant directly: every admitted surface is -+ // the same SemanticRef value, independent of a Rust allocation. - let surfaces = semantic_registry::admitted_surfaces_for_semantic_id(CAR_SEMANTIC_ID); - assert!( - surfaces.len() >= 2, -@@ -325,23 +314,13 @@ mod tests { - got {surfaces:?}" - ); - -- let handles: Vec> = surfaces -- .iter() -- .map(|surface| { -- let value = value_for_surface(surface) -- .unwrap_or_else(|| panic!("registry-admitted surface {surface:?} should route through Canon")); -- let Value::Builtin(ref handle) = value else { -- panic!("PRIM_CAR must be a first-class builtin value for surface {surface:?}"); -- }; -- handle.clone() -- }) -- .collect(); -- -- let first = &handles[0]; -- for (surface, handle) in surfaces.iter().zip(handles.iter()) { -- assert!( -- Rc::ptr_eq(first, handle), -- "surface {surface:?} did not share Canon's one stable handle for 0005" -+ for surface in surfaces { -+ assert_eq!( -+ value_for_surface(surface), -+ Some(Value::SemanticRef(CAR_SEMANTIC_ID)), -+ "surface {surface:?} did not materialize semantic value 0005" - ); - } - } -diff --git a/crates/my-lisp/src/eval/mod.rs b/crates/my-lisp/src/eval/mod.rs ---- a/crates/my-lisp/src/eval/mod.rs -+++ b/crates/my-lisp/src/eval/mod.rs -@@ -99,6 +99,9 @@ pub(crate) fn invoke_value( - ) -> Result { - match function { -+ Value::SemanticRef(semantic_id) => { -+ canon::invoke_semantic_ref(semantic_id, arguments, environment, span) -+ } - Value::Builtin(builtin) => (builtin.func)(arguments, environment, span), - Value::Closure(closure) => closures::apply_values(closure.clone(), arguments, span), -@@ -221,6 +224,14 @@ fn evaluate_list( - } - let function = evaluate(&items[0], environment)?; - match &function { -+ Value::SemanticRef(semantic_id) => { -+ let mut values = Vec::with_capacity(arguments.len()); -+ for argument in arguments { -+ values.push(evaluate(argument, environment)?); -+ } -+ canon::invoke_semantic_ref(semantic_id, &values, environment, span) -+ .map(EvalStep::Value) -+ } - Value::Builtin(builtin) => { - let mut values = Vec::with_capacity(arguments.len()); -diff --git a/crates/my-lisp/src/eval/closures.rs b/crates/my-lisp/src/eval/closures.rs ---- a/crates/my-lisp/src/eval/closures.rs -+++ b/crates/my-lisp/src/eval/closures.rs -@@ -344,6 +344,13 @@ pub(super) fn value_to_expr(value: Value, span: Span) -> Result ExprKind::Rational(rational.clone()), - Value::NumericBuffer(buffer) => ExprKind::NumericBuffer(buffer.clone()), - Value::String(val) => ExprKind::String(val.clone()), -+ Value::SemanticRef(semantic_id) => { -+ return Err(LanguageError::new( -+ ErrorKind::Type, -+ format!("a semantic callable ({semantic_id}) is a value, not executable source code"), -+ span, -+ )); -+ } - // A builtin is callable but not syntax: it cannot round-trip - // through eval/macro-expansion as code (contract 2.1 decision). -diff --git a/crates/my-lisp/src/layout.rs b/crates/my-lisp/src/layout.rs ---- a/crates/my-lisp/src/layout.rs -+++ b/crates/my-lisp/src/layout.rs -@@ -20,6 +20,8 @@ pub const TAG_TCP_CONN: u64 = 10; - pub const TAG_TCP_LIST: u64 = 11; - pub const TAG_NUMERIC_BUFFER: u64 = 13; -+/// Process-local implementation pointer; never portable language identity. -+pub const TAG_HOST_BUILTIN: u64 = 14; -@@ -72,12 +74,22 @@ impl NanBox { - let ptr = Rc::as_ptr(m) as u64; - NanBox(Self::pack_ptr(TAG_MACRO, ptr)) - } -- // TAG_PRIMITIVE was reserved in the memory-layout contract -- // from day one -- contract 2.1 finally fills it. -+ // Portable primitive identity: numeric semantic ID, not an address -+ // inside this Rust process. -+ Value::SemanticRef(semantic_id) => { -+ let payload = semantic_id -+ .parse::() -+ .expect("semantic registry IDs must be numeric"); -+ assert!(payload <= 0x0FFF_FFFF, "semantic ID exceeds NaN-box payload"); -+ NanBox(MASK_QNAN | (TAG_PRIMITIVE << 28) | payload) -+ } -+ // Legacy host implementation projection. It gets a distinct tag -+ // so an Rc address can never masquerade as semantic identity. - Value::Builtin(b) => { - let ptr = Rc::as_ptr(b) as u64; -- NanBox(Self::pack_ptr(TAG_PRIMITIVE, ptr)) -+ NanBox(Self::pack_ptr(TAG_HOST_BUILTIN, ptr)) - } - Value::Vector(v) => { -diff --git a/crates/my-lisp/src/presentation.rs b/crates/my-lisp/src/presentation.rs ---- a/crates/my-lisp/src/presentation.rs -+++ b/crates/my-lisp/src/presentation.rs -@@ -85,6 +85,9 @@ fn uk_decimal(text: String) -> String { - - fn render_uk(value: &Value) -> String { - match value { -+ Value::SemanticRef(semantic_id) => { -+ format!("#<вбудована {}>", uk_operation_name(semantic_id)) -+ } - Value::Builtin(builtin) => { - format!("#<вбудована {}>", uk_operation_name(builtin.name)) - } From 4b3a5519b30e7feab608a962e14b4e7bfa6c7e76 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:16:29 +0300 Subject: [PATCH 16/26] test(canon): keep RED branch representation-neutral --- .../eco-canon-2-semantic-ref-once.yml | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 .github/workflows/eco-canon-2-semantic-ref-once.yml diff --git a/.github/workflows/eco-canon-2-semantic-ref-once.yml b/.github/workflows/eco-canon-2-semantic-ref-once.yml deleted file mode 100644 index cffee877..00000000 --- a/.github/workflows/eco-canon-2-semantic-ref-once.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: ECO-CANON-2 semantic-ref vertical slice - -on: - push: - branches: - - feat/eco-canon-2-semantic-identity-red - paths: - - .github/patches/eco-canon-2-semantic-ref.patch - - .github/workflows/eco-canon-2-semantic-ref-once.yml - -permissions: - contents: write - -jobs: - verify-and-commit: - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@v5 - with: - ref: feat/eco-canon-2-semantic-identity-red - fetch-depth: 0 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - uses: Swatinem/rust-cache@v2 - - name: Apply semantic-ref slice - run: | - awk ' - /^diff --git a\/crates\/my-lisp\/src\/layout.rs / { skip=1; next } - /^diff --git / && skip { skip=0 } - !skip { print } - ' .github/patches/eco-canon-2-semantic-ref.patch > /tmp/eco-canon-2-semantic-ref.patch - git apply --recount --check /tmp/eco-canon-2-semantic-ref.patch - git apply --recount /tmp/eco-canon-2-semantic-ref.patch - - perl -0pi -e 's~ // TAG_PRIMITIVE was reserved in the memory-layout contract\n // from day one -- contract 2\.1 finally fills it\.\n Value::Builtin\(b\) => \{\n let ptr = Rc::as_ptr\(b\) as u64;\n NanBox\(Self::pack_ptr\(TAG_PRIMITIVE, ptr\)\)\n \}~ // Portable primitive identity: the numeric semantic ID, never a\n // process-local Rust allocation.\n Value::SemanticRef(semantic_id) => {\n let payload = semantic_id\n .parse::()\n .expect("semantic registry IDs must be numeric");\n assert!(payload <= 0x0FFF_FFFF, "semantic ID exceeds NaN-box payload");\n NanBox(MASK_QNAN | (TAG_PRIMITIVE << 28) | payload)\n }\n // A legacy Builtin is only a host implementation projection. It\n // deliberately has no portable identity; fail closed rather than\n // smuggling a Rust pointer through the language ABI.\n Value::Builtin(_) => {\n panic!("legacy host builtin has no portable NaN-box representation")\n }~s' crates/my-lisp/src/layout.rs - - grep -q 'Value::SemanticRef(semantic_id)' crates/my-lisp/src/layout.rs - grep -q 'legacy host builtin has no portable NaN-box representation' crates/my-lisp/src/layout.rs - if grep -q 'TAG_HOST_BUILTIN' crates/my-lisp/src/layout.rs; then - echo 'unexpected host-builtin ABI tag' >&2 - exit 1 - fi - cargo fmt --all - git diff --check - - name: Focused language-power witnesses - run: | - cargo test -p my-lisp --test first_class_builtins -- --nocapture - cargo test -p my-lisp eval::canon::tests:: -- --nocapture - cargo test -p my-lisp layout::semantic_identity_tests:: -- --nocapture - cargo test -p my-lisp presentation::tests::ukrainian_presentation_localizes_builtin_and_function_markers -- --nocapture - - name: Full workspace regression - run: cargo test --workspace - - name: Workspace build - run: cargo build --workspace - - name: Zero-warning Clippy - run: cargo clippy --workspace --all-targets -- -D warnings - - name: Repository policy and projection checks - run: cargo run -p xtask -- verify - - name: Commit verified vertical slice - run: | - rm .github/patches/eco-canon-2-semantic-ref.patch - rm .github/workflows/eco-canon-2-semantic-ref-once.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add crates/my-lisp/src/value.rs \ - crates/my-lisp/src/eval/canon.rs \ - crates/my-lisp/src/eval/mod.rs \ - crates/my-lisp/src/eval/closures.rs \ - crates/my-lisp/src/layout.rs \ - crates/my-lisp/src/presentation.rs \ - .github/patches/eco-canon-2-semantic-ref.patch \ - .github/workflows/eco-canon-2-semantic-ref-once.yml - git diff --cached --check - git commit -m "feat(canon): make semantic refs language values" - git push origin HEAD:feat/eco-canon-2-semantic-identity-red From 34c7949808ed7dce4d9514a5de106e59b2fdb794 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:25:30 +0300 Subject: [PATCH 17/26] knowledge: propose guard-reference candidate on guix --target flag scope Discovered live 2026-09-13 while cross-compiling my-lisp-cli for aarch64-linux-gnu (Android TV box oracle experiment): --target= works only for guix build, not guix install/guix shell (unrecognized option on both). Working path is guix build --target= pkg, then use the resulting /gnu/store path directly. Also confirmed aarch64-linux-musl is not a supported guix cross-compilation target (gnu/glibc only), relevant for Android deployment since Bionic needs a fully-static glibc binary rather than the usual musl-static approach. Co-Authored-By: Claude Sonnet 5 --- knowledge/guard-reference-inbox.mylog | 1 + 1 file changed, 1 insertion(+) diff --git a/knowledge/guard-reference-inbox.mylog b/knowledge/guard-reference-inbox.mylog index 1115c101..36040073 100644 --- a/knowledge/guard-reference-inbox.mylog +++ b/knowledge/guard-reference-inbox.mylog @@ -4,3 +4,4 @@ (reference-candidate (schema guard-reference-candidate/1) (status promoted) (recorded-at-unix 1788499446) (route ask-agent) (topic "stub-tasks-my-missing-paren-signature") (question "Як швидко й надійно виправити auto-generated tasks.my заглушки, що падають з unclosed-list?") (answer "Знайдено 8 разів того самого дня (2026-09-04) в 8 різних репо (OpenManus, yantra, radio-log, esp32-pendulum, vault-semantic-mcp, sanskritworld_texts, coreboot, wsm) -- усі мають ІДЕНТИЧНИЙ source-digest sha256:51fae8823e0... і однакову помилку: my-lisp --oracle-check повідомляє (code unclosed-list) (offset 579), бо auto-generated шаблон (той самий 'CREATED tasks.my for all repos that lacked one' пасс, wsl-nidana-bridge, той самий день) забув одну закриваючу дужку в кінці кожного файлу. Надійний, підтверджений 8 разів фікс: printf ')' >> tasks.my, потім my-lisp --oracle-check tasks.my для підтвердження outcome valid. Не універсальний фікс для БУДЬ-якого unclosed-list (див. automated-bulk-edit-paren-safety для ширшого класу) -- специфічно для цього одного шаблону з тим самим digest/offset.") (source "agent:pramana (Claude Sonnet 5) live investigation 2026-09-04, 8 independent confirmations") (evidence "ecosystem commit 862bce9 context; my-lisp commit 899d7b5; per-repo commits: chess-lisp-zero 614e3d4, wsm ea622f4, +6 more same day, all outcome error->valid")) (reference-candidate (schema guard-reference-candidate/1) (status promoted) (recorded-at-unix 1788585600) (route ask-agent) (topic "semantic-situation-and-ryad-contrast") (question "Як корінь РЯД- розмежовується з іншими юридичними термінами через семантичні ситуації, а не лише пошук графем?") (answer "Семантичний аудит ситуацій (Relation Track vs Lexeme Track) та сліпе кластерування виявили: РЯД- обирає структурні патерни попередньої специфікації для майбутньої поведінки (Pattern A: кабальний найм/позика, Pattern D: нормативний мир), але відсутній у разових передачах майна (Pattern B: купівля/дарування Софія № 23) та судовому стягненні (Pattern C: Звенигород № 2). Аудит контрприкладів показав чітких функціональних конкурентів: ОУСТАВ- (одностороннє веління), КРЕСТНОЕ ЦЕЛОВАНИЕ/РОТА (сакральне скріплення), операційні дієслова (разові дії).") (source "Darshana/Antigravity pair session 2026-09-05") (evidence "pravda commit 387d394; ecosystem commit e56dd09; docs/dictionary/RYAD-vs-DOGOVOR-vs-PAKT-CONTRAST.md Sections AA, BB")) (reference-candidate (schema guard-reference-candidate/1) (status promoted) (recorded-at-unix 1788865291) (route ask-agent) (topic "guix-swarm-node-onboarding") (question "Як правильно підняти нову swarm-node ноду через окремий Guix-профіль агента, і які реальні операційні пастки при цьому трапляються?") (answer "Живий onboarding wsl-pratyaksha-1 (2026-09-08) через окремий Guix-профіль виявив шість підтверджених операційних інваріантів. (1) `--connect` створює TCP/peer-звʼязок, але НЕ замінює протокольний `(join (capabilities ...) (roles ...))` -- до явного join нода може бути в `presence`, але не мати запису з ролями/capabilities в `members`. (2) `peer-welcome` доводить успішний handshake лише з ОДНИМ peer, не повну anti-entropy конвергенцію -- для цього потрібен окремий `(status)` з `(synced t)`. (3) На великому журналі свіжа нода тривалий час відповідає `(synced nil)`, застосовуючи пакети по 250 подій -- це спостережена startup backpressure, не зависання. (4) Systemd user-unit МУСИТЬ активувати Guix-профіль перед `exec`; сам факт, що target/release/swarm-node лежить поруч із Guix-профілем, НЕ доказ, що нода працює саме з Guix output. (5) Реальний виконуваний файл перевіряється через `/proc//exe`, який має вести в `/gnu/store/...` -- не лише текст ExecStart чи symlink профілю. (6) `sw`-клієнту треба передавати повну wire-форму `'(status)'`; голе `status` некоректне; сервер тримає зʼєднання відкритим після відповіді, клієнт має зупинити читання на збалансованій S-формі. Спостережена межа: деякі gossip-адреси можуть повертати Connection refused, поки локальні peers працюють і нода все одно досягає `(synced t)` -- це не можна подавати ані як повну справність усіх peers, ані як провал локальної ноди.") (source "evidence/investigation: agent:pratyaksha (Codex live-evidence worker) live onboarding 2026-09-08, owner-authorized; write-up/promotion into guard-reference: Claude Sonnet 5, same day -- pratyaksha's own comms-log messages (id a751487271a24c20, id cbb1f8dfc2444da8) claimed this candidate was already appended to this inbox file and oracle-check valid (153 forms), but live repo state at time of promotion showed no such content in this file or in guard-reference.wsm -- the original append was never pushed, likely lost on session disconnect; reconstructed here from the same underlying evidence, not re-investigated from scratch") (evidence "ecosystem/memory/pratyaksha.md; ecosystem/runs/2026-09-08-pratyaksha-node-onboard/README.md; comms-log.md entries a751487271a24c20 (2026-09-08T02:16, join evidence) and cbb1f8dfc2444da8 (2026-09-08T02:20, original unfulfilled review request)")) +(reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789255512) (route ask-agent) (topic "guix-cross-compilation-target-flag-scope") (question "Чому команда guix build --target= pkg спрацьовує, а guix install/guix shell --target= pkg -- ні, і як тоді фактично отримати крос-компілятор?") (answer "Живо перевірено 2026-09-13 при спробі крос-компілювати my-lisp-cli під aarch64-linux-gnu (для деплою на Android TV box, X96 Max Plus2, порт ADB 5555 відкритий у локальній мережі). guix install --target=... і guix shell --target=... обидва повертають (error: unrecognized option) -- прапор --target існує ЛИШЕ для guix build (і споріднених низькорівневих команд), не для install/package/shell, всупереч інтуїтивному очікуванню, що ці підкоманди приймають однакові прапори. Робочий шлях: guix build --target=aarch64-linux-gnu gcc-toolchain реально завантажує/будує крос-toolchain (bzip2, coreutils, gcc-14/16 substitutes підтверджено з bordeaux.guix.gnu.org) і друкує кінцевий /gnu/store/... шлях; потрібно використати той шлях напряму (напр. через CARGO_TARGET__LINKER=/bin/aarch64-linux-gnu-gcc), а не покладатися, що guix install/shell зробить крос-tools доступними на PATH. Окремо підтверджено: guix НЕ підтримує aarch64-linux-musl як cross-compilation target (лише aarch64-linux-gnu, glibc) -- guix build --list-targets це підтверджує; для деплою на Android (Bionic libc, не glibc) це означає потребу в повністю статичному glibc-білді (уникає Bionic dynamic linker mismatch, бо статичний бінарник не викликає системний linker), а не musl-static, як типово роблять для Linux-контейнерів.") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-directed (TV-tuner/Android-oracle experiment)") (evidence "командний вивід guix build/install/shell --target=... на цій WSL-машині (Ubuntu, guix 0cc8f41), сесія my-lisp-09; ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum з повним контекстом")) From 69726e0475b989063e76c5950b0b9b14aeba6ef6 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:27:21 +0300 Subject: [PATCH 18/26] fix(canon): observe stable callable identity through eq --- crates/my-lisp/src/eval/special_forms/core.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/my-lisp/src/eval/special_forms/core.rs b/crates/my-lisp/src/eval/special_forms/core.rs index 8c7d40a3..94501c6f 100644 --- a/crates/my-lisp/src/eval/special_forms/core.rs +++ b/crates/my-lisp/src/eval/special_forms/core.rs @@ -161,5 +161,13 @@ pub(crate) fn eq_values(left: Value, right: Value, span: Span) -> Result Rc::ptr_eq(left, right), + _ => left == right, + }; + Ok(Value::truth(same_identity)) } From 1c221397e1e8e7872554084f0027094197fb5ff3 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:56:25 +0300 Subject: [PATCH 19/26] knowledge: record guix aarch64 cross-bootstrap dead end (config.sub in mesboot) guix build --target=aarch64-linux-gnu gcc-toolchain fails at the gawk-mesboot-3.1.8 configure phase: config.sub does not recognize aarch64. Confirmed reproducible before AND after guix pull (0cc8f41 -> a39872ef5) -- the frozen mesboot bootstrap tarball is not updated by channel pulls, since it exists specifically to stay frozen for bootstrap reproducibility. Practical implication: full-source-bootstrap Guix cross-compilation to aarch64-linux-gnu is not viable on this host without patching mesboot or sourcing a prebuilt substitute elsewhere. Co-Authored-By: Claude Sonnet 5 --- knowledge/guard-reference-inbox.mylog | 1 + 1 file changed, 1 insertion(+) diff --git a/knowledge/guard-reference-inbox.mylog b/knowledge/guard-reference-inbox.mylog index 36040073..9da61166 100644 --- a/knowledge/guard-reference-inbox.mylog +++ b/knowledge/guard-reference-inbox.mylog @@ -5,3 +5,4 @@ (reference-candidate (schema guard-reference-candidate/1) (status promoted) (recorded-at-unix 1788585600) (route ask-agent) (topic "semantic-situation-and-ryad-contrast") (question "Як корінь РЯД- розмежовується з іншими юридичними термінами через семантичні ситуації, а не лише пошук графем?") (answer "Семантичний аудит ситуацій (Relation Track vs Lexeme Track) та сліпе кластерування виявили: РЯД- обирає структурні патерни попередньої специфікації для майбутньої поведінки (Pattern A: кабальний найм/позика, Pattern D: нормативний мир), але відсутній у разових передачах майна (Pattern B: купівля/дарування Софія № 23) та судовому стягненні (Pattern C: Звенигород № 2). Аудит контрприкладів показав чітких функціональних конкурентів: ОУСТАВ- (одностороннє веління), КРЕСТНОЕ ЦЕЛОВАНИЕ/РОТА (сакральне скріплення), операційні дієслова (разові дії).") (source "Darshana/Antigravity pair session 2026-09-05") (evidence "pravda commit 387d394; ecosystem commit e56dd09; docs/dictionary/RYAD-vs-DOGOVOR-vs-PAKT-CONTRAST.md Sections AA, BB")) (reference-candidate (schema guard-reference-candidate/1) (status promoted) (recorded-at-unix 1788865291) (route ask-agent) (topic "guix-swarm-node-onboarding") (question "Як правильно підняти нову swarm-node ноду через окремий Guix-профіль агента, і які реальні операційні пастки при цьому трапляються?") (answer "Живий onboarding wsl-pratyaksha-1 (2026-09-08) через окремий Guix-профіль виявив шість підтверджених операційних інваріантів. (1) `--connect` створює TCP/peer-звʼязок, але НЕ замінює протокольний `(join (capabilities ...) (roles ...))` -- до явного join нода може бути в `presence`, але не мати запису з ролями/capabilities в `members`. (2) `peer-welcome` доводить успішний handshake лише з ОДНИМ peer, не повну anti-entropy конвергенцію -- для цього потрібен окремий `(status)` з `(synced t)`. (3) На великому журналі свіжа нода тривалий час відповідає `(synced nil)`, застосовуючи пакети по 250 подій -- це спостережена startup backpressure, не зависання. (4) Systemd user-unit МУСИТЬ активувати Guix-профіль перед `exec`; сам факт, що target/release/swarm-node лежить поруч із Guix-профілем, НЕ доказ, що нода працює саме з Guix output. (5) Реальний виконуваний файл перевіряється через `/proc//exe`, який має вести в `/gnu/store/...` -- не лише текст ExecStart чи symlink профілю. (6) `sw`-клієнту треба передавати повну wire-форму `'(status)'`; голе `status` некоректне; сервер тримає зʼєднання відкритим після відповіді, клієнт має зупинити читання на збалансованій S-формі. Спостережена межа: деякі gossip-адреси можуть повертати Connection refused, поки локальні peers працюють і нода все одно досягає `(synced t)` -- це не можна подавати ані як повну справність усіх peers, ані як провал локальної ноди.") (source "evidence/investigation: agent:pratyaksha (Codex live-evidence worker) live onboarding 2026-09-08, owner-authorized; write-up/promotion into guard-reference: Claude Sonnet 5, same day -- pratyaksha's own comms-log messages (id a751487271a24c20, id cbb1f8dfc2444da8) claimed this candidate was already appended to this inbox file and oracle-check valid (153 forms), but live repo state at time of promotion showed no such content in this file or in guard-reference.wsm -- the original append was never pushed, likely lost on session disconnect; reconstructed here from the same underlying evidence, not re-investigated from scratch") (evidence "ecosystem/memory/pratyaksha.md; ecosystem/runs/2026-09-08-pratyaksha-node-onboard/README.md; comms-log.md entries a751487271a24c20 (2026-09-08T02:16, join evidence) and cbb1f8dfc2444da8 (2026-09-08T02:20, original unfulfilled review request)")) (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789255512) (route ask-agent) (topic "guix-cross-compilation-target-flag-scope") (question "Чому команда guix build --target= pkg спрацьовує, а guix install/guix shell --target= pkg -- ні, і як тоді фактично отримати крос-компілятор?") (answer "Живо перевірено 2026-09-13 при спробі крос-компілювати my-lisp-cli під aarch64-linux-gnu (для деплою на Android TV box, X96 Max Plus2, порт ADB 5555 відкритий у локальній мережі). guix install --target=... і guix shell --target=... обидва повертають (error: unrecognized option) -- прапор --target існує ЛИШЕ для guix build (і споріднених низькорівневих команд), не для install/package/shell, всупереч інтуїтивному очікуванню, що ці підкоманди приймають однакові прапори. Робочий шлях: guix build --target=aarch64-linux-gnu gcc-toolchain реально завантажує/будує крос-toolchain (bzip2, coreutils, gcc-14/16 substitutes підтверджено з bordeaux.guix.gnu.org) і друкує кінцевий /gnu/store/... шлях; потрібно використати той шлях напряму (напр. через CARGO_TARGET__LINKER=/bin/aarch64-linux-gnu-gcc), а не покладатися, що guix install/shell зробить крос-tools доступними на PATH. Окремо підтверджено: guix НЕ підтримує aarch64-linux-musl як cross-compilation target (лише aarch64-linux-gnu, glibc) -- guix build --list-targets це підтверджує; для деплою на Android (Bionic libc, не glibc) це означає потребу в повністю статичному glibc-білді (уникає Bionic dynamic linker mismatch, бо статичний бінарник не викликає системний linker), а не musl-static, як типово роблять для Linux-контейнерів.") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-directed (TV-tuner/Android-oracle experiment)") (evidence "командний вивід guix build/install/shell --target=... на цій WSL-машині (Ubuntu, guix 0cc8f41), сесія my-lisp-09; ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum з повним контекстом")) +(reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789257374) (route ask-agent) (topic "guix-aarch64-cross-bootstrap-config-sub-dead-end") (question "Чи можна на цьому Guix-хості (WSL, x86_64) отримати aarch64-linux-gnu крос-toolchain для деплою на ARM-пристрій, і чи вирішує це guix pull?") (answer "Ні, підтверджено двічі (до і після guix pull, 2026-09-13): guix build --target=aarch64-linux-gnu gcc-toolchain падає на РАННІЙ стадії bootstrap з (Invalid configuration aarch64-linux-gnu: machine aarch64 not recognized) під час configure gawk-mesboot-3.1.8 (частина 'full source bootstrap' ланцюга Guix, навмисно старий/заморожений пакет для reproducibility). guix pull оновив сам Guix (0cc8f41 -> a39872ef5) успішно, але це НЕ виправило проблему -- config.sub, вбудований у gawk-mesboot's власний джерельний tarball, лишається старим незалежно від версії Guix, бо цей конкретний мезбут-пакет свідомо заморожений заради відтворюваності бутстрапу, а не оновлюється разом з рештою дистрибуції. Практичний висновок: на цьому хості (і ймовірно на будь-якому стандартному Guix без спеціального патчу mesboot chain) aarch64-linux-gnu через from-scratch bootstrap не збирається; або (a) знайти вже готовий /gnu/store substitute для aarch64 gcc-toolchain з іншого джерела/каналу, (b) обійти mesboot повністю через сторонній prebuilt крос-toolchain (напр. Android NDK для aarch64-linux-android, не gnu), або (c) патчити/оновити config.sub у самому mesboot-пакеті (ризиковано, ламає bootstrap-reproducibility гарантії, не пробувано).") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-directed") (evidence "guix build --target=aarch64-linux-gnu gcc-toolchain, двічі відтворено (guix 0cc8f41 і a39872ef5), обидва рази ідентична помилка на gawk-mesboot-3.1.8; ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum")) From 3de04e2651da3690368f51dc60031dbbb34fad33 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:05:15 +0300 Subject: [PATCH 20/26] feat(surface): generate deterministic Ukrainian audit inventory --- scripts/generate-uk-surface-audit.my | 204 +++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 scripts/generate-uk-surface-audit.my diff --git a/scripts/generate-uk-surface-audit.my b/scripts/generate-uk-surface-audit.my new file mode 100644 index 00000000..876f0eda --- /dev/null +++ b/scripts/generate-uk-surface-audit.my @@ -0,0 +1,204 @@ +; scripts/generate-uk-surface-audit.my — generated Ukrainian surface inventory +; for my-lisp#85. This is an AUDIT PROJECTION, never semantic authority. +; +; Authority path: +; semantic-registry.wsm +; -> scripts/generate-function-table.my +; -> lib/generated/function-table.wsm +; -> this audit projection +; +; Candidate evidence path: +; lib/surface/український-профіль-джерела.всм +; Candidate spellings remain proposals. This generator MUST NOT promote them +; into the authoritative `full-uk` column by itself. +; +; Output: +; lib/generated/uk-surface-audit.wsm +; +; Usage from repo root: +; cargo run -p my-lisp-cli --bin my-lisp -- scripts/generate-uk-surface-audit.my + +(def str+ + (lambda args (reduce (lambda (acc s) (string-append acc s)) "" args))) + +(def pad4 + (lambda (n) + (let ((s (number->string n))) + (cond + ((eq (string-length s) 1) (string-append "000" s)) + ((eq (string-length s) 2) (string-append "00" s)) + ((eq (string-length s) 3) (string-append "0" s)) + (t s))))) + +(def second (lambda (xs) (car (cdr xs)))) +(def third (lambda (xs) (car (cdr (cdr xs))))) +(def fourth (lambda (xs) (car (cdr (cdr (cdr xs)))))) +(def fifth (lambda (xs) (car (cdr (cdr (cdr (cdr xs))))))) +(def sixth (lambda (xs) (car (cdr (cdr (cdr (cdr (cdr xs)))))))) +(def seventh (lambda (xs) (car (cdr (cdr (cdr (cdr (cdr (cdr xs))))))))) +(def eighth (lambda (xs) (car (cdr (cdr (cdr (cdr (cdr (cdr (cdr xs)))))))))) +(def ninth (lambda (xs) (car (cdr (cdr (cdr (cdr (cdr (cdr (cdr (cdr xs))))))))))) + +(def join-newline-onto + (lambda (strings acc) + (cond + ((atom strings) acc) + ((eq acc "") (join-newline-onto (cdr strings) (car strings))) + (t (join-newline-onto (cdr strings) (str+ acc "\n" (car strings))))))) +(def join-newline (lambda (strings) (join-newline-onto strings ""))) + +; Function-table rows have the generated schema: +; (id formal (uk word status) (full-uk word status) (en word status) +; (sa word status) (sym word status) primary-status authority) +(def ft-form (car (read-all (read-file "lib/generated/function-table.wsm")))) +(def ft-rows (cdr ft-form)) + +(def profile-form + (car (read-all (read-file "lib/surface/український-профіль-джерела.всм")))) + +(def find-section + (lambda (name sections) + (cond + ((atom sections) (quote ())) + ((and (not (atom (car sections))) (eq (car (car sections)) name)) + (car sections)) + (t (find-section name (cdr sections)))))) + +(def candidate-section (find-section (quote назви) (cdr profile-form))) +(def candidate-rows + (cond + ((atom candidate-section) (quote ())) + (t (cdr candidate-section)))) + +(def find-candidate-row + (lambda (sid rows) + (cond + ((atom rows) (quote ())) + ((eq (car (car rows)) sid) (car rows)) + (t (find-candidate-row sid (cdr rows)))))) + +(def surface-word (lambda (surface) (second surface))) +(def surface-status (lambda (surface) (third surface))) + +(def missing-surface? + (lambda (surface) + (or (eq (surface-status surface) (quote missing)) + (eq (surface-word surface) (quote —))))) + +; Classification here is intentionally conservative. The audit may report a +; candidate, but only an explicit later review may classify it as compact, +; ambiguous, etc. We therefore never infer linguistic facts from spelling. +(def audit-class + (lambda (uk candidate) + (cond + ((eq (surface-status uk) (quote compatibility-only)) (quote compatibility-only)) + ((missing-surface? uk) (quote needs-research)) + ((atom candidate) (quote needs-research)) + ((eq (second candidate) (third candidate)) (quote full)) + (t (quote needs-research))))) + +(def candidate-full-word + (lambda (candidate) + (cond ((atom candidate) (quote —)) + (t (third candidate))))) + +(def candidate-evidence-status + (lambda (candidate) + (cond ((atom candidate) (quote no-staging-evidence)) + (t (fourth candidate))))) + +; `candidate-compact-uk` belongs to #86/#89. #85 must expose the empty slot, +; not invent abbreviations while performing an inventory. +(def candidate-compact-word (lambda () (quote —))) + +(def ambiguity-status + (lambda (candidate) + (cond + ((atom candidate) (quote needs-research)) + (t (quote not-yet-assessed))))) + +(def render-row + (lambda (row) + (let* ((sid (car row)) + (uk (third row)) + (full-authority (fourth row)) + (en (fifth row)) + (sa (sixth row)) + (sym (seventh row)) + (primary (eighth row)) + (candidate (find-candidate-row sid candidate-rows)) + (class (audit-class uk candidate)) + (candidate-full (candidate-full-word candidate)) + (candidate-status (candidate-evidence-status candidate)) + (ambiguity (ambiguity-status candidate))) + (str+ + " (row " (pad4 sid) + " (current-uk " (write-to-string (surface-word uk)) " " + (write-to-string (surface-status uk)) ")" + " (authoritative-full-uk " (write-to-string (surface-word full-authority)) " " + (write-to-string (surface-status full-authority)) ")" + " (en " (write-to-string (surface-word en)) " " + (write-to-string (surface-status en)) ")" + " (sa " (write-to-string (surface-word sa)) " " + (write-to-string (surface-status sa)) ")" + " (sym " (write-to-string (surface-word sym)) " " + (write-to-string (surface-status sym)) ")" + " (primary-status " (write-to-string primary) ")" + " (class " (write-to-string class) ")" + " (candidate-full-uk " (write-to-string candidate-full) ")" + " (candidate-full-status " (write-to-string candidate-status) ")" + " (candidate-compact-uk " (write-to-string (candidate-compact-word)) ")" + " (ambiguity " (write-to-string ambiguity) ")" + " (evidence generated-function-table staging-profile))")))) + +(def count-class + (lambda (wanted rows acc) + (cond + ((atom rows) acc) + (t + (let* ((sid (car (car rows))) + (uk (third (car rows))) + (candidate (find-candidate-row sid candidate-rows)) + (class (audit-class uk candidate))) + (count-class wanted (cdr rows) + (cond ((eq class wanted) (+ acc 1)) + (t acc)))))))) + +(def count-candidates + (lambda (rows acc) + (cond + ((atom rows) acc) + (t + (count-candidates (cdr rows) + (cond ((atom (find-candidate-row (car (car rows)) candidate-rows)) acc) + (t (+ acc 1)))))))) + +(def total (length ft-rows)) +(def full-count (count-class (quote full) ft-rows 0)) +(def compatibility-count (count-class (quote compatibility-only) ft-rows 0)) +(def needs-research-count (count-class (quote needs-research) ft-rows 0)) +(def candidate-count (count-candidates ft-rows 0)) + +(def header + (list + "; GENERATED — DO NOT EDIT BY HAND" + "; Semantic authority: lib/surface/semantic-registry.wsm via lib/generated/function-table.wsm" + "; Candidate evidence only: lib/surface/український-профіль-джерела.всм" + "; Generator: scripts/generate-uk-surface-audit.my (my-lisp#85)" + "; candidate-full-uk is NOT automatically promoted into authoritative full-uk" + "" + "(uk-surface-audit/1" + (str+ " (summary (total " (number->string total) ")" + " (full " (number->string full-count) ")" + " (already-compact 0)" + " (ambiguous 0)" + " (needs-research " (number->string needs-research-count) ")" + " (compatibility-only " (number->string compatibility-count) ")" + " (staging-evidence " (number->string candidate-count) "))") + " (rows")) + +(def body (join-newline (append header (map render-row ft-rows)))) +(def output (str+ body "\n )\n)\n")) + +(write-file "lib/generated/uk-surface-audit.wsm" output) +(print (str+ "uk-surface-audit: " (number->string total) " identities written")) From fb1c231da4d124b7ec819f55c7d54d8aa81d1d0c Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:07:11 +0300 Subject: [PATCH 21/26] test(source): require .lisp canonical extension in Ukrainian profile --- .../tests/canonical_source_extension.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 crates/my-lisp/tests/canonical_source_extension.rs diff --git a/crates/my-lisp/tests/canonical_source_extension.rs b/crates/my-lisp/tests/canonical_source_extension.rs new file mode 100644 index 00000000..6b210c19 --- /dev/null +++ b/crates/my-lisp/tests/canonical_source_extension.rs @@ -0,0 +1,23 @@ +use std::fs; +use std::path::PathBuf; + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +#[test] +fn ukrainian_staging_profile_does_not_claim_a_legacy_extension_as_canonical() { + let profile = fs::read_to_string( + repo_root().join("lib/surface/український-профіль-джерела.всм"), + ) + .expect("Ukrainian staging profile must be readable"); + + assert!( + profile.contains("(основне-розширення \".lisp\")"), + "#81 declares .lisp the canonical source extension; the Ukrainian staging profile must track that policy" + ); + assert!( + !profile.contains("(основне-розширення \".всм\")"), + "legacy .всм may remain a compatibility alias, but must not be labeled the primary/canonical extension" + ); +} From 9147b5651a0cfaca607d43fc61d60f6328731344 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:16:37 +0300 Subject: [PATCH 22/26] fix(source): align Ukrainian staging profile with canonical .lisp extension --- ...65\321\200\320\265\320\273\320\260.\320\262\321\201\320\274" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/lib/surface/\321\203\320\272\321\200\320\260\321\227\320\275\321\201\321\214\320\272\320\270\320\271-\320\277\321\200\320\276\321\204\321\226\320\273\321\214-\320\264\320\266\320\265\321\200\320\265\320\273\320\260.\320\262\321\201\320\274" "b/lib/surface/\321\203\320\272\321\200\320\260\321\227\320\275\321\201\321\214\320\272\320\270\320\271-\320\277\321\200\320\276\321\204\321\226\320\273\321\214-\320\264\320\266\320\265\321\200\320\265\320\273\320\260.\320\262\321\201\320\274" index 505e22cb..941a3fab 100644 --- "a/lib/surface/\321\203\320\272\321\200\320\260\321\227\320\275\321\201\321\214\320\272\320\270\320\271-\320\277\321\200\320\276\321\204\321\226\320\273\321\214-\320\264\320\266\320\265\321\200\320\265\320\273\320\260.\320\262\321\201\320\274" +++ "b/lib/surface/\321\203\320\272\321\200\320\260\321\227\320\275\321\201\321\214\320\272\320\270\320\271-\320\277\321\200\320\276\321\204\321\226\320\273\321\214-\320\264\320\266\320\265\321\200\320\265\320\273\320\260.\320\262\321\201\320\274" @@ -22,7 +22,7 @@ (політика (основна-людська-мова українська) - (основне-розширення ".всм") + (основне-розширення ".lisp") (повні-слова t) (програмістські-скорочення заборонені-за-замовчуванням) (кодування utf-8-nfc) From 74f5c0c92d4c0ed1c4de0c672143c40d385ad7ff Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:19:03 +0300 Subject: [PATCH 23/26] knowledge: record successful Android TV-box oracle deployment Live-verified 2026-09-13: my-lisp --tcp oracle deployed and confirmed working on an Android TV box (X96 Max Plus2), not just the usual x86_64 droplet. Key finding: the box's Android userspace is 32-bit only (armeabi-v7a) despite a 64-bit-capable SoC -- an aarch64 build fails with a misleading not-executable error unrelated to noexec/ SELinux; armv7-linux-androideabi via NDK r30 fixed it immediately. Full TCP oracle-check round-trip confirmed through adb forward, and a simple shell watchdog loop confirmed to actually restart the process after kill -9 within 2s (new server-generation observed). Reboot persistence intentionally left unsolved (would need Termux:Boot APK install, not done without separate owner permission). Co-Authored-By: Claude Sonnet 5 --- knowledge/guard-reference-inbox.mylog | 1 + 1 file changed, 1 insertion(+) diff --git a/knowledge/guard-reference-inbox.mylog b/knowledge/guard-reference-inbox.mylog index 9da61166..ff53de92 100644 --- a/knowledge/guard-reference-inbox.mylog +++ b/knowledge/guard-reference-inbox.mylog @@ -6,3 +6,4 @@ (reference-candidate (schema guard-reference-candidate/1) (status promoted) (recorded-at-unix 1788865291) (route ask-agent) (topic "guix-swarm-node-onboarding") (question "Як правильно підняти нову swarm-node ноду через окремий Guix-профіль агента, і які реальні операційні пастки при цьому трапляються?") (answer "Живий onboarding wsl-pratyaksha-1 (2026-09-08) через окремий Guix-профіль виявив шість підтверджених операційних інваріантів. (1) `--connect` створює TCP/peer-звʼязок, але НЕ замінює протокольний `(join (capabilities ...) (roles ...))` -- до явного join нода може бути в `presence`, але не мати запису з ролями/capabilities в `members`. (2) `peer-welcome` доводить успішний handshake лише з ОДНИМ peer, не повну anti-entropy конвергенцію -- для цього потрібен окремий `(status)` з `(synced t)`. (3) На великому журналі свіжа нода тривалий час відповідає `(synced nil)`, застосовуючи пакети по 250 подій -- це спостережена startup backpressure, не зависання. (4) Systemd user-unit МУСИТЬ активувати Guix-профіль перед `exec`; сам факт, що target/release/swarm-node лежить поруч із Guix-профілем, НЕ доказ, що нода працює саме з Guix output. (5) Реальний виконуваний файл перевіряється через `/proc//exe`, який має вести в `/gnu/store/...` -- не лише текст ExecStart чи symlink профілю. (6) `sw`-клієнту треба передавати повну wire-форму `'(status)'`; голе `status` некоректне; сервер тримає зʼєднання відкритим після відповіді, клієнт має зупинити читання на збалансованій S-формі. Спостережена межа: деякі gossip-адреси можуть повертати Connection refused, поки локальні peers працюють і нода все одно досягає `(synced t)` -- це не можна подавати ані як повну справність усіх peers, ані як провал локальної ноди.") (source "evidence/investigation: agent:pratyaksha (Codex live-evidence worker) live onboarding 2026-09-08, owner-authorized; write-up/promotion into guard-reference: Claude Sonnet 5, same day -- pratyaksha's own comms-log messages (id a751487271a24c20, id cbb1f8dfc2444da8) claimed this candidate was already appended to this inbox file and oracle-check valid (153 forms), but live repo state at time of promotion showed no such content in this file or in guard-reference.wsm -- the original append was never pushed, likely lost on session disconnect; reconstructed here from the same underlying evidence, not re-investigated from scratch") (evidence "ecosystem/memory/pratyaksha.md; ecosystem/runs/2026-09-08-pratyaksha-node-onboard/README.md; comms-log.md entries a751487271a24c20 (2026-09-08T02:16, join evidence) and cbb1f8dfc2444da8 (2026-09-08T02:20, original unfulfilled review request)")) (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789255512) (route ask-agent) (topic "guix-cross-compilation-target-flag-scope") (question "Чому команда guix build --target= pkg спрацьовує, а guix install/guix shell --target= pkg -- ні, і як тоді фактично отримати крос-компілятор?") (answer "Живо перевірено 2026-09-13 при спробі крос-компілювати my-lisp-cli під aarch64-linux-gnu (для деплою на Android TV box, X96 Max Plus2, порт ADB 5555 відкритий у локальній мережі). guix install --target=... і guix shell --target=... обидва повертають (error: unrecognized option) -- прапор --target існує ЛИШЕ для guix build (і споріднених низькорівневих команд), не для install/package/shell, всупереч інтуїтивному очікуванню, що ці підкоманди приймають однакові прапори. Робочий шлях: guix build --target=aarch64-linux-gnu gcc-toolchain реально завантажує/будує крос-toolchain (bzip2, coreutils, gcc-14/16 substitutes підтверджено з bordeaux.guix.gnu.org) і друкує кінцевий /gnu/store/... шлях; потрібно використати той шлях напряму (напр. через CARGO_TARGET__LINKER=/bin/aarch64-linux-gnu-gcc), а не покладатися, що guix install/shell зробить крос-tools доступними на PATH. Окремо підтверджено: guix НЕ підтримує aarch64-linux-musl як cross-compilation target (лише aarch64-linux-gnu, glibc) -- guix build --list-targets це підтверджує; для деплою на Android (Bionic libc, не glibc) це означає потребу в повністю статичному glibc-білді (уникає Bionic dynamic linker mismatch, бо статичний бінарник не викликає системний linker), а не musl-static, як типово роблять для Linux-контейнерів.") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-directed (TV-tuner/Android-oracle experiment)") (evidence "командний вивід guix build/install/shell --target=... на цій WSL-машині (Ubuntu, guix 0cc8f41), сесія my-lisp-09; ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum з повним контекстом")) (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789257374) (route ask-agent) (topic "guix-aarch64-cross-bootstrap-config-sub-dead-end") (question "Чи можна на цьому Guix-хості (WSL, x86_64) отримати aarch64-linux-gnu крос-toolchain для деплою на ARM-пристрій, і чи вирішує це guix pull?") (answer "Ні, підтверджено двічі (до і після guix pull, 2026-09-13): guix build --target=aarch64-linux-gnu gcc-toolchain падає на РАННІЙ стадії bootstrap з (Invalid configuration aarch64-linux-gnu: machine aarch64 not recognized) під час configure gawk-mesboot-3.1.8 (частина 'full source bootstrap' ланцюга Guix, навмисно старий/заморожений пакет для reproducibility). guix pull оновив сам Guix (0cc8f41 -> a39872ef5) успішно, але це НЕ виправило проблему -- config.sub, вбудований у gawk-mesboot's власний джерельний tarball, лишається старим незалежно від версії Guix, бо цей конкретний мезбут-пакет свідомо заморожений заради відтворюваності бутстрапу, а не оновлюється разом з рештою дистрибуції. Практичний висновок: на цьому хості (і ймовірно на будь-якому стандартному Guix без спеціального патчу mesboot chain) aarch64-linux-gnu через from-scratch bootstrap не збирається; або (a) знайти вже готовий /gnu/store substitute для aarch64 gcc-toolchain з іншого джерела/каналу, (b) обійти mesboot повністю через сторонній prebuilt крос-toolchain (напр. Android NDK для aarch64-linux-android, не gnu), або (c) патчити/оновити config.sub у самому mesboot-пакеті (ризиковано, ламає bootstrap-reproducibility гарантії, не пробувано).") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-directed") (evidence "guix build --target=aarch64-linux-gnu gcc-toolchain, двічі відтворено (guix 0cc8f41 і a39872ef5), обидва рази ідентична помилка на gawk-mesboot-3.1.8; ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum")) +(reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789258732) (route ask-agent) (topic "android-tv-box-oracle-deployment") (question "Як розгорнути my-lisp --tcp oracle на завжди-увімкненому Android TV box (не x86_64 droplet), і на що звертати увагу?") (answer "Живо задеплоєно і підтверджено 2026-09-13 на X96 Max Plus2 (Amlogic S905X3, Android 9/SDK 28, ARM64 SoC але 32-bit-only userspace -- getprop ro.product.cpu.abilist показує лише armeabi-v7a/armeabi, немає /system/bin/linker64 взагалі). Ключова пастка: перша спроба з aarch64-linux-android провалилась з (not executable: 64-bit ELF file) -- це НЕ noexec mount (/data не мав noexec, SELinux Permissive), а справжня причина архітектурна: система суто 32-бітна, попри 64-бітний SoC (типово для дешевих китайських TV-box). Фікс: rustup target add armv7-linux-androideabi, лінкер armv7a-linux-androideabi28-clang з Android NDK r30, збірка одразу спрацювала (ELF 32-bit LSB pie executable, ARM, interpreter /system/bin/linker). Деплой через adb push у /data/local/tmp (root-adb, стандартно на цьому пристрої, su доступний). --tcp сервер у коді (crates/my-lisp-cli/src/tcp_repl.rs) жорстко прив'язаний до Ipv4Addr::LOCALHOST, без --bind прапора -- свідоме рішення проєкту, не баг; доступ ззовні лише через adb forward tcp::9999 з машини з живим adb-з'єднанням, не пряме мережеве підключення інших peer. Стійкість до крашів (не до перезавантаження пристрою): простий shell while-true watchdog-цикл у /data/local/tmp/oracle-watchdog.sh, живо підтверджено -- kill -9 процесу призвів до реального автоматичного рестарту за 2с з новим PID, oracle.log зафіксував подію, TCP-запит через adb forward після рестарту пройшов успішно (новий server-generation). Reboot-persistence НЕ вирішено: пристрій має Termux встановлений, але без Termux:Boot addon (був би найпростіший шлях), встановлення додаткового APK на робочому пристрої власника свідомо не зроблено без окремого дозволу.") (source "agent:my-lisp-09 (Claude Sonnet 5) live deployment 2026-09-13, owner-directed (TV-tuner oracle experiment continuation)") (evidence "живий TCP round-trip через adb forward tcp:19999:9999 до X96Max_Plus2_T (192.168.110.55:5555), (op oracle-check) повернув коректний oracle-result двічі (до і після kill -9 + watchdog-рестарту, різні server-generation); ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum")) From 645e8a578e341f4f1c9cc68b66bd3253f5869d09 Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:35:21 +0300 Subject: [PATCH 24/26] knowledge: record Termux:Boot adb-install stopped-state gotcha, fix confirmed live Two real device reboots (not simulated): first reboot after installing termux-boot via adb and writing ~/.termux/boot/start-oracle.sh produced zero autostart -- Android keeps never-manually-opened apps in a stopped state that blocks even correctly-registered BOOT_COMPLETED receivers. Fixed via one-time on both com.termux and com.termux.boot launchable activities. Second real reboot confirmed the oracle auto-starts under the correct Termux uid with no manual intervention, and answers a real TCP oracle-check round-trip immediately after boot. Co-Authored-By: Claude Sonnet 5 --- knowledge/guard-reference-inbox.mylog | 1 + 1 file changed, 1 insertion(+) diff --git a/knowledge/guard-reference-inbox.mylog b/knowledge/guard-reference-inbox.mylog index ff53de92..e5d694d1 100644 --- a/knowledge/guard-reference-inbox.mylog +++ b/knowledge/guard-reference-inbox.mylog @@ -7,3 +7,4 @@ (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789255512) (route ask-agent) (topic "guix-cross-compilation-target-flag-scope") (question "Чому команда guix build --target= pkg спрацьовує, а guix install/guix shell --target= pkg -- ні, і як тоді фактично отримати крос-компілятор?") (answer "Живо перевірено 2026-09-13 при спробі крос-компілювати my-lisp-cli під aarch64-linux-gnu (для деплою на Android TV box, X96 Max Plus2, порт ADB 5555 відкритий у локальній мережі). guix install --target=... і guix shell --target=... обидва повертають (error: unrecognized option) -- прапор --target існує ЛИШЕ для guix build (і споріднених низькорівневих команд), не для install/package/shell, всупереч інтуїтивному очікуванню, що ці підкоманди приймають однакові прапори. Робочий шлях: guix build --target=aarch64-linux-gnu gcc-toolchain реально завантажує/будує крос-toolchain (bzip2, coreutils, gcc-14/16 substitutes підтверджено з bordeaux.guix.gnu.org) і друкує кінцевий /gnu/store/... шлях; потрібно використати той шлях напряму (напр. через CARGO_TARGET__LINKER=/bin/aarch64-linux-gnu-gcc), а не покладатися, що guix install/shell зробить крос-tools доступними на PATH. Окремо підтверджено: guix НЕ підтримує aarch64-linux-musl як cross-compilation target (лише aarch64-linux-gnu, glibc) -- guix build --list-targets це підтверджує; для деплою на Android (Bionic libc, не glibc) це означає потребу в повністю статичному glibc-білді (уникає Bionic dynamic linker mismatch, бо статичний бінарник не викликає системний linker), а не musl-static, як типово роблять для Linux-контейнерів.") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-directed (TV-tuner/Android-oracle experiment)") (evidence "командний вивід guix build/install/shell --target=... на цій WSL-машині (Ubuntu, guix 0cc8f41), сесія my-lisp-09; ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum з повним контекстом")) (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789257374) (route ask-agent) (topic "guix-aarch64-cross-bootstrap-config-sub-dead-end") (question "Чи можна на цьому Guix-хості (WSL, x86_64) отримати aarch64-linux-gnu крос-toolchain для деплою на ARM-пристрій, і чи вирішує це guix pull?") (answer "Ні, підтверджено двічі (до і після guix pull, 2026-09-13): guix build --target=aarch64-linux-gnu gcc-toolchain падає на РАННІЙ стадії bootstrap з (Invalid configuration aarch64-linux-gnu: machine aarch64 not recognized) під час configure gawk-mesboot-3.1.8 (частина 'full source bootstrap' ланцюга Guix, навмисно старий/заморожений пакет для reproducibility). guix pull оновив сам Guix (0cc8f41 -> a39872ef5) успішно, але це НЕ виправило проблему -- config.sub, вбудований у gawk-mesboot's власний джерельний tarball, лишається старим незалежно від версії Guix, бо цей конкретний мезбут-пакет свідомо заморожений заради відтворюваності бутстрапу, а не оновлюється разом з рештою дистрибуції. Практичний висновок: на цьому хості (і ймовірно на будь-якому стандартному Guix без спеціального патчу mesboot chain) aarch64-linux-gnu через from-scratch bootstrap не збирається; або (a) знайти вже готовий /gnu/store substitute для aarch64 gcc-toolchain з іншого джерела/каналу, (b) обійти mesboot повністю через сторонній prebuilt крос-toolchain (напр. Android NDK для aarch64-linux-android, не gnu), або (c) патчити/оновити config.sub у самому mesboot-пакеті (ризиковано, ламає bootstrap-reproducibility гарантії, не пробувано).") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-directed") (evidence "guix build --target=aarch64-linux-gnu gcc-toolchain, двічі відтворено (guix 0cc8f41 і a39872ef5), обидва рази ідентична помилка на gawk-mesboot-3.1.8; ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum")) (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789258732) (route ask-agent) (topic "android-tv-box-oracle-deployment") (question "Як розгорнути my-lisp --tcp oracle на завжди-увімкненому Android TV box (не x86_64 droplet), і на що звертати увагу?") (answer "Живо задеплоєно і підтверджено 2026-09-13 на X96 Max Plus2 (Amlogic S905X3, Android 9/SDK 28, ARM64 SoC але 32-bit-only userspace -- getprop ro.product.cpu.abilist показує лише armeabi-v7a/armeabi, немає /system/bin/linker64 взагалі). Ключова пастка: перша спроба з aarch64-linux-android провалилась з (not executable: 64-bit ELF file) -- це НЕ noexec mount (/data не мав noexec, SELinux Permissive), а справжня причина архітектурна: система суто 32-бітна, попри 64-бітний SoC (типово для дешевих китайських TV-box). Фікс: rustup target add armv7-linux-androideabi, лінкер armv7a-linux-androideabi28-clang з Android NDK r30, збірка одразу спрацювала (ELF 32-bit LSB pie executable, ARM, interpreter /system/bin/linker). Деплой через adb push у /data/local/tmp (root-adb, стандартно на цьому пристрої, su доступний). --tcp сервер у коді (crates/my-lisp-cli/src/tcp_repl.rs) жорстко прив'язаний до Ipv4Addr::LOCALHOST, без --bind прапора -- свідоме рішення проєкту, не баг; доступ ззовні лише через adb forward tcp::9999 з машини з живим adb-з'єднанням, не пряме мережеве підключення інших peer. Стійкість до крашів (не до перезавантаження пристрою): простий shell while-true watchdog-цикл у /data/local/tmp/oracle-watchdog.sh, живо підтверджено -- kill -9 процесу призвів до реального автоматичного рестарту за 2с з новим PID, oracle.log зафіксував подію, TCP-запит через adb forward після рестарту пройшов успішно (новий server-generation). Reboot-persistence НЕ вирішено: пристрій має Termux встановлений, але без Termux:Boot addon (був би найпростіший шлях), встановлення додаткового APK на робочому пристрої власника свідомо не зроблено без окремого дозволу.") (source "agent:my-lisp-09 (Claude Sonnet 5) live deployment 2026-09-13, owner-directed (TV-tuner oracle experiment continuation)") (evidence "живий TCP round-trip через adb forward tcp:19999:9999 до X96Max_Plus2_T (192.168.110.55:5555), (op oracle-check) повернув коректний oracle-result двічі (до і після kill -9 + watchdog-рестарту, різні server-generation); ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum")) +(reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789259710) (route ask-agent) (topic "termux-boot-adb-installed-app-stopped-state-gotcha") (question "Чому Termux:Boot не запускає скрипт при завантаженні пристрою, навіть якщо APK встановлено і boot-скрипт правильно лежить у ~/.termux/boot/, і як це виправити?") (answer "Живо підтверджено 2026-09-13, двома реальними перезавантаженнями пристрою (не симуляцією через adb su): додаток, встановлений через adb install і НІ РАЗУ не відкритий вручну користувачем, лишається в Android-специфічному 'stopped state' -- ОС свідомо НЕ доставляє йому жодні broadcast, включно з android.intent.action.BOOT_COMPLETED, навіть якщо AndroidManifest.xml і Receiver Resolver Table (підтверджено через dumpsys package) коректно реєструють приймач. Перше перезавантаження після встановлення termux-boot APK і написання ~/.termux/boot/start-oracle.sh дало ПОВНУ тишу -- logcat показав лише dexopt/компіляцію самого com.termux.boot APK при першому запуску системи, жодного виконання BootReceiver. Причина знайдена через порівняння: жоден із двох додатків (com.termux, com.termux.boot) ніколи не запускався через UI/adb, обидва в stopped state. Фікс: adb shell am start -n com.termux.boot/.BootActivity і adb shell am start -n com.termux/.app.TermuxActivity (одноразово, будь-яка launchable activity кожного пакета) -- це виводить застосунок зі stopped state. Після цього повторне реальне перезавантаження (adb reboot) підтвердило: my-lisp-процес запустився САМ під правильним Termux-користувачем (u0_a84) без жодного ручного втручання, і повний TCP oracle-check round-trip пройшов успішно одразу після завантаження.") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-authorized real device reboot (twice)") (evidence "dumpsys package com.termux.boot підтвердив коректний BOOT_COMPLETED receiver ДО фіксу; перше реальне adb reboot -- 0 my-lisp-процесів після завантаження; am start на обидва пакети; друге реальне adb reboot -- my-lisp-arm32 запущений автоматично під u0_a84, живий TCP oracle-check round-trip пройшов (server-generation 1789259669); ecosystem/memory/my-lisp-architect.md 2026-09-13 фінальний addendum")) From 7036a54ffd31d9cdc803763062da67ea329bb72d Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:44:04 +0300 Subject: [PATCH 25/26] docs(surface): clarify audit projection authority boundary --- scripts/generate-uk-surface-audit.my | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/generate-uk-surface-audit.my b/scripts/generate-uk-surface-audit.my index 876f0eda..70dc100e 100644 --- a/scripts/generate-uk-surface-audit.my +++ b/scripts/generate-uk-surface-audit.my @@ -9,8 +9,9 @@ ; ; Candidate evidence path: ; lib/surface/український-профіль-джерела.всм -; Candidate spellings remain proposals. This generator MUST NOT promote them -; into the authoritative `full-uk` column by itself. +; Candidate spellings remain proposals. `lib/generated/function-table.wsm` +; is a checked projection, not a second semantic authority. This generator +; MUST NOT promote staging candidates into that current `full-uk` projection. ; ; Output: ; lib/generated/uk-surface-audit.wsm From a908db1891fbb25c39f857d915c6d0651320b71d Mon Sep 17 00:00:00 2001 From: juv4uk <85576956+juv4uk@users.noreply.github.com> Date: Sun, 13 Sep 2026 03:44:36 +0300 Subject: [PATCH 26/26] knowledge: record all 3 oracles live 2026-09-13, flag stale droplet status doc Direct TCP oracle-check round-trips confirm three live my-lisp oracles simultaneously: local WSL host, the droplet (which docs/swarm-remote-access.md still lists as DOWN from its 2026-08-13 audit -- stale, someone revived it since without updating the address book), and the new Android TV box deployment. Practical lesson: verify oracle status via a live TCP request, not by trusting the address books last-audit date. Co-Authored-By: Claude Sonnet 5 --- knowledge/guard-reference-inbox.mylog | 1 + 1 file changed, 1 insertion(+) diff --git a/knowledge/guard-reference-inbox.mylog b/knowledge/guard-reference-inbox.mylog index e5d694d1..7fcdb2b2 100644 --- a/knowledge/guard-reference-inbox.mylog +++ b/knowledge/guard-reference-inbox.mylog @@ -8,3 +8,4 @@ (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789257374) (route ask-agent) (topic "guix-aarch64-cross-bootstrap-config-sub-dead-end") (question "Чи можна на цьому Guix-хості (WSL, x86_64) отримати aarch64-linux-gnu крос-toolchain для деплою на ARM-пристрій, і чи вирішує це guix pull?") (answer "Ні, підтверджено двічі (до і після guix pull, 2026-09-13): guix build --target=aarch64-linux-gnu gcc-toolchain падає на РАННІЙ стадії bootstrap з (Invalid configuration aarch64-linux-gnu: machine aarch64 not recognized) під час configure gawk-mesboot-3.1.8 (частина 'full source bootstrap' ланцюга Guix, навмисно старий/заморожений пакет для reproducibility). guix pull оновив сам Guix (0cc8f41 -> a39872ef5) успішно, але це НЕ виправило проблему -- config.sub, вбудований у gawk-mesboot's власний джерельний tarball, лишається старим незалежно від версії Guix, бо цей конкретний мезбут-пакет свідомо заморожений заради відтворюваності бутстрапу, а не оновлюється разом з рештою дистрибуції. Практичний висновок: на цьому хості (і ймовірно на будь-якому стандартному Guix без спеціального патчу mesboot chain) aarch64-linux-gnu через from-scratch bootstrap не збирається; або (a) знайти вже готовий /gnu/store substitute для aarch64 gcc-toolchain з іншого джерела/каналу, (b) обійти mesboot повністю через сторонній prebuilt крос-toolchain (напр. Android NDK для aarch64-linux-android, не gnu), або (c) патчити/оновити config.sub у самому mesboot-пакеті (ризиковано, ламає bootstrap-reproducibility гарантії, не пробувано).") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-directed") (evidence "guix build --target=aarch64-linux-gnu gcc-toolchain, двічі відтворено (guix 0cc8f41 і a39872ef5), обидва рази ідентична помилка на gawk-mesboot-3.1.8; ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum")) (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789258732) (route ask-agent) (topic "android-tv-box-oracle-deployment") (question "Як розгорнути my-lisp --tcp oracle на завжди-увімкненому Android TV box (не x86_64 droplet), і на що звертати увагу?") (answer "Живо задеплоєно і підтверджено 2026-09-13 на X96 Max Plus2 (Amlogic S905X3, Android 9/SDK 28, ARM64 SoC але 32-bit-only userspace -- getprop ro.product.cpu.abilist показує лише armeabi-v7a/armeabi, немає /system/bin/linker64 взагалі). Ключова пастка: перша спроба з aarch64-linux-android провалилась з (not executable: 64-bit ELF file) -- це НЕ noexec mount (/data не мав noexec, SELinux Permissive), а справжня причина архітектурна: система суто 32-бітна, попри 64-бітний SoC (типово для дешевих китайських TV-box). Фікс: rustup target add armv7-linux-androideabi, лінкер armv7a-linux-androideabi28-clang з Android NDK r30, збірка одразу спрацювала (ELF 32-bit LSB pie executable, ARM, interpreter /system/bin/linker). Деплой через adb push у /data/local/tmp (root-adb, стандартно на цьому пристрої, su доступний). --tcp сервер у коді (crates/my-lisp-cli/src/tcp_repl.rs) жорстко прив'язаний до Ipv4Addr::LOCALHOST, без --bind прапора -- свідоме рішення проєкту, не баг; доступ ззовні лише через adb forward tcp::9999 з машини з живим adb-з'єднанням, не пряме мережеве підключення інших peer. Стійкість до крашів (не до перезавантаження пристрою): простий shell while-true watchdog-цикл у /data/local/tmp/oracle-watchdog.sh, живо підтверджено -- kill -9 процесу призвів до реального автоматичного рестарту за 2с з новим PID, oracle.log зафіксував подію, TCP-запит через adb forward після рестарту пройшов успішно (новий server-generation). Reboot-persistence НЕ вирішено: пристрій має Termux встановлений, але без Termux:Boot addon (був би найпростіший шлях), встановлення додаткового APK на робочому пристрої власника свідомо не зроблено без окремого дозволу.") (source "agent:my-lisp-09 (Claude Sonnet 5) live deployment 2026-09-13, owner-directed (TV-tuner oracle experiment continuation)") (evidence "живий TCP round-trip через adb forward tcp:19999:9999 до X96Max_Plus2_T (192.168.110.55:5555), (op oracle-check) повернув коректний oracle-result двічі (до і після kill -9 + watchdog-рестарту, різні server-generation); ecosystem/memory/my-lisp-architect.md 2026-09-13 addendum")) (reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789259710) (route ask-agent) (topic "termux-boot-adb-installed-app-stopped-state-gotcha") (question "Чому Termux:Boot не запускає скрипт при завантаженні пристрою, навіть якщо APK встановлено і boot-скрипт правильно лежить у ~/.termux/boot/, і як це виправити?") (answer "Живо підтверджено 2026-09-13, двома реальними перезавантаженнями пристрою (не симуляцією через adb su): додаток, встановлений через adb install і НІ РАЗУ не відкритий вручну користувачем, лишається в Android-специфічному 'stopped state' -- ОС свідомо НЕ доставляє йому жодні broadcast, включно з android.intent.action.BOOT_COMPLETED, навіть якщо AndroidManifest.xml і Receiver Resolver Table (підтверджено через dumpsys package) коректно реєструють приймач. Перше перезавантаження після встановлення termux-boot APK і написання ~/.termux/boot/start-oracle.sh дало ПОВНУ тишу -- logcat показав лише dexopt/компіляцію самого com.termux.boot APK при першому запуску системи, жодного виконання BootReceiver. Причина знайдена через порівняння: жоден із двох додатків (com.termux, com.termux.boot) ніколи не запускався через UI/adb, обидва в stopped state. Фікс: adb shell am start -n com.termux.boot/.BootActivity і adb shell am start -n com.termux/.app.TermuxActivity (одноразово, будь-яка launchable activity кожного пакета) -- це виводить застосунок зі stopped state. Після цього повторне реальне перезавантаження (adb reboot) підтвердило: my-lisp-процес запустився САМ під правильним Termux-користувачем (u0_a84) без жодного ручного втручання, і повний TCP oracle-check round-trip пройшов успішно одразу після завантаження.") (source "agent:my-lisp-09 (Claude Sonnet 5) live investigation 2026-09-13, owner-authorized real device reboot (twice)") (evidence "dumpsys package com.termux.boot підтвердив коректний BOOT_COMPLETED receiver ДО фіксу; перше реальне adb reboot -- 0 my-lisp-процесів після завантаження; am start на обидва пакети; друге реальне adb reboot -- my-lisp-arm32 запущений автоматично під u0_a84, живий TCP oracle-check round-trip пройшов (server-generation 1789259669); ecosystem/memory/my-lisp-architect.md 2026-09-13 фінальний addendum")) +(reference-candidate (schema guard-reference-candidate/1) (status pending-review) (recorded-at-unix 1789260265) (route ask-agent) (topic "three-live-oracles-2026-09-13") (question "Скільки живих my-lisp TCP oracle зараз реально відповідають, і чи актуальна адресна книга в docs/swarm-remote-access.md?") (answer "Живо перевірено 2026-09-13 прямим TCP-запитом (не читанням документації чи припущенням): ТРИ oracle відповідають одночасно, кожен реальним (op oracle-check) round-trip з коректним oracle-result і живим server-generation timestamp: (1) локальний WSL-хост, 127.0.0.1:9999, pid 10446, /home/agents/GitHub/my-lisp/target/release/my-lisp --tcp=9999 --protocol=sexpr; (2) droplet, 100.113.68.50:9999 -- ВІДПОВІДАЄ, попри те що docs/swarm-remote-access.md (аудит 2026-08-13) досі стверджує 'DOWN (service not listening)' -- документ явно застарів, хтось підняв сервіс після того аудиту без оновлення адресної книги; (3) Android TV box (X96Max_Plus2_T, 192.168.110.55), досяжний через adb forward tcp::9999, деплой і reboot-persistence задокументовані в тому ж дні (topic android-tv-box-oracle-deployment, termux-boot-adb-installed-app-stopped-state-gotcha). Практичний висновок: docs/swarm-remote-access.md's address book не можна вважати достовірним джерелом поточного статусу без живої перевірки -- перевіряти TCP напряму (echo '(request (id N) (op oracle-check) (source \"...\"))' через /dev/tcp чи еквівалент), а не покладатися на дату останнього аудиту в документі.") (source "agent:my-lisp-09 (Claude Sonnet 5) live triple-check 2026-09-13, owner-directed") (evidence "три окремі живі (op oracle-check) round-trips того самого дня, кожен з унікальним server-generation; ecosystem/memory/my-lisp-architect.md 2026-09-13 фінальний addendum; docs/swarm-remote-access.md (не оновлено цим комітом, лише позначено як застаріле джерело для droplet-статусу)"))