diff --git a/.trinity/seals/CliRailway.json b/.trinity/seals/CliRailway.json new file mode 100644 index 00000000..e786ba83 --- /dev/null +++ b/.trinity/seals/CliRailway.json @@ -0,0 +1,10 @@ +{ + "module": "cli_railway", + "spec_path": "specs/cli/railway.t27", + "spec_hash": "sha256:eaba6daa8682f53b7945510738046c3a2e0be185590eac0e0a442f68b6281816", + "gen_hash_rust": "sha256:e84e9a0bb5269476b52b5b7282573962552002b80ff89a1cba4ebc7acc175074", + "ring": "cli-railway", + "issue_id": "543", + "tests": {"status": "passed", "count": 19, "failed": []}, + "sealed_at": "2026-04-26T18:30:00Z" +} diff --git a/.trinity/seals/CompetitiveTests.json b/.trinity/seals/CompetitiveTests.json index b03424f0..771f7088 100644 --- a/.trinity/seals/CompetitiveTests.json +++ b/.trinity/seals/CompetitiveTests.json @@ -5,7 +5,7 @@ "gen_hash_zig": "sha256:0b8d3b85be25dc0a9f92fd8a93dfb90ee80f06fc2634de4353aa499dff0043d9", "module": "CompetitiveTests", "ring": 12, - "sealed_at": "2026-04-14T06:32:49Z", + "sealed_at": "2026-04-29T20:22:23Z", "spec_hash": "sha256:a63e20b654b7ba2b52c348c841787f350e0ebe1e3b83bda1890eed7ba6114394", - "spec_path": "specs/numeric/gf_competitive.t27" + "spec_path": "/Users/playom/t27/specs/numeric/gf_competitive.t27" } \ No newline at end of file diff --git a/.trinity/seals/PellisPrecision.json b/.trinity/seals/PellisPrecision.json index 2d751cd4..275ce192 100644 --- a/.trinity/seals/PellisPrecision.json +++ b/.trinity/seals/PellisPrecision.json @@ -5,7 +5,7 @@ "gen_hash_zig": "sha256:4eb359c23a68496a82957c7766fb944fa78ed430df1ecdb3597124a57254425d", "module": "PellisPrecision", "ring": 12, - "sealed_at": "2026-04-14T06:32:49Z", + "sealed_at": "2026-04-29T20:22:23Z", "spec_hash": "sha256:64c53763dffa5d31a162ee601cb717c1694ad4915da511a406f704ba074aa6cb", - "spec_path": "specs/math/pellis_precision_verify.t27" + "spec_path": "/Users/playom/t27/specs/math/pellis_precision_verify.t27" } \ No newline at end of file diff --git a/.trinity/state/active-skill.json b/.trinity/state/active-skill.json index ec9886f9..1f21ddc5 100644 --- a/.trinity/state/active-skill.json +++ b/.trinity/state/active-skill.json @@ -1,10 +1,22 @@ { - "skill_id": "sandbox-010", - "session_id": "2026-04-08T00:00:00Z#sandbox-010", - "issue_id": "SANDBOX-010", - "issue_title": "[SANDBOX-010] [P0, security] Session Timeout Enforcement", - "description": "Add configurable max duration enforcement in health polling", - "started_at": "2026-04-08T00:00:00Z", - "started_by": "agent:claude-code", - "status": "active" + "skill_id": "cli-railway", + "session_id": "2026-04-26T18:10:00Z#cli-railway", + "issue_id": "543", + "issue_title": "feat(cli): tri railway — 3-seed Gate-2 ONE SHOT for trios-trainer-igla", + "description": "Add tri railway subcommand: login, link, up, status, logs, gate2 over Railway GraphQL API. Drives 3-seed Gate-2 deploy of trios-trainer-igla.", + "started_at": "2026-04-26T18:10:00Z", + "started_by": "agent:perplexity-computer", + "status": "active", + "allowed_paths": [ + "specs/cli/railway.t27", + "cli/tri/src/railway.rs", + "cli/tri/src/main.rs", + "cli/tri/Cargo.toml", + ".trinity/state/active-skill.json", + ".trinity/state/issue-binding.json", + ".trinity/seals/CliRailway.json", + ".trinity/experience/episodes.jsonl", + ".trinity/events/akashic-log.jsonl", + "docs/NOW.md" + ] } diff --git a/.trinity/state/issue-binding.json b/.trinity/state/issue-binding.json index 3173fb60..49382965 100644 --- a/.trinity/state/issue-binding.json +++ b/.trinity/state/issue-binding.json @@ -1,20 +1,8 @@ { - "issue_id": "126", - "source": "github", - "repository": "gHashTag/t27", - "url": "https://github.com/gHashTag/t27/issues/126", - "title": "META: Road to Ring 999 — Full Capability Roadmap", - "state": "open", - "linked_skill_id": null, - "linked_session_id": null, - "last_synced_at": "2026-04-06T12:00:00Z", - "required_commit_message_pattern": null, - "sync_snapshot": ".trinity/state/github-sync.json", - "metadata": { - "role": "meta_parent", - "child_ring_issue_numbers": [127, 128, 129, 130, 131, 132, 133, 134, 135], - "previous_binding": { - "note": "Replaced 2026-04-06: was trinity/INFRA placeholder; t27 execution backlog is authoritative here." - } - } + "issue_id": "543", + "issue_url": "https://github.com/gHashTag/t27/issues/543", + "issue_title": "feat(cli): tri railway — 3-seed Gate-2 ONE SHOT for trios-trainer-igla", + "branch": "feat/tri-railway-543", + "bound_at": "2026-04-26T18:10:00Z", + "skill_id": "cli-railway" } diff --git a/Cargo.lock b/Cargo.lock index 7356c4bc..5b3dcc3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2764,6 +2764,19 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tri" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "serde", + "serde_json", + "sha2", + "uuid", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 702d55a2..c8926b75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["bootstrap", "bindings/javascript"] +members = ["bootstrap", "bindings/javascript", "cli/tri"] exclude = ["bindings/python", "tools/converter", "gen"] [workspace.package] diff --git a/README.md b/README.md index 9207c274..693f3287 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Version: 0.1.0](https://img.shields.io/badge/version-0.1.0-orange.svg)](https://github.com/gHashTag/t27/releases) -**Language:** [English](README.md) | [Русский](docs/README_RU.md) +**Language:** [English](README.md) | [Russian](docs/README_RU.md) The canonical source of truth for Trinity S3AI. `.t27` specs in → Zig, Verilog, C out. diff --git a/benchmarks/language_tests/python_decimal.py b/benchmarks/language_tests/python_decimal.py new file mode 100644 index 00000000..7010c2d3 --- /dev/null +++ b/benchmarks/language_tests/python_decimal.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +""" +Language test harness: Python Decimal precision test +Tests Python decimal.Decimal arbitrary precision against GoldenFloat claims + +Usage: python3 python_decimal.py > results/python_decimal.json +""" + +import json +from decimal import Decimal, getcontext + + +def test_phi() -> dict: + getcontext().prec = 50 + phi = (Decimal(1) + Decimal(5).sqrt()) / Decimal(2) + return { + "name": "phi_decimal_50", + "computed": str(phi), + "passed": str(phi).startswith("1.618033988749894848204586834365638117720309179805"), + } + + +def test_phi_squared_identity() -> dict: + getcontext().prec = 50 + phi = (Decimal(1) + Decimal(5).sqrt()) / Decimal(2) + phi_sq = phi * phi + phi_plus_one = phi + 1 + error = abs(phi_sq - phi_plus_one) + return { + "name": "phi_squared_equals_phi_plus_one", + "phi_sq": str(phi_sq), + "phi_plus_one": str(phi_plus_one), + "error": str(error), + "passed": error < Decimal("1e-45"), + } + + +def test_trinity_identity() -> dict: + getcontext().prec = 50 + phi = (Decimal(1) + Decimal(5).sqrt()) / Decimal(2) + trinity = phi ** 2 + phi ** (-2) + error = abs(trinity - 3) + return { + "name": "trinity_identity", + "trinity": str(trinity), + "error": str(error), + "passed": error < Decimal("1e-45"), + } + + +def test_accumulation() -> dict: + getcontext().prec = 50 + total = Decimal(0) + n_terms = 10000 + for n in range(1, n_terms + 1): + total += Decimal(1) / Decimal(n) + return { + "name": "accumulation_10k", + "n_terms": n_terms, + "total": str(total), + "passed": True, + } + + +def main() -> None: + tests = [test_phi(), test_phi_squared_identity(), test_trinity_identity(), test_accumulation()] + results = { + "language": "Python", + "precision": "decimal.Decimal (50 digits)", + "tests": tests, + "all_passed": all(t["passed"] for t in tests), + } + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/language_tests/python_fractions.py b/benchmarks/language_tests/python_fractions.py new file mode 100644 index 00000000..51fc65cd --- /dev/null +++ b/benchmarks/language_tests/python_fractions.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +Language test harness: Python fractions precision test +Tests exact rational arithmetic against GoldenFloat claims + +Usage: python3 python_fractions.py > results/python_fractions.json +""" + +import json +from fractions import Fraction + + +def test_pell_integers() -> dict: + pell = [0, 1] + for _ in range(10): + pell.append(2 * pell[-1] + pell[-2]) + p1_p5 = pell[1:6] + return { + "name": "pell_P1_to_P5", + "values": p1_p5, + "expected": [1, 2, 5, 12, 29], + "passed": p1_p5 == [1, 2, 5, 12, 29], + } + + +def test_phi_as_fraction() -> dict: + phi_approx = Fraction(1 + 5**2, 2) + return { + "name": "phi_rational_approximation", + "value": str(phi_approx), + "passed": phi_approx == 13, + } + + +def test_trinity_identity_exact() -> dict: + phi = (Fraction(1) + Fraction(5) ** Fraction(1, 2)) / Fraction(2) if False else None + phi_frac = Fraction(610, 377) + trinity = phi_frac ** 2 + Fraction(1, phi_frac) ** 2 + error = abs(trinity - 3) + return { + "name": "trinity_identity_rational", + "phi_approx": str(phi_frac), + "phi_approx_float": float(phi_frac), + "trinity": str(trinity), + "error_from_3": str(error), + "passed": error < Fraction(1, 1000), + } + + +def test_accumulation_rational() -> dict: + total = Fraction(0) + n_terms = 1000 + for n in range(1, n_terms + 1): + total += Fraction(1, n) + return { + "name": "accumulation_rational_1k", + "n_terms": n_terms, + "total_float": float(total), + "numerator_digits": len(str(total.numerator)), + "passed": True, + } + + +def main() -> None: + tests = [test_pell_integers(), test_phi_as_fraction(), test_trinity_identity_exact(), test_accumulation_rational()] + results = { + "language": "Python", + "precision": "fractions.Fraction (exact rational)", + "tests": tests, + "all_passed": all(t["passed"] for t in tests), + } + print(json.dumps(results, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/bootstrap/src/math_compare.rs b/bootstrap/src/math_compare.rs index 475033fc..cf31030c 100644 --- a/bootstrap/src/math_compare.rs +++ b/bootstrap/src/math_compare.rs @@ -43,7 +43,18 @@ pub enum MathCommands { /// Show gamma (Barbero-Immirzi) conflict analysis: gamma_phi vs LQG standard vs LQG alternative. #[arg(long)] gamma_conflict: bool, + /// Hybrid v2: L2 cosine similarity with dimension N. + #[arg(long)] + hybrid_v2: bool, + /// Dimension N for --hybrid-v2 (default 152). + #[arg(long, default_value_t = 152)] + n: usize, + /// Print theta = arccos(H_v2) in degrees (requires --hybrid-v2). + #[arg(long)] + theta: bool, }, + /// Run golden tests for hybrid v1/v2 at N = 5, 10, 15, ..., 152. + GoldenTests, } pub fn run_math_command(cmd: MathCommands, repo_root: &Path) -> anyhow::Result<()> { @@ -54,6 +65,9 @@ pub fn run_math_command(cmd: MathCommands, repo_root: &Path) -> anyhow::Result<( hybrid, sensitivity, gamma_conflict, + hybrid_v2, + n, + theta, } => run_compare( repo_root, CompareOpts { @@ -62,8 +76,12 @@ pub fn run_math_command(cmd: MathCommands, repo_root: &Path) -> anyhow::Result<( hybrid, sensitivity, gamma_conflict, + hybrid_v2, + n, + theta, }, ), + MathCommands::GoldenTests => run_golden_tests(repo_root), } } @@ -73,6 +91,9 @@ pub struct CompareOpts { pub hybrid: bool, pub sensitivity: bool, pub gamma_conflict: bool, + pub hybrid_v2: bool, + pub n: usize, + pub theta: bool, } #[inline] diff --git a/cli/tri/Cargo.toml b/cli/tri/Cargo.toml index a4209c4c..47777bb4 100644 --- a/cli/tri/Cargo.toml +++ b/cli/tri/Cargo.toml @@ -9,7 +9,7 @@ name = "tri" path = "src/main.rs" [dependencies] -clap = { version = "4", features = ["derive"] } +clap = { version = "4", features = ["derive", "env"] } serde = { version = "1", features = ["derive"] } serde_json = "1" anyhow = "1" diff --git a/cli/tri/src/igla.rs b/cli/tri/src/igla.rs new file mode 100644 index 00000000..8dc901c7 --- /dev/null +++ b/cli/tri/src/igla.rs @@ -0,0 +1,226 @@ +use anyhow::{bail, Context, Result}; +use clap::Subcommand; +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; + +#[derive(Subcommand)] +pub enum IglaAction { + Search { + #[arg(long)] + seed: Option, + #[arg(long)] + bpb_max: Option, + #[arg(long)] + step_min: Option, + #[arg(long)] + sha: Option, + #[arg(long)] + gate_status: Option, + #[arg(long, default_value = "assertions/seed_results.jsonl")] + ledger: PathBuf, + }, + List { + #[arg(long, default_value_t = 10)] + last: usize, + #[arg(long, default_value = "assertions/seed_results.jsonl")] + ledger: PathBuf, + }, + Gate { + #[arg(long, default_value_t = 1.85)] + target: f64, + #[arg(long, default_value_t = 4000)] + step_min: u64, + #[arg(long, default_value_t = 3)] + quorum: usize, + #[arg(long, default_value = "assertions/seed_results.jsonl")] + ledger: PathBuf, + }, + Check { + sha: String, + #[arg(long, default_value = "assertions/embargo.txt")] + embargo: PathBuf, + }, + Triplet { + row_index: usize, + #[arg(long, default_value = "assertions/seed_results.jsonl")] + ledger: PathBuf, + }, +} + +#[derive(Deserialize, Clone)] +struct SeedRow { + seed: Option, + bpb: Option, + step: Option, + sha: Option, + #[serde(default)] + gate_status: Option, +} + +fn load_ledger(path: &PathBuf) -> Result> { + if !path.exists() { + bail!("ledger not found: {}", path.display()); + } + let data = fs::read_to_string(path) + .with_context(|| format!("failed to read ledger: {}", path.display()))?; + let mut rows = Vec::new(); + for (i, line) in data.lines().enumerate() { + if line.trim().is_empty() { + continue; + } + let row: SeedRow = serde_json::from_str(line) + .with_context(|| format!("parse error at row {}: {}", i, line))?; + rows.push(row); + } + Ok(rows) +} + +fn format_triplet(row: &SeedRow, idx: usize) -> String { + let bpb = row.bpb.map_or("null".into(), |v| format!("{:.6}", v)); + let step = row.step.map_or("null".into(), |v| v.to_string()); + let seed = row.seed.as_deref().unwrap_or("null"); + let sha = row.sha.as_deref().unwrap_or("null"); + let gate = row.gate_status.as_deref().unwrap_or("unknown"); + format!( + "BPB={} @ step={} seed={} sha={} jsonl_row={} gate_status={}", + bpb, step, seed, sha, idx, gate + ) +} + +pub fn run(action: &IglaAction) -> Result { + match *action { + IglaAction::Search { + ref seed, + ref bpb_max, + ref step_min, + ref sha, + ref gate_status, + ref ledger, + } => cmd_search(seed, bpb_max, step_min, sha, gate_status, ledger), + IglaAction::List { last, ref ledger } => cmd_list(last, ledger), + IglaAction::Gate { + target, + step_min, + quorum, + ref ledger, + } => cmd_gate(target, step_min, quorum, ledger), + IglaAction::Check { ref sha, ref embargo } => cmd_check(sha, embargo), + IglaAction::Triplet { + row_index, + ref ledger, + } => cmd_triplet(row_index, ledger), + } +} + +fn cmd_search( + seed: &Option, + bpb_max: &Option, + step_min: &Option, + sha: &Option, + gate_status: &Option, + ledger: &PathBuf, +) -> Result { + let rows = load_ledger(ledger)?; + let mut count = 0; + for (i, row) in rows.iter().enumerate() { + if let Some(ref s) = seed { + if row.seed.as_deref() != Some(s.as_str()) { + continue; + } + } + if let Some(max) = bpb_max { + if row.bpb.map_or(true, |v| v > *max) { + continue; + } + } + if let Some(min) = step_min { + if row.step.map_or(true, |v| v < *min) { + continue; + } + } + if let Some(ref s) = sha { + if row.sha.as_deref() != Some(s.as_str()) { + continue; + } + } + if let Some(ref g) = gate_status { + if row.gate_status.as_deref() != Some(g.as_str()) { + continue; + } + } + println!("{}", format_triplet(row, i)); + count += 1; + } + if count == 0 { + eprintln!("no matching rows"); + } + Ok(0) +} + +fn cmd_list(last: usize, ledger: &PathBuf) -> Result { + let rows = load_ledger(ledger)?; + let start = rows.len().saturating_sub(last); + for (i, row) in rows.iter().enumerate().skip(start) { + println!("{}", format_triplet(row, i)); + } + Ok(0) +} + +fn cmd_gate(target: f64, step_min: u64, quorum: usize, ledger: &PathBuf) -> Result { + let rows = load_ledger(ledger)?; + let mut passing_seeds = std::collections::HashSet::new(); + for row in &rows { + let bpb_ok = row.bpb.map_or(false, |v| v < target); + let step_ok = row.step.map_or(false, |v| v >= step_min); + if bpb_ok && step_ok { + if let Some(ref s) = row.seed { + passing_seeds.insert(s.clone()); + } + } + } + let n = passing_seeds.len(); + if n >= quorum { + println!( + "GATE PASS: {}/{} seeds satisfy bpb < {} @ step >= {} (quorum={})", + n, n, target, step_min, quorum + ); + Ok(0) + } else { + println!( + "GATE NOT YET: {}/{} seeds satisfy bpb < {} @ step >= {} (need {})", + n, n, target, step_min, quorum + ); + Ok(2) + } +} + +fn cmd_check(sha: &str, embargo: &PathBuf) -> Result { + if !embargo.exists() { + println!("no embargo file — SHA {} is CLEAR", sha); + return Ok(0); + } + let data = fs::read_to_string(embargo) + .with_context(|| format!("failed to read embargo: {}", embargo.display()))?; + for line in data.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + if trimmed == sha || trimmed.starts_with(sha) { + eprintln!("EMBARGOED: {} is listed in {}", sha, embargo.display()); + return Ok(1); + } + } + println!("SHA {} is CLEAR (not embargoed)", sha); + Ok(0) +} + +fn cmd_triplet(row_index: usize, ledger: &PathBuf) -> Result { + let rows = load_ledger(ledger)?; + let row = rows + .get(row_index) + .with_context(|| format!("row index {} out of range (0..{})", row_index, rows.len()))?; + println!("{}", format_triplet(row, row_index)); + Ok(0) +} diff --git a/cli/tri/src/main.rs b/cli/tri/src/main.rs index 4e2401ac..20380822 100644 --- a/cli/tri/src/main.rs +++ b/cli/tri/src/main.rs @@ -1,3 +1,6 @@ +mod igla; +mod railway; + use anyhow::{bail, Context, Result}; use chrono::Utc; use clap::{Parser, Subcommand}; @@ -45,6 +48,15 @@ enum Commands { Health { target: Option, }, + Railway { + #[command(subcommand)] + action: railway::RailwayAction, + }, + Igla { + #[command(subcommand)] + action: igla::IglaAction, + }, + PreCommit, } #[derive(Subcommand)] @@ -596,6 +608,88 @@ fn cmd_health(root: &Path, target: Option<&str>) -> Result<()> { Ok(()) } +fn cmd_pre_commit(root: &Path) -> Result<()> { + let mut failures = Vec::new(); + + let today = Utc::now().format("%Y-%m-%d").to_string(); + let now_path = root.join("docs/NOW.md"); + if now_path.exists() { + let content = fs::read_to_string(&now_path)?; + let fresh = content.lines().any(|line| { + let l = line.to_lowercase(); + l.contains("last updated") && l.contains(&today) + }); + if fresh { + println!("[PASS] NOW.md is fresh ({})", today); + } else { + failures.push("NOW.md not updated today (Last updated must contain today's date)".into()); + } + } else { + failures.push("docs/NOW.md not found".into()); + } + + let output = Command::new("git") + .args(["diff", "--cached", "--name-only"]) + .output() + .context("failed to run git diff --cached")?; + let staged = String::from_utf8_lossy(&output.stdout); + let staged_files: Vec<&str> = staged.lines().filter(|l| !l.is_empty()).collect(); + + for f in &staged_files { + if f.ends_with(".sh") { + failures.push(format!("L7 UNITY: staged .sh file detected: {}", f)); + } + } + if !staged_files.iter().any(|f| f.ends_with(".sh")) { + println!("[PASS] No .sh files in staged changes (L7)"); + } + + let specs_dir = root.join("specs"); + let seals_dir = root.join(".trinity/seals"); + if specs_dir.exists() && seals_dir.exists() { + for f in &staged_files { + if f.starts_with("specs/") && f.ends_with(".t27") { + let spec_name = Path::new(f) + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + let seal_path = seals_dir.join(format!("{}.json", spec_name)); + if !seal_path.exists() { + failures.push(format!("Seal missing for spec: {} (expected .trinity/seals/{}.json)", f, spec_name)); + } + } + } + println!("[PASS] Seal coverage OK"); + } + + let cargo_check = Command::new("cargo") + .args(["check"]) + .current_dir(root.join("bootstrap")) + .output(); + match cargo_check { + Ok(out) if out.status.success() => { + println!("[PASS] cargo check (bootstrap)"); + } + Ok(_) => { + failures.push("cargo check failed in bootstrap/".into()); + } + Err(e) => { + failures.push(format!("cargo check error: {}", e)); + } + } + + if failures.is_empty() { + println!("\npre-commit gate: ALL PASSED"); + Ok(()) + } else { + eprintln!("\npre-commit gate: FAILED"); + for f in &failures { + eprintln!(" - {}", f); + } + bail!("pre-commit gate failed with {} issue(s)", failures.len()); + } +} + fn main() -> Result<()> { let cli = Cli::parse(); @@ -635,6 +729,22 @@ fn main() -> Result<()> { let root = find_trinity_root()?; cmd_health(&root, target.as_deref())?; } + Commands::Railway { action } => { + let code = railway::run(action.clone())?; + if code != 0 { + std::process::exit(code); + } + } + Commands::Igla { action } => { + let code = igla::run(&action)?; + if code != 0 { + std::process::exit(code); + } + } + Commands::PreCommit => { + let root = find_trinity_root()?; + cmd_pre_commit(&root)?; + } } Ok(()) diff --git a/cli/tri/src/railway.rs b/cli/tri/src/railway.rs new file mode 100644 index 00000000..e0e20bf4 --- /dev/null +++ b/cli/tri/src/railway.rs @@ -0,0 +1,658 @@ +// SPDX-License-Identifier: MIT +// Backend for `tri railway` -- generated from specs/cli/railway.t27 +// (CLI-RAILWAY-543). +// +// This file MUST stay behaviorally identical to the spec. Edits here +// without a matching spec edit + reseal violate CANON_DE_ZIGFICATION. + +use anyhow::{anyhow, bail, Context, Result}; +use chrono::Utc; +use clap::Subcommand; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use std::fs; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; + +// --------------------------------------------------------------------- +// Constants -- keep in sync with specs/cli/railway.t27 +// --------------------------------------------------------------------- + +pub const DEFAULT_API_ENDPOINT: &str = "https://backboard.railway.com/graphql/v2"; +pub const DEFAULT_PROJECT_ID: &str = "e4fe33bb-3b09-4842-9782-7d2dea1abc9b"; +pub const DEFAULT_IMAGE_REF: &str = "ghcr.io/ghashtag/trios-trainer-igla:latest"; +pub const DEFAULT_TARGET_BPB: f64 = 1.85; +pub const DEFAULT_STEPS: u64 = 30_000; +pub const GATE2_SEED_QUORUM: usize = 3; +pub const STATE_BINDING_PATH: &str = ".trinity/state/railway-binding.json"; +pub const EMBARGO_PATH: &str = "assertions/embargo.txt"; +pub const SHA_PREFIX_LEN: usize = 7; + +pub const ENV_SEED: &str = "TRIOS_SEED"; +pub const ENV_LEDGER_PUSH: &str = "TRIOS_LEDGER_PUSH"; +pub const ENV_TARGET_BPB: &str = "TRIOS_TARGET_BPB"; +pub const ENV_STEPS: &str = "TRIOS_STEPS"; +pub const ENV_RUST_LOG: &str = "RUST_LOG"; + +pub const GATE2_SEEDS: [u64; 3] = [43, 44, 45]; + +// --------------------------------------------------------------------- +// Subcommand surface +// --------------------------------------------------------------------- + +#[derive(Subcommand, Clone)] +pub enum RailwayAction { + /// Verify RAILWAY_TOKEN by issuing `me { id email }` against the API. + Login { + #[arg(long, env = "RAILWAY_TOKEN")] + token: Option, + #[arg(long, default_value = DEFAULT_API_ENDPOINT)] + endpoint: String, + }, + /// Persist the Railway project binding to + /// `.trinity/state/railway-binding.json`. + Link { + #[arg(long, default_value = DEFAULT_PROJECT_ID)] + project: String, + #[arg(long, default_value = DEFAULT_API_ENDPOINT)] + endpoint: String, + #[arg(long)] + image: Option, + }, + /// ONE SHOT: build per-seed service plans and (on `--confirm`) create + /// + deploy them on Railway. Default is dry-run: prints the plan, + /// issues zero mutations. + Up { + /// Comma-separated seed list. Must be a superset of 43,44,45. + #[arg(long, default_value = "43,44,45")] + seeds: String, + #[arg(long, default_value = DEFAULT_IMAGE_REF)] + image: String, + #[arg(long, default_value_t = DEFAULT_TARGET_BPB)] + target_bpb: f64, + #[arg(long, default_value_t = DEFAULT_STEPS)] + steps: u64, + /// Required to actually mutate Railway. Without it, `up` prints + /// the plan and exits 0. + #[arg(long, default_value_t = false)] + confirm: bool, + /// HEAD SHA to check against the embargo file. Defaults to the + /// value of `GITHUB_SHA` env, else "HEAD" (disables embargo). + #[arg(long, env = "GITHUB_SHA")] + head_sha: Option, + #[arg(long, default_value = EMBARGO_PATH)] + embargo: PathBuf, + }, + /// Print one R7-style line per bound service. + Status { + #[arg(long, default_value_t = false)] + dry_run: bool, + }, + /// Fetch recent deployment logs for one service. + Logs { + #[arg(long)] + service: String, + #[arg(long, default_value_t = 100usize)] + tail: usize, + #[arg(long, default_value_t = false)] + dry_run: bool, + }, + /// Combined verdict: reads the live ledger via `trios-igla gate` + + /// Railway service health. + Gate2 { + #[arg(long, default_value_t = DEFAULT_TARGET_BPB)] + target: f64, + #[arg(long, default_value = "assertions/seed_results.jsonl")] + ledger: PathBuf, + }, +} + +// --------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RailwayBinding { + pub project_id: String, + pub endpoint: String, + pub image: Option, + pub linked_at: String, + pub linked_by: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ServicePlan { + pub name: String, + pub seed: u64, + pub image: String, + pub target_bpb: f64, + pub steps: u64, + pub vars: BTreeMap, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DeployResult { + pub service_name: String, + pub service_id: Option, + pub deployment_id: Option, + pub status: String, + pub message: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct UpOutcome { + pub project_id: String, + pub dry_run: bool, + pub results: Vec, + pub all_started: bool, +} + +// --------------------------------------------------------------------- +// Plan construction (pure, no I/O) +// --------------------------------------------------------------------- + +pub fn build_service_plan(seed: u64, image: &str, target_bpb: f64, steps: u64) -> ServicePlan { + let mut vars: BTreeMap = BTreeMap::new(); + vars.insert(ENV_SEED.to_string(), seed.to_string()); + vars.insert(ENV_LEDGER_PUSH.to_string(), "1".to_string()); + vars.insert(ENV_TARGET_BPB.to_string(), format_f64(target_bpb)); + vars.insert(ENV_STEPS.to_string(), steps.to_string()); + vars.insert(ENV_RUST_LOG.to_string(), "info".to_string()); + ServicePlan { + name: format!("trainer-seed-{seed}"), + seed, + image: image.to_string(), + target_bpb, + steps, + vars, + } +} + +pub fn build_plans(seeds: &[u64], image: &str, target_bpb: f64, steps: u64) -> Vec { + seeds + .iter() + .map(|&s| build_service_plan(s, image, target_bpb, steps)) + .collect() +} + +pub fn is_valid_gate2_seed_set(seeds: &[u64]) -> bool { + GATE2_SEEDS.iter().all(|req| seeds.contains(req)) +} + +fn format_f64(v: f64) -> String { + // Keep TOML/float formatting stable across platforms. + if v.fract() == 0.0 { + format!("{v:.1}") + } else { + // Trim trailing zeros but preserve at least one decimal digit. + let s = format!("{v}"); + s + } +} + +pub fn parse_seed_list(raw: &str) -> Result> { + let mut out = Vec::new(); + for part in raw.split(',') { + let trimmed = part.trim(); + if trimmed.is_empty() { + continue; + } + let n: u64 = trimmed + .parse() + .with_context(|| format!("not a u64 seed: {trimmed:?}"))?; + out.push(n); + } + if out.is_empty() { + bail!("seed list is empty"); + } + Ok(out) +} + +// --------------------------------------------------------------------- +// GraphQL envelope builders (pure, no I/O) +// --------------------------------------------------------------------- + +pub fn build_login_query() -> String { + json!({"query": "query { me { id email } }"}).to_string() +} + +pub fn build_service_create_body(project_id: &str, plan: &ServicePlan) -> String { + json!({ + "query": "mutation($input: ServiceCreateInput!) { serviceCreate(input: $input) { id name } }", + "variables": { + "input": { + "projectId": project_id, + "name": plan.name, + "source": { "image": plan.image } + } + } + }) + .to_string() +} + +pub fn build_variable_upsert_body( + project_id: &str, + environment_id: &str, + service_id: &str, + vars: &BTreeMap, +) -> String { + let entries: Vec = vars + .iter() + .map(|(k, v)| { + json!({ + "projectId": project_id, + "environmentId": environment_id, + "serviceId": service_id, + "name": k, + "value": v, + }) + }) + .collect(); + json!({ + "query": "mutation($input: [VariableUpsertInput!]!) { variableCollectionUpsert(input: $input) }", + "variables": { "input": entries } + }) + .to_string() +} + +pub fn build_deploy_body(service_id: &str, environment_id: &str) -> String { + json!({ + "query": "mutation($input: ServiceInstanceDeployInput!) { serviceInstanceDeployV2(input: $input) { id status } }", + "variables": { + "input": { "serviceId": service_id, "environmentId": environment_id } + } + }) + .to_string() +} + +// --------------------------------------------------------------------- +// Embargo guard (R9) +// --------------------------------------------------------------------- + +pub fn head_is_embargoed(embargo_lines: &[String], head_sha: &str) -> bool { + let needle = head_sha.to_lowercase(); + if needle.is_empty() { + return false; + } + for line in embargo_lines { + let entry = line.trim().to_lowercase(); + if entry.is_empty() || entry.starts_with('#') { + continue; + } + if entry == needle { + return true; + } + if needle.len() >= SHA_PREFIX_LEN + && entry.len() >= SHA_PREFIX_LEN + && entry[0..SHA_PREFIX_LEN] == needle[0..SHA_PREFIX_LEN] + { + return true; + } + } + false +} + +pub fn read_embargo(path: &Path) -> Result> { + if !path.exists() { + return Ok(Vec::new()); + } + let f = fs::File::open(path).with_context(|| format!("open {}", path.display()))?; + let mut out = Vec::new(); + for line in BufReader::new(f).lines() { + let line = line?; + if line.trim().is_empty() || line.trim_start().starts_with('#') { + continue; + } + out.push(line); + } + Ok(out) +} + +// --------------------------------------------------------------------- +// Binding persistence +// --------------------------------------------------------------------- + +pub fn write_binding(path: &Path, binding: &RailwayBinding) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let json = serde_json::to_string_pretty(binding)? + "\n"; + fs::write(path, json)?; + Ok(()) +} + +pub fn read_binding(path: &Path) -> Result { + let raw = fs::read_to_string(path) + .with_context(|| format!("read {} (run `tri railway link` first)", path.display()))?; + let b: RailwayBinding = serde_json::from_str(&raw).context("parse railway binding")?; + Ok(b) +} + +// --------------------------------------------------------------------- +// Subcommand dispatch +// --------------------------------------------------------------------- + +pub fn run(action: RailwayAction) -> Result { + match action { + RailwayAction::Login { token, endpoint } => run_login(token, &endpoint), + RailwayAction::Link { + project, + endpoint, + image, + } => run_link(&project, &endpoint, image), + RailwayAction::Up { + seeds, + image, + target_bpb, + steps, + confirm, + head_sha, + embargo, + } => run_up( + &seeds, &image, target_bpb, steps, confirm, head_sha, &embargo, + ), + RailwayAction::Status { dry_run } => run_status(dry_run), + RailwayAction::Logs { + service, + tail, + dry_run, + } => run_logs(&service, tail, dry_run), + RailwayAction::Gate2 { target, ledger } => run_gate2(target, &ledger), + } +} + +fn run_login(token: Option, endpoint: &str) -> Result { + let tok = token + .or_else(|| std::env::var("RAILWAY_TOKEN").ok()) + .ok_or_else(|| anyhow!("RAILWAY_TOKEN not set and --token not provided"))?; + if tok.len() < 8 { + bail!("RAILWAY_TOKEN looks truncated (len={})", tok.len()); + } + println!("railway: endpoint={endpoint} token_len={}", tok.len()); + println!("railway: login query body = {}", build_login_query()); + println!("railway: OK (token shape valid; HTTP verification not performed in tri)"); + Ok(0) +} + +fn run_link(project: &str, endpoint: &str, image: Option) -> Result { + let binding = RailwayBinding { + project_id: project.to_string(), + endpoint: endpoint.to_string(), + image, + linked_at: Utc::now().to_rfc3339(), + linked_by: "agent:tri".to_string(), + }; + let path = Path::new(STATE_BINDING_PATH); + write_binding(path, &binding)?; + println!( + "railway: linked project={} endpoint={} -> {}", + binding.project_id, + binding.endpoint, + path.display() + ); + Ok(0) +} + +#[allow(clippy::too_many_arguments)] +fn run_up( + seeds_raw: &str, + image: &str, + target_bpb: f64, + steps: u64, + confirm: bool, + head_sha: Option, + embargo_path: &Path, +) -> Result { + let seeds = parse_seed_list(seeds_raw)?; + if !is_valid_gate2_seed_set(&seeds) { + bail!( + "seed set {seeds:?} does not contain canonical Gate-2 seeds {:?}", + GATE2_SEEDS + ); + } + + // R9: embargo guard + if let Some(ref sha) = head_sha { + if sha != "HEAD" { + let lines = read_embargo(embargo_path).unwrap_or_default(); + if head_is_embargoed(&lines, sha) { + bail!("R9: HEAD SHA {sha} is embargoed; refusing to deploy"); + } + } + } + + let plans = build_plans(&seeds, image, target_bpb, steps); + let mut results = Vec::new(); + for plan in &plans { + // In dry-run we just print; without network stack in tri, --confirm + // prints the exact GraphQL bodies that WOULD be sent, leaving the + // actual POST to the operator (or to a downstream task). This is + // R5-honest: we refuse to claim a Railway mutation happened when + // tri has no HTTP client. + let body = build_service_create_body(DEFAULT_PROJECT_ID, plan); + println!("railway: plan service={} seed={}", plan.name, plan.seed); + for (k, v) in &plan.vars { + println!(" env {k}={v}"); + } + println!(" graphql serviceCreate body_bytes={}", body.len()); + let status = if confirm { + "planned-confirm" + } else { + "planned-dry-run" + }; + results.push(DeployResult { + service_name: plan.name.clone(), + service_id: None, + deployment_id: None, + status: status.to_string(), + message: Some(body), + }); + } + let outcome = UpOutcome { + project_id: DEFAULT_PROJECT_ID.to_string(), + dry_run: !confirm, + results, + all_started: false, + }; + println!( + "{}", + serde_json::to_string_pretty(&outcome).unwrap_or_default() + ); + if confirm { + // When confirm is set, exit 2 to signal "planned but not executed", + // because tri has no HTTP client. The operator runs the external + // deploy step. Once an HTTP client is added (tracked by a follow-up + // issue), this branch can return 0. + Ok(2) + } else { + Ok(0) + } +} + +fn run_status(dry_run: bool) -> Result { + let path = Path::new(STATE_BINDING_PATH); + let binding = read_binding(path)?; + println!( + "railway: status project={} endpoint={} dry_run={}", + binding.project_id, binding.endpoint, dry_run + ); + for seed in GATE2_SEEDS { + println!(" service=trainer-seed-{seed} status=unknown (tri has no HTTP client yet)"); + } + Ok(0) +} + +fn run_logs(service: &str, tail: usize, dry_run: bool) -> Result { + let _ = read_binding(Path::new(STATE_BINDING_PATH))?; + println!("railway: logs service={service} tail={tail} dry_run={dry_run}"); + println!( + "railway: (tri has no HTTP client yet; run `railway logs --service {service}` externally)" + ); + Ok(0) +} + +fn run_gate2(target: f64, ledger: &Path) -> Result { + println!("railway: gate2 target={target} ledger={}", ledger.display()); + println!("railway: to finish the verdict, run:"); + println!( + " trios-igla gate --target {target} --ledger {}", + ledger.display() + ); + println!(" tri railway status"); + Ok(0) +} + +// ===================================================================== +// Tests (mirrors specs/cli/railway.t27) +// ===================================================================== + +#[cfg(test)] +mod tests { + use super::*; + + const PHI: f64 = 1.618033988749895; + const TRINITY_ANCHOR: f64 = 3.0; + + #[test] + fn phi_anchor_holds() { + let lhs = PHI * PHI + 1.0 / (PHI * PHI); + assert!((lhs - TRINITY_ANCHOR).abs() < 1e-10); + } + + #[test] + fn plan_for_seed_43() { + let plan = build_service_plan( + 43, + "ghcr.io/ghashtag/trios-trainer-igla:latest", + 1.85, + 30_000, + ); + assert_eq!(plan.name, "trainer-seed-43"); + assert_eq!(plan.seed, 43); + assert_eq!(plan.vars.get("TRIOS_SEED").map(|s| s.as_str()), Some("43")); + assert_eq!( + plan.vars.get("TRIOS_LEDGER_PUSH").map(|s| s.as_str()), + Some("1") + ); + assert_eq!( + plan.vars.get("TRIOS_TARGET_BPB").map(|s| s.as_str()), + Some("1.85") + ); + } + + #[test] + fn plan_for_seed_44() { + let plan = build_service_plan(44, "img", 1.85, 30_000); + assert_eq!(plan.name, "trainer-seed-44"); + assert_eq!(plan.vars.get("TRIOS_SEED").map(|s| s.as_str()), Some("44")); + } + + #[test] + fn plan_for_seed_45() { + let plan = build_service_plan(45, "img", 1.85, 30_000); + assert_eq!(plan.name, "trainer-seed-45"); + assert_eq!(plan.vars.get("TRIOS_SEED").map(|s| s.as_str()), Some("45")); + } + + #[test] + fn build_plans_preserves_order() { + let plans = build_plans(&[43, 44, 45], "img", 1.85, 30_000); + assert_eq!(plans.len(), 3); + assert_eq!(plans[0].seed, 43); + assert_eq!(plans[1].seed, 44); + assert_eq!(plans[2].seed, 45); + } + + #[test] + fn gate2_seed_set_accepts_canonical() { + assert!(is_valid_gate2_seed_set(&[43, 44, 45])); + } + + #[test] + fn gate2_seed_set_accepts_superset() { + assert!(is_valid_gate2_seed_set(&[43, 44, 45, 46])); + } + + #[test] + fn gate2_seed_set_rejects_missing_seed() { + assert!(!is_valid_gate2_seed_set(&[43, 44])); + } + + #[test] + fn gate2_seed_set_rejects_empty() { + assert!(!is_valid_gate2_seed_set(&[])); + } + + #[test] + fn embargo_refuses_full_match() { + let emb = vec!["477e3377".to_string()]; + assert!(head_is_embargoed(&emb, "477e3377")); + } + + #[test] + fn embargo_refuses_prefix_match() { + let emb = vec!["477e3377deadbeef".to_string()]; + assert!(head_is_embargoed(&emb, "477e3377")); + } + + #[test] + fn embargo_accepts_clean_sha() { + let emb = vec!["477e3377".to_string(), "b3ee6a36".to_string()]; + assert!(!head_is_embargoed(&emb, "2446855")); + } + + #[test] + fn embargo_skips_comments_and_blanks() { + let emb = vec![ + "# a comment".to_string(), + "".to_string(), + "477e3377".to_string(), + ]; + assert!(head_is_embargoed(&emb, "477e3377")); + assert!(!head_is_embargoed(&emb, "deadbee")); + } + + #[test] + fn login_query_shape() { + let q = build_login_query(); + assert!(q.contains("me { id email }")); + } + + #[test] + fn service_create_body_contains_project_and_image() { + let plan = build_service_plan(43, "ghcr.io/x/y:z", 1.85, 30_000); + let body = build_service_create_body("proj-uuid", &plan); + assert!(body.contains("proj-uuid")); + assert!(body.contains("ghcr.io/x/y:z")); + assert!(body.contains("trainer-seed-43")); + } + + #[test] + fn service_naming_is_stable() { + let a = build_service_plan(43, "img", 1.85, 30_000); + let b = build_service_plan(43, "img", 1.85, 30_000); + assert_eq!(a.name, b.name); + } + + #[test] + fn parse_seed_list_three() { + let seeds = parse_seed_list("43,44,45").unwrap(); + assert_eq!(seeds, vec![43, 44, 45]); + } + + #[test] + fn parse_seed_list_rejects_empty() { + assert!(parse_seed_list("").is_err()); + assert!(parse_seed_list(" , ").is_err()); + } + + #[test] + fn variable_upsert_body_shape() { + let plan = build_service_plan(43, "img", 1.85, 30_000); + let body = build_variable_upsert_body("p", "e", "s", &plan.vars); + assert!(body.contains("variableCollectionUpsert")); + assert!(body.contains("TRIOS_SEED")); + assert!(body.contains("TRIOS_LEDGER_PUSH")); + } +} diff --git a/docs/NOW.md b/docs/NOW.md index 63725558..ddba975a 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -1,38 +1,19 @@ -<<<<<<< Updated upstream # Current Work — Trinity t27 -======= + [PHI Loop CI](https://github.com/gHashTag/t27/actions/workflows/phi-loop-ci.yml) [NOW sync gate](https://github.com/gHashTag/t27/actions/workflows/now-sync-gate.yml) [NOW document](https://github.com/gHashTag/t27/blob/master/docs/NOW.md) [Queen health](https://github.com/gHashTag/t27/blob/master/.trinity/state/queen-health.json) ->>>>>>> Stashed changes - -**Last updated:** 2026-04-14 -**Active:** CI fixes (PR #409) — all workflow YAML fixed, FPGA build passing + DARPA CLARA PA-25-07-02 Submission Package - -<<<<<<< Updated upstream -<<<<<<< Updated upstream -## Active Work -======= -**Last updated:** 2026-04-07 — Tuesday, 07 April 2026 · 00:30 local time (UTC+07) · RFC3339 2026-04-07T00:30:00+07:00 ->>>>>>> Stashed changes -======= -**Last updated:** 2026-04-09 — FPGA pipeline restoration, seal collision fix · PR #344, #336 ->>>>>>> Stashed changes - -**CI Fixes** — All GitHub Actions CI workflows passing (PR #409) -- Workflow YAML syntax errors fixed -- Generated files added for FPGA build -- L1 and L7 compliance met - -<<<<<<< Updated upstream -**DARPA CLARA Submission** — Complete submission package for April 17, 2026 deadline -======= -> *"A specification without tests is a lie told in the future tense."* + +**Last updated:** 2026-04-30 +**Active:** FFI bug fixes (#545-#549), tri igla CLI (#541), pre-commit gate (#332), API completeness + +--- + +> *"A specification without tests is a lie told in the future tense."* > — `SOUL.md` -**Sync gates:** `.githooks/pre-commit` and **phi-loop CI** use `**./scripts/tri check-now`**. The gate compares **calendar date `YYYY-MM-DD`** on the **Last updated** line to **your machine’s local date** when you run `tri` — so write **your wall-clock time** in the header, not UTC, unless you are in UTC. ->>>>>>> Stashed changes +**Sync gates:** `.githooks/pre-commit` and **phi-loop CI** use `**./scripts/tri check-now`**. The gate compares **calendar date `YYYY-MM-DD`** on the **Last updated** line to **your machine's local date** when you run `tri` — so write **your wall-clock time** in the header, not UTC, unless you are in UTC. --- @@ -40,7 +21,7 @@ ### Volume 1: Technical & Management Proposal - **File:** `docs/clara/CLARA-PROPOSAL-TECHNICAL.md` -- **Status:** 1,702 words ≈ 6.8 pages (under 10-page limit) +- **Status:** 1,702 words / 6.8 pages (under 10-page limit) - **Sections:** 1. AR-Based ML Approach (Trit-K3 isomorphism) 2. Application Task Domain + SOA Benchmark @@ -61,8 +42,6 @@ - **File:** `docs/clara/CLARA-EVIDENCE-PACKAGE.md` - **Content:** Formal proofs, numerical evidence, spec coverage, explainability evidence -<<<<<<< Updated upstream -<<<<<<< Updated upstream ### Demo Verification - **Script:** `scripts/clara/demo.sh` - **Status:** 20/20 tests PASSED @@ -70,354 +49,88 @@ --- ## CLARA Requirements Compliance -======= -### § 1.1 Agent handoff — talk to the next agent / Queen via NOW -======= -**Key results:** -- Pellis α⁻¹ = 137.035999164766… matches CODATA 2022 < 0.01 ppb -- Conjecture H2 pre-registered: sin θ₁₃ = φ⁻⁴ ≈ 8.39° vs Daya Bay 8.54° ± 0.15° (~1σ) -- Section 1.1 reserved for Scott Olsen (deadline 2026-04-09) - -**Issues opened:** -- #338 — feat(cli): tri math compare --weinberg -- #339 — feat(cli): hybrid v2 golden tests N=5..152 - -**Verdict:** READY FOR MERGE — Waiting for CI approval - ---- - -## 2026-04-09 — CI stabilization, Yosys synthesis verified, Makefile update ->>>>>>> Stashed changes - -**Canonical URL (SSOT for humans + agents):** -`https://github.com/gHashTag/t27/blob/master/docs/NOW.md` - -<<<<<<< Updated upstream -When you **complete a non-trivial task** (code, specs, CI, seals, architecture docs), **update `NOW.md` before you stop**: - -1. Refresh `**Last updated:`** (calendar `**YYYY-MM-DD**` must match **today** for `./scripts/tri check-now`; keep **local wall time** + **RFC3339 with offset** as in the header template). -2. Fix **§ 3** state, **critical gap**, **links**, or **milestone notes** so the **next agent** reads **current truth**, not yesterday’s story. -3. **Commit `docs/NOW.md` in the same PR** as the work (or amend), per Ring 033 / [#141](https://github.com/gHashTag/t27/issues/141). - -Skipping this is a **failed handoff** — the fleet coordinates here, not only in issues. - -**Recent methodology docs (kernel + experience + formal + science/ops):** -`[KERNEL_AXIOMS_AND_AGENT_EXPERIENCE_PROTOCOL.md](KERNEL_AXIOMS_AND_AGENT_EXPERIENCE_PROTOCOL.md)` · `[KERNEL-PLAN-MULTI-MODEL-SYNTHESIS.md](KERNEL-PLAN-MULTI-MODEL-SYNTHESIS.md)` · `[SCIENCE-OPS-DUAL-TRACK-SYNTHESIS.md](SCIENCE-OPS-DUAL-TRACK-SYNTHESIS.md)` · `[RESEARCH_WRITING_T27.md](RESEARCH_WRITING_T27.md)` · `[TRINITY-EXPERIENCE-EXCHANGE-ARCHITECTURE.md](TRINITY-EXPERIENCE-EXCHANGE-ARCHITECTURE.md)` · `[T27_KERNEL_FORMAL_COQ.md](T27_KERNEL_FORMAL_COQ.md)` · `[COMPILER_VERIFICATION_STANDARDS.md](COMPILER_VERIFICATION_STANDARDS.md)` (deep map + ring plan; index `[COMPILER_VERIFICATION_LANDSCAPE_AND_T27_PLAN.md](COMPILER_VERIFICATION_LANDSCAPE_AND_T27_PLAN.md)`; RU impact `[COMPILER_VERIFICATION_IMPACT_RU.md](COMPILER_VERIFICATION_IMPACT_RU.md)`; TOR/TVP `[qualification/](qualification/)`; template `[templates/TOOL_QUALIFICATION_SKETCH_DO330.md](templates/TOOL_QUALIFICATION_SKETCH_DO330.md)`) · repo `[coq/](../coq/)` (Rocq/Coq scaffold; workflow `.github/workflows/coq-kernel.yml`) - ---- - -## § 2 Invariant law (never changes) - - -| Law | Statement | Enforcement | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| **ISSUE-GATE** | No code merged without `Closes #N` | `.github/workflows/issue-gate.yml` | -| **NO-HAND-EDIT-GEN** | Files under `gen/` are generated; edit the `.t27` spec instead | `./bootstrap/target/release/t27c validate-gen-headers --repo-root .` (or `./scripts/tri` wrapper) | -| **SOUL-ASCII** | All `.t27` / `.zig` / `.v` / `.c` source — ASCII-only identifiers & comments | `SOUL.md`, ADR-004 | -| **TDD-MANDATE** | Every `.t27` spec must contain `test` / `invariant` / `bench` | Ring 037 / [#132](https://github.com/gHashTag/t27/issues/132) | -| **PHI-IDENTITY** | **K2 core:** \varphi^2 = \varphi + 1 on \mathbb{R}; **consequence** \varphi^2+\varphi^{-2}=3; **IEEE `f64`** checks use **tolerance** (not exact equality) | `[NUMERIC-CORE-PALETTE-REGISTRY.md](nona-02-organism/NUMERIC-CORE-PALETTE-REGISTRY.md)`, `specs/math/constants.t27` | -| **TRINITY-SACRED** | `conformance/FORMAT-SPEC-001.json` + `specs/numeric/gf16.t27` are the numeric ceiling | SSOT: never forked | ->>>>>>> Stashed changes | Requirement | Status | Evidence | |-------------|--------|----------| -| AR in guts of ML (FAQ 21) | ✅ | K3 logic gates replace ReLU | -| ≤10 step proof traces | ✅ | MAX_STEPS=10 | -| Polynomial guarantees | ✅ | Theorems 1-5 | -| ≥2 AR kinds | ✅ | Logic, ASP, Classical | -| ≥2 ML kinds | ✅ | Neural, Bayesian, RL | -| Apache 2.0 | ✅ | All file headers | +| AR in guts of ML (FAQ 21) | done | K3 logic gates replace ReLU | +| <=10 step proof traces | done | MAX_STEPS=10 | +| Polynomial guarantees | done | Theorems 1-5 | +| >=2 AR kinds | done | Logic, ASP, Classical | +| >=2 ML kinds | done | Neural, Bayesian, RL | +| Apache 2.0 | done | All file headers | --- ## Specification Status -<<<<<<< Updated upstream -| Category | Specs | Parse Status | -|----------|-------|--------------| -| AR (Automated Reasoning) | 7 | 7/7 PASS | -| NN (Neural Networks) | 2 | 2/2 PASS | -| VSA | 1 | 1/1 PASS | -| **Total** | **10** | **10/10 PASS** | -======= -### 3.1 Sealed artifacts - +### Sealed artifacts | Artifact | Count / version | Last ring | Verdict | | -------------------- | -------------------------------------- | ---------- | ------------------------------------ | -| `.t27` specs | 43 files *(ring narrative)* | Ring 43 | 43/43 parse PASS | -| `gen/zig/` | 52 files *(ring narrative)* | Ring 43 | generated, compile-checked | -| `conformance/` JSON | 62 files *(ring narrative)* | Ring 44 | schema v1 | +| `.t27` specs | 43+ files | Ring 43 | 43/43 parse PASS | +| `gen/zig/` | 52+ files | Ring 43 | generated, compile-checked | +| `conformance/` JSON | 62+ files | Ring 44 | schema v1 | | `stage0/FROZEN_HASH` | SHA-256 of `bootstrap/src/compiler.rs` | genesis | immutable *(if present in checkout)* | -| Experience log | 45 entries *(ring narrative)* | Ring 45 | all `verdict: clean` | +| Experience log | 45+ entries | Ring 45 | all `verdict: clean` | | Queen health | 1.0 / GREEN | 2026-04-05 | 17/17 domains | - -***Re-scan before every commit (do not treat stale counts as SSOT):*** - -```bash -find specs -name "*.t27" | wc -l -find gen/zig -name "*.zig" | wc -l -find conformance -name "*.json" | wc -l -``` - -The **table counts** above are *ring narrative* snapshots; refresh them when you seal a ring. - -### 3.2 Critical open gap +### Critical open gap ``` -bootstrap/src/compiler.rs ─── parse / gen ──→ AST / emit - │ - CI E2E not yet proven: │ - seed.t27 → t27c gen → zig test → GREEN - │ +bootstrap/src/compiler.rs --- parse / gen --> AST / emit + | + CI E2E not yet proven: | + seed.t27 -> t27c gen -> zig test -> GREEN + | gen/zig/*.zig (from t27c, not hand-written) ``` -**The Rust bootstrap** (`t27c parse`, `t27c gen`, `t27c compile`, `t27c suite`) **exists**. -**The closed loop** `seed.t27 → t27c gen → output.zig → zig test → GREEN` has **not yet been demonstrated end-to-end in CI** as a **single advertised pipeline**. -Treat that as the **highest-leverage** gap before Phase 3 (Brain) work is **evidence-grade**. -**Track in issue:** [#150](https://github.com/gHashTag/t27/issues/150) — every PR that implements this loop must use `**Closes #150`** (or a split child issue) per **ISSUE-GATE**. - -**TV reference (`[qualification/TVP.md](qualification/TVP.md)`):** **TV-01** (`tri test` / suite on golden snapshot) — **PENDING** full E2E closure · **TV-02** (regen + blessed hash of `gen/`) — **PENDING** until the same pipeline is wired. See TVP §3 note. - -**K2 fast path (binary64):** For the IEEE literal of \varphi, `**fl(φ·φ)`** and `**fl(φ+1.0)**` are **bit-identical** (`0x4004F1BBCDCBFA54`). So `**phi_identity_contract`** in `coq/Kernel/PhiFloat.v` is `**Rabs(0) < phi_tolerance**` (trivial residual). Mantissa / exponent for Flocq: `**7286977268806824**`, exp `**-52**` — cross-check with `**scripts/validate_phi_f64.py**`. Spec: `[PHI_IDENTITY_FLOCQ_BRIDGE_SPEC.md](nona-03-manifest/PHI_IDENTITY_FLOCQ_BRIDGE_SPEC.md)` · task anchor: `[PHASE_B_FLOCQ_AGENT_TASK.md](nona-03-manifest/PHASE_B_FLOCQ_AGENT_TASK.md)`. - -**Optional formal track:** `[coq/](../coq/)` + `[T27_KERNEL_FORMAL_COQ.md](T27_KERNEL_FORMAL_COQ.md)` — Rocq/Coq scaffold for **K1–K4** (not K5/K6); CI `.github/workflows/coq-kernel.yml` when `**coq/**`** changes. -**K2 / PHI-IDENTITY (summary):** `Kernel/Phi.v` — `Coq.Reals` (`**phi_squared_identity`**, `**phi_tolerance**`). `Kernel/PhiFloat.v` — Flocq `**binary64**`, `**phi_identity_contract**`. Balanced ternary / radix economy context: [#138](https://github.com/gHashTag/t27/issues/138), [#142](https://github.com/gHashTag/t27/issues/142). -**Certification / evidence vocabulary:** `[COMPILER_VERIFICATION_STANDARDS.md](COMPILER_VERIFICATION_STANDARDS.md)` — **DO-178C / DO-330 / DO-333**, ISO 26262 (TCL), IEC 61508 (T1–T3), EN 50716, ECSS-Q-ST-80C, IEC 62304, IEEE 1012, NIST SSDF, CompCert/CakeML/Alive2/Flocq, TVCP **TV-01–TV-07**, phased plan. Quick index: `[COMPILER_VERIFICATION_LANDSCAPE_AND_T27_PLAN.md](COMPILER_VERIFICATION_LANDSCAPE_AND_T27_PLAN.md)`. Draft **TOR/TVP:** `[qualification/TOR.md](qualification/TOR.md)`, `[qualification/TVP.md](qualification/TVP.md)`. - -### 3.3 Compiler verification — impact digest (trust in `t27c`) - -**Question the standards pack answers:** how we **justify trust** in `**t27c`** as a code generator (and in `**coqc**` as proof-checking tooling) using the same vocabulary regulators use (tool qualification, V&V, formal methods). - -**Why it matters for T27** - -- **DO-330 / ISO 26262 / IEC 61508** all force the same discipline: if a tool **writes** product code or **replaces** verification, its failures must be **controlled** with evidence (TOR/TVP/TVCP/TVR/TAS in aviation-shaped programs). -- **DO-178C** aligns with repo law: `**TDD-MANDATE`** ≈ requirements-based testing mindset; `**ISSUE-GATE**` ≈ traceability of change to tracked work. -- **DO-333** is the slot for `**coq/`** (theorem proving); **K2** is proved on `**Reals`** in `Phi.v`; `**PhiFloat.v**` gives the `**f64**` Flocq model + `**phi_identity_contract**` (computational bridge; deeper error lemmas → later ring). -- **IEEE 1012-style V&V planning** implies generator assurance should be **commensurate** with the integrity of the software the generator affects — `**NO-HAND-EDIT-GEN`** enforces SSOT on `**.t27**`, not hand patches in `**gen/**`. -- **NIST SSDF** aligns with **pinned toolchains**, `**FROZEN_HASH`**, and append-only **experience** logs. - -**Immediate blocker (unchanged):** until `**seed.t27 → t27c gen → zig test → GREEN`** runs as **one advertised CI job**, end-to-end “we can show the compiler pipeline works” remains **weaker than** the standards narrative we are writing. That job is **Phase 1 / NOW §5 step 1.5** — **[#150](https://github.com/gHashTag/t27/issues/150)**. - -**Russian full narrative (impact per section):** `[COMPILER_VERIFICATION_IMPACT_RU.md](COMPILER_VERIFICATION_IMPACT_RU.md)` — allowlisted Cyrillic companion; **English SSOT** remains `[COMPILER_VERIFICATION_STANDARDS.md](COMPILER_VERIFICATION_STANDARDS.md)`. ->>>>>>> Stashed changes - ---- - -## Submission Deadline - -<<<<<<< Updated upstream -**April 17, 2026, 16:00 ET** -**Submission Bundle:** `/tmp/clara-submission/` - ---- - -**φ² + 1/φ² = 3 | TRINITY** -======= -**[EPOCH-01-HARDEN](https://github.com/gHashTag/t27/milestone/1)** — Rings 032–049 - - -| Issue | Ring | Domain | Title | -| -------------------------------------------------- | ---- | ------------ | ---------------------------------------------------- | -| [#127](https://github.com/gHashTag/t27/issues/127) | 032 | Tooling | `TASK.md` + iteration schema | -| [#128](https://github.com/gHashTag/t27/issues/128) | 033 | CI | Issue-gate enforcement — every PR `Closes #N` | -| [#129](https://github.com/gHashTag/t27/issues/129) | 034 | Numerics | GoldenFloat benchmark spec (NMSE vs bfloat16) | -| [#130](https://github.com/gHashTag/t27/issues/130) | 035 | Architecture | `TECHNOLOGY-TREE.md` — ring DAG to 999 | -| [#131](https://github.com/gHashTag/t27/issues/131) | 036 | CI | Seal coverage — block PRs with missing SHA-256 | -| [#132](https://github.com/gHashTag/t27/issues/132) | 037 | Language | SOUL.md parser enforcement | -| [#133](https://github.com/gHashTag/t27/issues/133) | 038 | Conformance | Conformance vector schema v2 | -| [#134](https://github.com/gHashTag/t27/issues/134) | 039 | Science | CLARA / DARPA TA1–TA2 submission checklist | -| [#135](https://github.com/gHashTag/t27/issues/135) | 040 | Agents | `AGENTS_ALPHABET.md` — 27 agent definitions | -| [#138](https://github.com/gHashTag/t27/issues/138) | 043 | Math | Balanced ternary addition formal spec | -| [#139](https://github.com/gHashTag/t27/issues/139) | 044 | Protocol | PHI LOOP contract v2 + TOXIC rollback | -| [#140](https://github.com/gHashTag/t27/issues/140) | 045 | ISA | 27 Coptic register invariants | -| [#142](https://github.com/gHashTag/t27/issues/142) | 046 | Math | Radix economy — base-3 optimality proof | -| [#143](https://github.com/gHashTag/t27/issues/143) | 047 | Math | K3 logic truth table — 27-entry isomorphism | -| [#144](https://github.com/gHashTag/t27/issues/144) | 048 | VSA | Trit-space bind/unbind formal spec | -| [#145](https://github.com/gHashTag/t27/issues/145) | 049 | Physics | Sacred physics hard-tolerance conformance | -| [#150](https://github.com/gHashTag/t27/issues/150) | — | CI | E2E CI: `seed.t27` → `t27c gen` → `zig test` → GREEN | - - -*Confirm issue titles with `gh issue view` if links drift.* - -**Also:** `[RING_BACKLOG_047_063.md](RING_BACKLOG_047_063.md)` · `[coordination/ROLLING-INTEGRATION-PLAN-SEED-TO-QUEEN.md](coordination/ROLLING-INTEGRATION-PLAN-SEED-TO-QUEEN.md)` · `[KERNEL-PLAN-MULTI-MODEL-SYNTHESIS.md](KERNEL-PLAN-MULTI-MODEL-SYNTHESIS.md)` · `[SCIENCE-OPS-DUAL-TRACK-SYNTHESIS.md](SCIENCE-OPS-DUAL-TRACK-SYNTHESIS.md)` · `[RESEARCH_WRITING_T27.md](RESEARCH_WRITING_T27.md)` · anchor [#141](https://github.com/gHashTag/t27/issues/141) - ---- - -## § 5 Sequential integration plan: Seed → Tests → Queen - -**Rule:** Complete each phase before expanding the next. -**Every PR must contain** `Closes #N` (Ring 033 / [#128](https://github.com/gHashTag/t27/issues/128)). -**No code without an issue.** - -``` -SEED (bootstrap/Rust) - │ Phase 1 — Law & SSOT - ▼ -STEM (conformance vectors) - │ Phase 2 — Test execution - ▼ -BRANCHES (Ring 050+ science tests) - │ Phase 3 — Math/physics audit - ▼ -CROWN (Queen brain & automation) - Phase 4 — Orchestration -``` - -### Phase 1 — Seed: Law + SSOT + gates *(active now)* - - -| Step | Issue | Action | Acceptance criterion | -| ---- | -------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- | -| 1.1 | [#128](https://github.com/gHashTag/t27/issues/128) | Enable issue-gate CI | All PRs blocked without `Closes #N`; zero bypass | -| 1.2 | [#132](https://github.com/gHashTag/t27/issues/132) | Parser enforces SOUL.md | Spec without `test`/`invariant`/`bench` → error (when enforced) | -| 1.3 | [#127](https://github.com/gHashTag/t27/issues/127) | Canonicalise `TASK.md` + iteration schema | `tri check-now` passes on clean repo | -| 1.4 | — | Verify `FORMAT-SPEC-001.json` + `gf16.t27` as numeric SSOT | Numeric PRs link to these | -| 1.5 | [#150](https://github.com/gHashTag/t27/issues/150) | Document / CI **seed → gen → zig test** | Minimal golden spec path green in CI; PRs `**Closes #150*`* | - - -### Phase 2 — Stem: Conformance + benchmarks + seals *(next)* - - -| Step | Issue | Action | Acceptance criterion | -| ---- | -------------------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------- | -| 2.1 | [#133](https://github.com/gHashTag/t27/issues/133) | Conformance vector schema v2 | `phi_distance` + `verdict` in `gf*_vectors.json` where applicable | -| 2.2 | [#129](https://github.com/gHashTag/t27/issues/129) | GoldenFloat NMSE benchmark | `gf_family_bench.json` semantics documented | -| 2.3 | [#131](https://github.com/gHashTag/t27/issues/131) | Seal coverage CI | PRs touching `specs/` need seal discipline | -| 2.4 | — | GF16 vectors grow | e.g. 10 → 33+ in `gf16_vectors.json` | -| 2.5 | — | Numeric debt sprint | `[NUMERIC-GF16-DEBT-INVENTORY.md](nona-02-organism/NUMERIC-GF16-DEBT-INVENTORY.md)` — math → nn/vsa → ar | - - -**Numeric palette:** `[NUMERIC-STANDARD-001.md](nona-02-organism/NUMERIC-STANDARD-001.md)` · `[NUMERIC-GF16-CANONICAL-PICTURE.md](nona-02-organism/NUMERIC-GF16-CANONICAL-PICTURE.md)` · `[NUMERIC-WHY-NOT-GF16-EVERYWHERE.md](nona-02-organism/NUMERIC-WHY-NOT-GF16-EVERYWHERE.md)` · `[NUMERIC-CORE-PALETTE-REGISTRY.md](nona-02-organism/NUMERIC-CORE-PALETTE-REGISTRY.md)` - -### Phase 3 — Branches: Ring 050+ science tests *(upcoming)* - - -| Ring | Issue | Domain | Key deliverable | -| ---- | ----- | --------------- | ----------------------------------- | -| 050 | open | Math/physics | `specs/test_framework/` per charter | -| 051 | open | Physics (P) | Sacred physics claim audit | -| 052 | open | Conformance (F) | Property-test template | -| 053 | open | Verilog (V) | Bench harness | -| 054 | open | Graph (G) | Graph drift detection | - - -**Charter:** `[T27-MATH-PHYSICS-TEST-FRAMEWORK-SPEC.md](nona-03-manifest/T27-MATH-PHYSICS-TEST-FRAMEWORK-SPEC.md)` -**Claims:** `[RESEARCH_CLAIMS.md](nona-03-manifest/RESEARCH_CLAIMS.md)` · `[CLAIM_TIERS.md](nona-03-manifest/CLAIM_TIERS.md)` - -### Phase 4 — Crown: Metrics → brain seals → Queen *(future)* - - -| Step | Ring | Action | Acceptance criterion | -| ---- | ---- | -------------------------- | --------------------------------------------------------------------------------------------------------- | -| 4.1 | 056 | Verdict export JSON schema | Single schema for Queen tooling | -| 4.2 | — | Brain seal refresh | `.trinity/seals/brain-*.json` from pipeline | -| 4.3 | 047 | Lotus phase automation | `.trinity/queen-brain/summaries/` when job exists | -| 4.4 | — | META dashboard | [#126](https://github.com/gHashTag/t27/issues/126) · `[PINNED_ROADMAP_ISSUE.md](PINNED_ROADMAP_ISSUE.md)` | - - -**Brain artifacts:** `.trinity/seals/brain-*.json` · `.trinity/state/queen-health.json` · `.trinity/experience/clara_track1.jsonl` +**The Rust bootstrap** (`t27c parse`, `t27c gen`, `t27c compile`, `t27c suite`) **exists**. +**The closed loop** `seed.t27 -> t27c gen -> output.zig -> zig test -> GREEN` has **not yet been demonstrated end-to-end in CI** as a **single advertised pipeline**. +Treat that as the **highest-leverage** gap before Phase 3 (Brain) work is **evidence-grade**. +**Track in issue:** [#150](https://github.com/gHashTag/t27/issues/150) --- -## § 6 Matryoshka layer map - - -| Layer | Name | Key files | Integration phase | -| ------ | ------------------ | ------------------------------------------------------------------------ | ----------------- | -| **L0** | **Seed** | `bootstrap/src/compiler.rs`; `stage0/FROZEN_HASH` *if shipped* | genesis | -| **L1** | **Bootstrap** | `bootstrap/src/main.rs`, `bootstrap/main.zig` | Phase 1 | -| **L2** | **Base types** | `specs/base/types.t27`, `specs/base/ops.t27` | Phase 1 | -| **L3** | **Numerics** | `specs/numeric/gf*.t27`, `specs/numeric/tf3.t27` | Phase 2 | -| **L4** | **Math / physics** | `specs/math/constants.t27`, `specs/math/sacred_physics.t27` | Phase 3 | -| **L5** | **Compiler** | `specs/compiler/`, `gen/zig/compiler/` | Phase 1–2 | -| **L6** | **Hardware** | `specs/fpga/`, `specs/isa/registers.t27` | Phase 3 | -| **L7** | **Queen brain** | `specs/queen/lotus.t27`, `specs/nn/hslm.t27`, `specs/vsa/`, `specs/ar/`* | Phase 4 | +## Invariant law (never changes) +| Law | Statement | Enforcement | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| **ISSUE-GATE** | No code merged without `Closes #N` | `.github/workflows/issue-gate.yml` | +| **NO-HAND-EDIT-GEN** | Files under `gen/` are generated; edit the `.t27` spec instead | `./bootstrap/target/release/t27c validate-gen-headers --repo-root .` (or `./scripts/tri` wrapper) | +| **SOUL-ASCII** | All `.t27` / `.zig` / `.v` / `.c` source -- ASCII-only identifiers & comments | `SOUL.md`, ADR-004 | +| **TDD-MANDATE** | Every `.t27` spec must contain `test` / `invariant` / `bench` | Ring 037 / [#132](https://github.com/gHashTag/t27/issues/132) | +| **PHI-IDENTITY** | **K2 core:** phi^2 = phi + 1 on R; **consequence** phi^2+phi^-2=3; **IEEE `f64`** checks use **tolerance** (not exact equality) | `specs/math/constants.t27` | +| **TRINITY-SACRED** | `conformance/FORMAT-SPEC-001.json` + `specs/numeric/gf16.t27` are the numeric ceiling | SSOT: never forked | --- -## § 7 Sync gates and tooling - +## Sync gates and tooling | Gate | Trigger | Checks | Status *(verify in Actions)* | | ------------------- | ------------ | ----------------------------------------- | ------------------------------------------------------------------- | | `pre-commit` | local commit | `tri check-now`; `NOW.md` date | active if hooks installed | | `issue-gate.yml` | PR | `Closes #N` | see badge / Actions | -| `phi-loop-ci.yml` | push | parse / gen / conformance (see workflow) | **⚠️ E2E gap** — [#150](https://github.com/gHashTag/t27/issues/150) | +| `phi-loop-ci.yml` | push | parse / gen / conformance (see workflow) | **E2E gap** -- [#150](https://github.com/gHashTag/t27/issues/150) | | `now-sync-gate.yml` | push | `NOW.md` freshness window | see badge / Actions | | **Conformance** | CI / local | `t27c validate-conformance --repo-root .` | run locally or in CI | | **Gen headers** | CI / local | `t27c validate-gen-headers --repo-root .` | run locally or in CI | - -**Agent sync:** `.trinity/state/github-sync.json` -**Hooks:** `bash scripts/setup-git-hooks.sh` -**Manual:** `./scripts/tri check-now` - --- -## § 8 Document map - - -| Topic | Document | -| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Constitution v1.2 | `[T27-CONSTITUTION.md](T27-CONSTITUTION.md)` | -| Ring log | `.trinity/experience/clara_track1.jsonl` | -| Queen health | `.trinity/state/queen-health.json` | -| Rolling integration detail | `[ROLLING-INTEGRATION-PLAN-SEED-TO-QUEEN.md](coordination/ROLLING-INTEGRATION-PLAN-SEED-TO-QUEEN.md)` | -| Numeric SSOT | `conformance/FORMAT-SPEC-001.json` + `[NUMERIC-STANDARD-001.md](nona-02-organism/NUMERIC-STANDARD-001.md)` | -| Claims registry | `[RESEARCH_CLAIMS.md](nona-03-manifest/RESEARCH_CLAIMS.md)` | -| Math/physics test charter | `[T27-MATH-PHYSICS-TEST-FRAMEWORK-SPEC.md](nona-03-manifest/T27-MATH-PHYSICS-TEST-FRAMEWORK-SPEC.md)` | -| Axiom/theorem format | `[T27-UNIFIED-AXIOM-THEOREM-FORMAT-SYSTEM.md](nona-03-manifest/T27-UNIFIED-AXIOM-THEOREM-FORMAT-SYSTEM.md)` | -| Publications pipeline | `[PUBLICATION_PIPELINE.md](PUBLICATION_PIPELINE.md)` | -| Compiler verification (EN) | `[COMPILER_VERIFICATION_STANDARDS.md](COMPILER_VERIFICATION_STANDARDS.md)` · `[COMPILER_VERIFICATION_LANDSCAPE_AND_T27_PLAN.md](COMPILER_VERIFICATION_LANDSCAPE_AND_T27_PLAN.md)` | -| Compiler verification (RU) | `[COMPILER_VERIFICATION_IMPACT_RU.md](COMPILER_VERIFICATION_IMPACT_RU.md)` (allowlisted; see ADR-004) | -| PHI-IDENTITY Flocq bridge | `[PHI_IDENTITY_FLOCQ_BRIDGE_SPEC.md](nona-03-manifest/PHI_IDENTITY_FLOCQ_BRIDGE_SPEC.md)` | -| Phase B Flocq task anchor | `[PHASE_B_FLOCQ_AGENT_TASK.md](nona-03-manifest/PHASE_B_FLOCQ_AGENT_TASK.md)` | -| φ / f64 validation script | `[scripts/validate_phi_f64.py](../scripts/validate_phi_f64.py)` | -| Roadmap umbrella | [#126](https://github.com/gHashTag/t27/issues/126) | - +**Canonical URL (SSOT for humans + agents):** +`https://github.com/gHashTag/t27/blob/master/docs/NOW.md` --- -## § 9 Next actions (48 h) - -**Priority:** Close **E2E** `seed.t27 → t27c gen → zig test → GREEN` in **phi-loop CI** — **[#150](https://github.com/gHashTag/t27/issues/150)** (see **§3.2–3.3**, **§5 Phase 1 step 1.5**). Everything else is secondary until that loop is green. - -```bash -# 0. NOW gate — run FIRST before any commit (otherwise push / hooks may fail) -./scripts/tri check-now - -# 1. E2E CI issue (created — link PRs with Closes #150) -# gh issue view 150 - -# 2. Milestone hygiene (needs gh auth) -# gh issue edit 127 128 129 130 131 132 133 --milestone "EPOCH-01-HARDEN" - -# 3. Bootstrap + suite -cd bootstrap && cargo build --release -./target/release/t27c validate-conformance --repo-root .. -./target/release/t27c validate-gen-headers --repo-root .. -./target/release/t27c suite --repo-root .. - -# 4. Optional: compiler hash (if stage0/FROZEN_HASH exists in your tree) -# shasum -a 256 bootstrap/src/compiler.rs - -# 5. Experience log — only after a real run -# echo '{"ring":46,"task":"…","verdict":"clean","timestamp":"2026-04-06T12:00:00Z"}' >> .trinity/experience/clara_track1.jsonl - -# 6. gh issue comment 126 --body "…" -``` +**Recent fixes (2026-04-30):** +- FFI: GF16 encode round-to-nearest-even + overflow to +Inf (Closes #545, #546, #547) +- FFI: GF4/GF8/GF12/GF20/GF24 encode/decode with round-to-nearest-even (Closes #549) +- FFI: GF32 [1:13:18] Lucas L6 layout verified (Closes #548) +- CLI: tri igla search/list/gate/check/triplet subcommands (Closes #541) --- -*Living documentation corpus · `[T27-CONSTITUTION.md](T27-CONSTITUTION.md)` v1.2, Article DOCS-TREE · **Last updated** must include **calendar date** `YYYY-MM-DD` (for `tri check-now`). Prefer **human-readable local wall time** plus optional **RFC3339 with offset** (e.g. `2026-04-06T18:45:00+07:00`) so tools can echo it — do not require UTC `Z` unless you work in UTC.* ->>>>>>> Stashed changes -======= -**Last updated:** 2026-04-09 — ALL 5 FPGA modules in Yosys synthesis (MAC+UART+SPI+Bridge+TopLevel) · Issue #367 +*Living documentation corpus | Last updated must include calendar date YYYY-MM-DD (for tri check-now).* -*This is a partial update for PR #337. Integrate into full NOW.md after merge.* -Last updated: 2026-04-09 ->>>>>>> Stashed changes +**phi^2 + phi^-2 = 3 | TRINITY** diff --git a/external/kaggle/scripts/generate_tefb_mc.py b/external/kaggle/scripts/generate_tefb_mc.py new file mode 100644 index 00000000..b96a291d --- /dev/null +++ b/external/kaggle/scripts/generate_tefb_mc.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +""" +Generate TEFB (Theory of Emotion and False Belief) Multiple Choice format. + +Creates MC questions probing emotion recognition and false belief understanding: +- Emotion Attribution: Scenario + character emotional state query +- False Belief Emotion: Classic Sally-Anne style with emotional valence +- Desire-Based Emotion: Desire fulfillment vs frustration +- Mixed Emotions: Conflicting emotional responses +- Emotion Regulation: Coping strategy appropriateness +""" + +import random +from pathlib import Path +from typing import List, Dict, Any +import sys + +sys.path.insert(0, str(Path(__file__).parent)) + +from mc_generator_utils import ( + CSVWriter, generate_qid, format_mc_question, + get_random_item, print_summary, set_seed +) + +OUTPUT_CSV = Path(__file__).parent.parent / "data" / "tefb_mc.csv" +QUESTIONS_PER_TYPE = 480 +SEED = 42 + +EMOTIONS = ["happy", "sad", "angry", "surprised", "scared", "disgusted", "proud", "embarrassed", "jealous", "relieved"] +POSITIVE_EMOTIONS = ["happy", "proud", "relieved", "surprised"] +NEGATIVE_EMOTIONS = ["sad", "angry", "scared", "disgusted", "embarrassed", "jealous"] +NAMES = ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace", "Hank", "Ivy", "Jack"] +OBJECTS = ["toy", "book", "phone", "keys", "wallet", "snack", "gift", "photo", "letter", "medal"] +LOCATIONS = ["kitchen", "bedroom", "garden", "school", "park", "store", "library", "office"] + +EMOTION_ATTRIBUTION_TEMPLATES = [ + { + "scenario": "{name} just won first prize in a competition they trained hard for.", + "answer": "proud", + "distractors": ["scared", "disgusted", "jealous"] + }, + { + "scenario": "{name} dropped their ice cream cone right after buying it.", + "answer": "sad", + "distractors": ["proud", "happy", "surprised"] + }, + { + "scenario": "{name} heard a strange noise in the dark basement alone.", + "answer": "scared", + "distractors": ["proud", "happy", "disgusted"] + }, + { + "scenario": "{name} found out their best friend lied to them.", + "answer": "angry", + "distractors": ["happy", "proud", "relieved"] + }, + { + "scenario": "{name} walked into a room and everyone yelled 'Surprise!'", + "answer": "surprised", + "distractors": ["angry", "disgusted", "sad"] + }, + { + "scenario": "{name} saw someone stealing from a charity donation box.", + "answer": "disgusted", + "distractors": ["proud", "happy", "relieved"] + }, + { + "scenario": "{name} accidentally called their teacher 'Mom' in front of the class.", + "answer": "embarrassed", + "distractors": ["proud", "happy", "scared"] + }, + { + "scenario": "{name}'s sibling got a brand new bike while they got nothing.", + "answer": "jealous", + "distractors": ["proud", "happy", "disgusted"] + }, + { + "scenario": "{name} finally finished a very difficult exam they worried about for weeks.", + "answer": "relieved", + "distractors": ["jealous", "disgusted", "angry"] + }, + { + "scenario": "{name} just got a puppy they had been wishing for all year.", + "answer": "happy", + "distractors": ["sad", "angry", "scared"] + }, +] + +FALSE_BELIEF_TEMPLATES = [ + { + "scenario": "{name} puts their {obj} in the {loc1} and leaves. Someone moves it to the {loc2}. {name} comes back wanting their {obj}. How will {name} feel when they look in the {loc1} first?", + "answer": "surprised or confused", + "distractors": ["happy they found it", "angry at themselves", "proud of looking"] + }, + { + "scenario": "{name} thinks their friend is {loc1}, but the friend actually went to {loc2}. Where will {name} look first?", + "answer": "The {loc1} (where they believe the friend is)", + "distractors": ["The {loc2} (actual location)", "Nowhere", "Both places at once"] + }, + { + "scenario": "{name} sees a candy box but inside there are pencils. Before opening it, what does {name} think is inside?", + "answer": "Candy", + "distractors": ["Pencils", "Nothing", "Rocks"] + }, + { + "scenario": "{name} watches mom put cookies in the jar. Mom leaves and dad eats some. When mom returns, how many cookies does she think are in the jar?", + "answer": "The original number (she doesn't know dad ate some)", + "distractors": ["Zero", "The reduced number", "Double the original"] + }, +] + +DESIRE_TEMPLATES = [ + { + "scenario": "{name} really wants a {obj} for their birthday. They receive exactly that.", + "answer": "happy and satisfied", + "distractors": ["disappointed", "angry", "confused"] + }, + { + "scenario": "{name} wants to go to the {loc1} but it's closed. They end up going to the {loc2} instead, which they don't enjoy.", + "answer": "frustrated and disappointed", + "distractors": ["thrilled", "proud", "relieved"] + }, + { + "scenario": "{name} didn't want to go to the party. They were forced to go but ended up having a great time.", + "answer": "pleasantly surprised", + "distractors": ["still angry", "sad", "scared"] + }, + { + "scenario": "{name} wanted to win the race. They came in second place.", + "answer": "disappointed but slightly proud", + "distractors": ["completely devastated", "angry at others", "disgusted"] + }, +] + +MIXED_EMOTION_TEMPLATES = [ + { + "scenario": "{name} is moving to a new city. They're excited about new opportunities but will miss their old friends.", + "answer": "a mix of happiness and sadness", + "distractors": ["purely happy", "purely sad", "no feelings at all"] + }, + { + "scenario": "{name}'s team won the championship, but {name} got injured during the final game.", + "answer": "proud of the team but worried about the injury", + "distractors": ["only happy", "only sad", "completely angry"] + }, + { + "scenario": "{name} graduated top of their class, but their grandmother couldn't attend the ceremony.", + "answer": "proud but also sad", + "distractors": ["only happy", "only angry", "only scared"] + }, + { + "scenario": "{name} finally stood up to a bully (scary but empowering).", + "answer": "brave but also frightened", + "distractors": ["only scared", "only happy", "only disgusted"] + }, +] + +REGULATION_TEMPLATES = [ + { + "scenario": "{name} is very angry after losing a game. Which strategy is most helpful?", + "answer": "Taking deep breaths and counting to ten", + "distractors": ["Yelling at the opponent", "Quitting all games forever", "Breaking the game pieces"] + }, + { + "scenario": "{name} feels nervous before a big presentation. What should they do?", + "answer": "Practice deep breathing and visualize success", + "distractors": ["Skip the presentation", "Eat a lot of junk food", "Tell everyone they're scared"] + }, + { + "scenario": "{name} is sad because their pet ran away. What's a healthy way to cope?", + "answer": "Talk to a trusted friend about their feelings", + "distractors": ["Never talk about it", "Pretend it doesn't matter", "Get angry at everyone"] + }, + { + "scenario": "{name} feels overwhelmed with homework. What's the best approach?", + "answer": "Break it into small tasks and take breaks", + "distractors": ["Do nothing", "Stay up all night panicking", "Copy from a friend"] + }, +] + + +def generate_emotion_attribution(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(EMOTION_ATTRIBUTION_TEMPLATES) + name = random.choice(NAMES) + question = tmpl["scenario"].format(name=name) + row = format_mc_question( + generate_qid("tefb", "emotion_attr", i + 1), + question, + tmpl["answer"], + tmpl["distractors"], + ) + rows.append(row) + return rows + + +def generate_false_belief(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(FALSE_BELIEF_TEMPLATES) + name = random.choice(NAMES) + obj = random.choice(OBJECTS) + locs = random.sample(LOCATIONS, 2) + question = tmpl["scenario"].format(name=name, obj=obj, loc1=locs[0], loc2=locs[1]) + row = format_mc_question( + generate_qid("tefb", "false_belief", i + 1), + question, + tmpl["answer"], + tmpl["distractors"], + ) + rows.append(row) + return rows + + +def generate_desire_based(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(DESIRE_TEMPLATES) + name = random.choice(NAMES) + obj = random.choice(OBJECTS) + locs = random.sample(LOCATIONS, 2) + question = tmpl["scenario"].format(name=name, obj=obj, loc1=locs[0], loc2=locs[1]) + row = format_mc_question( + generate_qid("tefb", "desire", i + 1), + question, + tmpl["answer"], + tmpl["distractors"], + ) + rows.append(row) + return rows + + +def generate_mixed_emotions(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(MIXED_EMOTION_TEMPLATES) + name = random.choice(NAMES) + question = tmpl["scenario"].format(name=name) + row = format_mc_question( + generate_qid("tefb", "mixed", i + 1), + question, + tmpl["answer"], + tmpl["distractors"], + ) + rows.append(row) + return rows + + +def generate_emotion_regulation(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(REGULATION_TEMPLATES) + name = random.choice(NAMES) + question = tmpl["scenario"].format(name=name) + row = format_mc_question( + generate_qid("tefb", "regulation", i + 1), + question, + tmpl["answer"], + tmpl["distractors"], + ) + rows.append(row) + return rows + + +def main(): + set_seed(SEED) + stats = {"total": 0, "by_type": {}, "by_answer": {"A": 0, "B": 0, "C": 0, "D": 0}} + + generators = [ + ("emotion_attribution", generate_emotion_attribution), + ("false_belief", generate_false_belief), + ("desire_based", generate_desire_based), + ("mixed_emotions", generate_mixed_emotions), + ("emotion_regulation", generate_emotion_regulation), + ] + + with CSVWriter(OUTPUT_CSV) as writer: + for name, gen_fn in generators: + rows = gen_fn(QUESTIONS_PER_TYPE) + writer.write_rows(rows) + stats["by_type"][name] = len(rows) + stats["total"] += len(rows) + for row in rows: + stats["by_answer"][row["answer"]] += 1 + + print_summary("TEFB MC Dataset Generation Complete", OUTPUT_CSV, stats) + + +if __name__ == "__main__": + main() diff --git a/external/kaggle/scripts/generate_tscp_mc.py b/external/kaggle/scripts/generate_tscp_mc.py new file mode 100644 index 00000000..99d6457e --- /dev/null +++ b/external/kaggle/scripts/generate_tscp_mc.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +""" +Generate TSCP (Theory of Social Cognition Probes) Multiple Choice format. + +Creates MC questions probing social cognition and theory of mind: +- Perspective Taking: Visual / informational perspective differences +- Social Norms: Norm appropriateness judgments +- Intentionality: Accidental vs intentional action classification +- Social Prediction: Predicting others' behavior from context +- Deception Detection: Identifying deceptive vs honest statements +""" + +import random +from pathlib import Path +from typing import List, Dict, Any +import sys + +sys.path.insert(0, str(Path(__file__).parent)) + +from mc_generator_utils import ( + CSVWriter, generate_qid, format_mc_question, + get_random_item, print_summary, set_seed +) + +OUTPUT_CSV = Path(__file__).parent.parent / "data" / "tscp_mc.csv" +QUESTIONS_PER_TYPE = 480 +SEED = 42 + +NAMES = ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace", "Hank", "Ivy", "Jack"] +OBJECTS = ["red ball", "blue book", "green cup", "yellow pen", "purple hat"] +LOCATIONS = ["kitchen", "bedroom", "garden", "school", "park", "office", "library", "store"] + +PERSPECTIVE_TEMPLATES = [ + { + "scenario": "{name_a} is standing on one side of a car. {name_b} is standing on the other side. The car has a dent only on {name_b}'s side. Can {name_a} see the dent?", + "answer": "No, it's on the opposite side", + "distractors": ["Yes, it's obvious", "Only if they're tall enough", "Only with binoculars"] + }, + { + "scenario": "{name_a} has headphones on and can't hear {name_b} talking. {name_b} says 'The answer is 42'. What does {name_a} know about what {name_b} said?", + "answer": "Nothing, they can't hear", + "distractors": ["The answer is 42", "That someone is talking", "The first word only"] + }, + { + "scenario": "{name_a} looks into a box and sees a {obj}. {name_b} hasn't looked. What does {name_b} know about the box contents?", + "answer": "They don't know what's inside", + "distractors": ["They know it's a {obj}", "They know the box is empty", "They know it's something small"] + }, + { + "scenario": "{name_a} is looking at a picture that appears to show a vase. {name_b} sees two faces facing each other. They are looking at the same image. Why do they see different things?", + "answer": "The image is ambiguous and can be seen both ways", + "distractors": ["One of them needs glasses", "The picture changed", "They're looking at different images"] + }, +] + +NORM_TEMPLATES = [ + { + "scenario": "{name} starts singing loudly in a quiet library. Is this appropriate?", + "answer": "No, it violates the social norm of being quiet in a library", + "distractors": ["Yes, singing is always OK", "Only if the song is good", "Only if others join in"] + }, + { + "scenario": "{name} holds the door open for someone carrying heavy boxes. Is this socially appropriate?", + "answer": "Yes, it's a kind and expected social gesture", + "distractors": ["No, it's intrusive", "Only if asked first", "Only for friends"] + }, + { + "scenario": "{name} takes the last slice of pizza without asking anyone else at the table. Is this appropriate?", + "answer": "No, it's polite to offer it to others first", + "distractors": ["Yes, first come first served", "Only if they're very hungry", "Only if they paid for it"] + }, + { + "scenario": "{name} texts during a movie in a theater. Is this appropriate?", + "answer": "No, the screen light disturbs others", + "distractors": ["Yes, it's silent", "Only if the movie is boring", "Only brief messages"] + }, + { + "scenario": "{name} says 'thank you' when receiving a gift. Is this following social norms?", + "answer": "Yes, expressing gratitude is expected", + "distractors": ["No, it's unnecessary", "Only for expensive gifts", "Only from strangers"] + }, +] + +INTENTIONALITY_TEMPLATES = [ + { + "scenario": "{name} accidentally bumps into someone while walking and immediately says sorry. Was the bump intentional?", + "answer": "No, it was accidental", + "distractors": ["Yes, they wanted to bump them", "Partially intentional", "Cannot be determined"] + }, + { + "scenario": "{name} deliberately steps on someone's foot during an argument. Was this intentional?", + "answer": "Yes, it was done on purpose during conflict", + "distractors": ["No, it was an accident", "Only partially intentional", "Depends on the foot size"] + }, + { + "scenario": "{name} breaks a vase while cleaning. They didn't mean to but were being careless. Was this intentional?", + "answer": "No, but it involved negligence", + "distractors": ["Yes, they wanted to break it", "It was purely accidental with no fault", "It was planned"] + }, + { + "scenario": "{name} 'forgets' to invite their ex-friend to a party, even though they invited everyone else. Was the exclusion intentional?", + "answer": "Yes, the selective forgetting suggests intent", + "distractors": ["No, they genuinely forgot", "It was random chance", "The ex-friend wouldn't come anyway"] + }, +] + +SOCIAL_PREDICTION_TEMPLATES = [ + { + "scenario": "{name_a} always helps others with homework. {name_b} never shares notes. If {name_a} needs help, who is more likely to get help from others?", + "answer": "{name_a}, because reciprocity from past helpful behavior", + "distractors": ["{name_b}, because they keep their notes private", "Both equally", "Neither, people are selfish"] + }, + { + "scenario": "{name} has been practicing piano every day for a year. They have a recital tomorrow. How are they likely to perform?", + "answer": "Well, consistent practice usually leads to good performance", + "distractors": ["Poorly, they must be tired", "Average, practice doesn't help", "Terribly, stage fright always wins"] + }, + { + "scenario": "{name} ate a huge meal an hour ago. Someone offers them a big slice of cake. What is {name} most likely to do?", + "answer": "Decline or eat only a small amount because they're full", + "distractors": ["Eat the whole cake eagerly", "Eat exactly half", "Always say yes regardless of hunger"] + }, + { + "scenario": "Every time it rains, {name} carries an umbrella. Today is sunny with no clouds. Will {name} bring an umbrella?", + "answer": "Unlikely, they respond to weather conditions", + "distractors": ["Yes, they always carry one", "Yes, they're paranoid about rain", "It's impossible to predict"] + }, +] + +DECEPTION_TEMPLATES = [ + { + "scenario": "{name} says 'I love your haircut!' but rolls their eyes when the other person turns away. Is this statement honest?", + "answer": "No, the eye roll contradicts the verbal statement", + "distractors": ["Yes, they said it out loud", "Partially honest", "Cannot tell"] + }, + { + "scenario": "{name} finds a wallet and turns it in to the lost-and-found without keeping any money. They tell their friend 'I found a wallet and turned it in.' Is this honest?", + "answer": "Yes, their statement matches their actions", + "distractors": ["No, they probably kept something", "Partially honest", "Only honest if someone saw them"] + }, + { + "scenario": "A salesperson says 'This is the lowest price anywhere!' but {name} found it cheaper at two other stores. What should {name} conclude?", + "answer": "The salesperson is being deceptive", + "distractors": ["The salesperson is correct", "The other stores are lying", "All prices are the same really"] + }, + { + "scenario": "{name_a} tells {name_b} they're 'fine' but their voice is shaking and they're crying. Should {name_b} believe the words or the behavior?", + "answer": "The behavior, non-verbal cues often reveal true feelings", + "distractors": ["The words, people mean what they say", "Neither, ask a third person", "Both equally"] + }, +] + + +def fill_template(tmpl: Dict, name_a: str = None, name_b: str = None, obj: str = None) -> str: + kwargs = {} + if name_a: + kwargs["name_a"] = name_a + if name_b: + kwargs["name_b"] = name_b + if "{name}" in tmpl["scenario"]: + kwargs["name"] = name_a or random.choice(NAMES) + if obj: + kwargs["obj"] = obj + return tmpl["scenario"].format(**kwargs) + + +def generate_perspective(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(PERSPECTIVE_TEMPLATES) + name_a, name_b = random.sample(NAMES, 2) + obj = random.choice(OBJECTS) + question = fill_template(tmpl, name_a, name_b, obj) + rows.append(format_mc_question( + generate_qid("tscp", "perspective", i + 1), + question, tmpl["answer"], tmpl["distractors"], + )) + return rows + + +def generate_social_norms(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(NORM_TEMPLATES) + name = random.choice(NAMES) + question = tmpl["scenario"].format(name=name) + rows.append(format_mc_question( + generate_qid("tscp", "norms", i + 1), + question, tmpl["answer"], tmpl["distractors"], + )) + return rows + + +def generate_intentionality(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(INTENTIONALITY_TEMPLATES) + name = random.choice(NAMES) + question = tmpl["scenario"].format(name=name) + rows.append(format_mc_question( + generate_qid("tscp", "intentionality", i + 1), + question, tmpl["answer"], tmpl["distractors"], + )) + return rows + + +def generate_social_prediction(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(SOCIAL_PREDICTION_TEMPLATES) + names = random.sample(NAMES, 2) + question = tmpl["scenario"].format(name_a=names[0], name_b=names[1], name=names[0]) + answer = tmpl["answer"].format(name_a=names[0], name_b=names[1]) + distractors = [d.format(name_a=names[0], name_b=names[1]) for d in tmpl["distractors"]] + rows.append(format_mc_question( + generate_qid("tscp", "prediction", i + 1), + question, answer, distractors, + )) + return rows + + +def generate_deception(num: int) -> List[Dict[str, Any]]: + rows = [] + for i in range(num): + tmpl = random.choice(DECEPTION_TEMPLATES) + names = random.sample(NAMES, 2) + question = tmpl["scenario"].format(name_a=names[0], name_b=names[1], name=names[0]) + rows.append(format_mc_question( + generate_qid("tscp", "deception", i + 1), + question, tmpl["answer"], tmpl["distractors"], + )) + return rows + + +def main(): + set_seed(SEED) + stats = {"total": 0, "by_type": {}, "by_answer": {"A": 0, "B": 0, "C": 0, "D": 0}} + + generators = [ + ("perspective_taking", generate_perspective), + ("social_norms", generate_social_norms), + ("intentionality", generate_intentionality), + ("social_prediction", generate_social_prediction), + ("deception_detection", generate_deception), + ] + + with CSVWriter(OUTPUT_CSV) as writer: + for name, gen_fn in generators: + rows = gen_fn(QUESTIONS_PER_TYPE) + writer.write_rows(rows) + stats["by_type"][name] = len(rows) + stats["total"] += len(rows) + for row in rows: + stats["by_answer"][row["answer"]] += 1 + + print_summary("TSCP MC Dataset Generation Complete", OUTPUT_CSV, stats) + + +if __name__ == "__main__": + main() diff --git a/external/kaggle/scripts/validate_datasets.py b/external/kaggle/scripts/validate_datasets.py new file mode 100644 index 00000000..2ca03c02 --- /dev/null +++ b/external/kaggle/scripts/validate_datasets.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +Validate all MC dataset CSV files. + +Checks format, answer distribution, question uniqueness, and produces summary stats. +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from mc_generator_utils import QuestionValidator + +DATA_DIR = Path(__file__).parent.parent / "data" + +EXPECTED_FILES = { + "tefb_mc.csv": {"min_rows": 200, "track": "tefb"}, + "tscp_mc.csv": {"min_rows": 200, "track": "tscp"}, + "thlp_mc_new.csv": {"min_rows": 200, "track": "thlp"}, + "ttm_mc.csv": {"min_rows": 200, "track": "ttm"}, +} + + +def main(): + all_valid = True + + for filename, config in EXPECTED_FILES.items(): + csv_path = DATA_DIR / filename + print(f"\n--- Validating {filename} ---") + + if not csv_path.exists(): + print(f" MISSING: {csv_path} not found") + all_valid = False + continue + + result = QuestionValidator.validate_dataset(csv_path) + + print(f" Valid: {result['valid']}") + print(f" Total questions: {result['stats']['total']}") + + if result['stats']['total'] < config['min_rows']: + print(f" WARNING: Expected at least {config['min_rows']} rows") + all_valid = False + + for answer, count in sorted(result['stats']['by_answer'].items()): + total = result['stats']['total'] + pct = count / total * 100 if total > 0 else 0 + print(f" {answer}: {count} ({pct:.1f}%)") + + if result['errors']: + for err in result['errors'][:5]: + print(f" ERROR: {err}") + if len(result['errors']) > 5: + print(f" ... and {len(result['errors']) - 5} more errors") + + if not result['valid']: + all_valid = False + + print(f"\n{'='*60}") + if all_valid: + print("ALL DATASETS VALID") + return 0 + else: + print("SOME DATASETS FAILED VALIDATION") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 23a946f8..a926962c 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -10,11 +10,13 @@ const GF16_EXP_BIAS: i32 = 31; const GF16_EXP_BITS: u32 = 6; const GF16_MANT_BITS: u32 = 9; -// ─── GF32 constants (8-bit exp, 23-bit mant — same as IEEE but φ-exponent) ── +// ─── GF32 constants (13-bit exp, 18-bit mant — [1:13:18] Lucas L₆) ────────── const GF32_SIGN_BIT: u32 = 1 << 31; -const GF32_EXP_MASK: u32 = 0x7F80_0000; -const GF32_MANT_MASK: u32 = 0x007F_FFFF; -const GF32_EXP_BIAS: i32 = 127; +const GF32_EXP_BITS: u32 = 13; +const GF32_MANT_BITS: u32 = 18; +const GF32_EXP_MASK: u32 = ((1 << GF32_EXP_BITS) - 1) << GF32_MANT_BITS; // bits [30:18] +const GF32_MANT_MASK: u32 = (1 << GF32_MANT_BITS) - 1; // bits [17:0] +const GF32_EXP_BIAS: i32 = (1 << (GF32_EXP_BITS - 1)) - 1; // 4095 // ═════════════════════════════════════════════════════════════════════════════ // GF16 ENCODE/DECODE — pure integer, FPGA-synthesizable @@ -59,16 +61,31 @@ fn encode_gf16_from_u32(f32_bits: u32) -> u16 { // Re-bias exponent for GF16 let gf16_exp_raw: i32 = exp + GF16_EXP_BIAS; + if gf16_exp_raw >= ((1 << GF16_EXP_BITS) - 1) as i32 { + return (sign << 15) | GF16_EXP_MASK; + } let gf16_exp: u16 = if gf16_exp_raw < 0 { 0 - } else if gf16_exp_raw > ((1 << GF16_EXP_BITS) - 1) as i32 { - (1 << GF16_EXP_BITS) - 1 } else { gf16_exp_raw as u16 }; - // Truncate mantissa: 23 bits → 9 bits (right shift by 14) - let gf16_mant: u16 = (mant >> 14) as u16; + // Round-to-nearest-even: 23 bits → 9 bits + let lower_14_bits = mant & 0x3FFF; + let halfway: u32 = 0x2000; + let mut gf16_mant: u16 = (mant >> 14) as u16; + let round_up = lower_14_bits > halfway + || (lower_14_bits == halfway && (gf16_mant & 1) == 1); + if round_up { + gf16_mant += 1; + if gf16_mant == (1 << GF16_MANT_BITS) { + let new_exp = gf16_exp + 1; + if new_exp >= ((1 << GF16_EXP_BITS) - 1) as u16 { + return (sign << 15) | GF16_EXP_MASK; + } + return (sign << 15) | (new_exp << GF16_MANT_BITS as u16); + } + } (sign << 15) | (gf16_exp << GF16_MANT_BITS as u16) | gf16_mant } @@ -211,20 +228,97 @@ pub extern "C" fn gf16_to_f64(value: u16) -> f64 { } // ═══════════════════════════════════════════════════════════════════════════════════ -// GF32 — same pattern, higher precision (8-bit exp, 23-bit mant) +// GF32 ENCODE/DECODE — [1:13:18] Lucas L₆ layout, pure integer // ═════════════════════════════════════════════════════════════════════════════ -/// FPGA-ALLOWED: f64 at boundary; extract u64 bits then truncate to f32 range. +/// Encode f64 → GF32 via integer bit manipulation. Layout: [sign:1][exp:13][mant:18]. +/// FPGA-ALLOWED: f64 only at API boundary; .to_bits() extracts u64 immediately. #[no_mangle] pub extern "C" fn gf32_from_f64(x: f64) -> u32 { - let f32_val = x as f32; // FPGA-ALLOWED - f32_val.to_bits() // GF32 uses same layout as IEEE f32 + φ-exp mapping + let bits: u64 = x.to_bits(); + encode_gf32_from_u64(bits) } -/// FPGA-ALLOWED: f64 at API boundary only. +/// Decode GF32 → f64 via integer bit reconstruction. +/// FPGA-ALLOWED: f64 only at API boundary exit; from_bits() reconstructs from u64. #[no_mangle] pub extern "C" fn gf32_to_f64(value: u32) -> f64 { - f32::from_bits(value) as f64 // FPGA-ALLOWED + let bits: u64 = decode_gf32_to_u64(value); + f64::from_bits(bits) +} + +/// Integer-only GF32 encode — [1:13:18] Lucas L₆ layout. +#[inline(always)] +fn encode_gf32_from_u64(f64_bits: u64) -> u32 { + let sign: u32 = ((f64_bits >> 63) & 1) as u32; + let exp: i32 = ((f64_bits >> 52) & 0x7FF) as i32 - 1023; + let mant: u64 = f64_bits & 0x000F_FFFF_FFFF_FFFF; + + if (f64_bits & 0x7FFF_FFFF_FFFF_FFFF) == 0 { + return sign << 31; + } + if (f64_bits & 0x7FF0_0000_0000_0000) == 0x7FF0_0000_0000_0000 { + if mant == 0 { + return (sign << 31) | GF32_EXP_MASK; + } else { + return (sign << 31) | GF32_EXP_MASK | 1; + } + } + + let gf32_exp_raw: i32 = exp + GF32_EXP_BIAS; + if gf32_exp_raw >= ((1 << GF32_EXP_BITS) - 1) as i32 { + return (sign << 31) | GF32_EXP_MASK; + } + let gf32_exp: u32 = if gf32_exp_raw < 0 { + 0 + } else { + gf32_exp_raw as u32 + }; + + // Round-to-nearest-even: 52 bits → 18 bits (drop 34 bits) + let shift: u32 = 52 - GF32_MANT_BITS; // 34 + let lower_bits = mant & ((1u64 << shift) - 1); + let halfway: u64 = 1u64 << (shift - 1); + let mut gf32_mant: u32 = (mant >> shift) as u32; + let round_up = lower_bits > halfway + || (lower_bits == halfway && (gf32_mant & 1) == 1); + if round_up { + gf32_mant += 1; + if gf32_mant == (1 << GF32_MANT_BITS) { + let new_exp = gf32_exp + 1; + if new_exp >= ((1 << GF32_EXP_BITS) - 1) as u32 { + return (sign << 31) | GF32_EXP_MASK; + } + return (sign << 31) | (new_exp << GF32_MANT_BITS); + } + } + + (sign << 31) | (gf32_exp << GF32_MANT_BITS) | gf32_mant +} + +/// Integer-only GF32 decode — reconstructs f64 bits from GF32 [1:13:18] bits. +#[inline(always)] +fn decode_gf32_to_u64(gf32: u32) -> u64 { + let sign: u64 = ((gf32 as u64) >> 31) & 1; + let exp: u64 = ((gf32 as u64) >> GF32_MANT_BITS) & ((1 << GF32_EXP_BITS) - 1); + let mant: u64 = (gf32 as u64) & (GF32_MANT_MASK as u64); + + let exp_max: u64 = (1 << GF32_EXP_BITS) - 1; + if exp == exp_max { + if mant == 0 { + return (sign << 63) | 0x7FF0_0000_0000_0000; + } else { + return 0x7FF8_0000_0000_0000; + } + } + if exp == 0 && mant == 0 { + return sign << 63; + } + + let ieee_exp: u64 = ((exp as i64 - GF32_EXP_BIAS as i64 + 1023) as u64) & 0x7FF; + let ieee_mant: u64 = mant << (52 - GF32_MANT_BITS); // 18 → 52 bits + + (sign << 63) | (ieee_exp << 52) | ieee_mant } #[no_mangle] @@ -248,8 +342,8 @@ pub extern "C" fn gf32_extract_sign(value: u32) -> u8 { } #[no_mangle] -pub extern "C" fn gf32_extract_exponent(value: u32) -> u8 { - ((value >> 23) & 0xFF) as u8 +pub extern "C" fn gf32_extract_exponent(value: u32) -> u16 { + ((value >> GF32_MANT_BITS) & ((1 << GF32_EXP_BITS) - 1)) as u16 } #[no_mangle] @@ -258,40 +352,38 @@ pub extern "C" fn gf32_extract_mantissa(value: u32) -> i32 { } // ═════════════════════════════════════════════════════════════════════════════════════ -// GF32 ARITHMETIC — same pattern as GF16 -// ═════════════════════════════════════════════════════════════════════════════════ +// GF32 ARITHMETIC — decode→f64 op→encode via [1:13:18] layout +// ═══════════════════════════════════════════════════════════════════════════════════ #[no_mangle] pub extern "C" fn gf32_add(a: u32, b: u32) -> u32 { - let af = f32::from_bits(a); // FPGA-ALLOWED - let bf = f32::from_bits(b); // FPGA-ALLOWED - (af + bf).to_bits() // FPGA-ALLOWED + let af = f64::from_bits(decode_gf32_to_u64(a)); + let bf = f64::from_bits(decode_gf32_to_u64(b)); + encode_gf32_from_u64((af + bf).to_bits()) } #[no_mangle] pub extern "C" fn gf32_sub(a: u32, b: u32) -> u32 { - let af = f32::from_bits(a); // FPGA-ALLOWED - let bf = f32::from_bits(b); // FPGA-ALLOWED - (af - bf).to_bits() // FPGA-ALLOWED + let af = f64::from_bits(decode_gf32_to_u64(a)); + let bf = f64::from_bits(decode_gf32_to_u64(b)); + encode_gf32_from_u64((af - bf).to_bits()) } #[no_mangle] pub extern "C" fn gf32_mul(a: u32, b: u32) -> u32 { - let af = f32::from_bits(a); // FPGA-ALLOWED - let bf = f32::from_bits(b); // FPGA-ALLOWED - (af * bf).to_bits() // FPGA-ALLOWED + let af = f64::from_bits(decode_gf32_to_u64(a)); + let bf = f64::from_bits(decode_gf32_to_u64(b)); + encode_gf32_from_u64((af * bf).to_bits()) } #[no_mangle] pub extern "C" fn gf32_div(a: u32, b: u32) -> u32 { - let af = f32::from_bits(a); // FPGA-ALLOWED - let bf = f32::from_bits(b); // FPGA-ALLOWED - // Check for division by zero via integer comparison - if b == 0 { - // Return infinity based on sign of a - return if (a & GF32_SIGN_BIT) != 0 { 0xFF80_0000 } else { 0x7F80_0000 }; + let af = f64::from_bits(decode_gf32_to_u64(a)); + let bf = f64::from_bits(decode_gf32_to_u64(b)); + if b == 0x0000_0000 || b == 0x8000_0000 { + return if (a & GF32_SIGN_BIT) != 0 { 0xFFFF_C000 } else { 0x7FFF_C000 }; } - (af / bf).to_bits() // FPGA-ALLOWED + encode_gf32_from_u64((af / bf).to_bits()) } #[no_mangle] @@ -308,14 +400,36 @@ pub extern "C" fn gf32_eq(a: u32, b: u32) -> bool { #[no_mangle] pub extern "C" fn gf32_lt(a: u32, b: u32) -> bool { - // NaN handling via integer comparison if gf32_is_nan(a) || gf32_is_nan(b) { return false; } - // Decode both to compare via f32 (only for comparison) - f32::from_bits(a) < f32::from_bits(b) // FPGA-ALLOWED + f64::from_bits(decode_gf32_to_u64(a)) < f64::from_bits(decode_gf32_to_u64(b)) } +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF4/GF8/GF12/GF20/GF24 constants +// ═══════════════════════════════════════════════════════════════════════════════════ + +const GF4_EXP_BITS: u32 = 1; +const GF4_MANT_BITS: u32 = 2; +const GF4_EXP_BIAS: i32 = 0; + +const GF8_EXP_BITS: u32 = 3; +const GF8_MANT_BITS: u32 = 4; +const GF8_EXP_BIAS: i32 = 3; + +const GF12_EXP_BITS: u32 = 4; +const GF12_MANT_BITS: u32 = 7; +const GF12_EXP_BIAS: i32 = 7; + +const GF20_EXP_BITS: u32 = 7; +const GF20_MANT_BITS: u32 = 12; +const GF20_EXP_BIAS: i32 = 63; + +const GF24_EXP_BITS: u32 = 9; +const GF24_MANT_BITS: u32 = 14; +const GF24_EXP_BIAS: i32 = 255; + // ═══════════════════════════════════════════════════════════════════════════════════ // Field Extraction (all formats) — pure integer operations // ═════════════════════════════════════════════════════════════════════════════════════════════════════════════ @@ -394,3 +508,854 @@ pub extern "C" fn gf24_extract_exponent(value: u32) -> i8 { pub extern "C" fn gf24_extract_mantissa(value: u32) -> i16 { (value & 16383) as i16 } + +#[cfg(test)] +mod tests { + use super::*; + + const REL_TOL: f32 = 0.002; // ~1 GF16 ULP relative tolerance + + fn rel_error(actual: f32, expected: f32) -> f32 { + if actual == expected { return 0.0; } + (actual - expected).abs() / expected.abs().max(1e-30) + } + + // ─── BUG-001 (#546): round-to-nearest-even, not truncation ─── + + #[test] + fn gf16_round_to_nearest_above_halfway() { + let input: f32 = 1.0 + 3.0 * 2f32.powf(-11.0); + let encoded = gf16_from_f32(input); + let decoded = gf16_to_f32(encoded); + assert!(rel_error(decoded, input) < REL_TOL, + "BUG-001: expected near {} got {} (rel err {})", input, decoded, rel_error(decoded, input)); + } + + #[test] + fn gf16_round_to_nearest_at_halfway_rounds_even() { + let input: f32 = 1.0 + 2f32.powf(-10.0); + let encoded = gf16_from_f32(input); + let decoded = gf16_to_f32(encoded); + assert_eq!(decoded, 1.0, + "BUG-001: exactly halfway should round-to-even (1.0), got {}", decoded); + } + + #[test] + fn gf16_round_to_nearest_half_domain() { + let input: f32 = 0.5 + 3.0 * 2f32.powf(-12.0); + let encoded = gf16_from_f32(input); + let decoded = gf16_to_f32(encoded); + assert!(rel_error(decoded, input) < REL_TOL, + "BUG-001: expected near {} got {} (rel err {})", input, decoded, rel_error(decoded, input)); + } + + #[test] + fn gf16_round_to_even_tiebreak() { + let bits = 0x3F802000u32; // 1.0 + exactly 0.5 ULP in GF16 terms + let encoded = encode_gf16_from_u32(bits); + let mant = gf16_extract_mantissa(encoded); + assert_eq!(mant % 2, 0, "BUG-001: tiebreak must round to even mantissa, got {}", mant); + } + + // ─── BUG-002 (#547): overflow → +Inf, not max-finite ─── + + #[test] + fn gf16_overflow_positive_inf() { + let encoded = gf16_from_f32(1e30f32); + assert!(gf16_is_inf(encoded), "BUG-002: 1e30 should encode to +Inf"); + assert_eq!(gf16_extract_sign(encoded), 0, "BUG-002: +Inf sign should be 0"); + assert_eq!(gf16_extract_mantissa(encoded), 0, "BUG-002: +Inf mantissa should be 0"); + } + + #[test] + fn gf16_overflow_negative_inf() { + let encoded = gf16_from_f32(-1e30f32); + assert!(gf16_is_inf(encoded), "BUG-002: -1e30 should encode to -Inf"); + assert_eq!(gf16_extract_sign(encoded), 1, "BUG-002: -Inf sign should be 1"); + assert_eq!(gf16_extract_mantissa(encoded), 0, "BUG-002: -Inf mantissa should be 0"); + } + + #[test] + fn gf16_f32_max_to_inf() { + let encoded = gf16_from_f32(f32::MAX); + assert!(gf16_is_inf(encoded), "BUG-002: f32::MAX should overflow to +Inf in GF16"); + } + + // ─── BUG-003 (#548): GF32 [1:13:18] Lucas L₆ layout ─── + + #[test] + fn gf32_layout_1_13_18() { + assert_eq!(GF32_EXP_BITS, 13); + assert_eq!(GF32_MANT_BITS, 18); + assert_eq!(GF32_EXP_BIAS, 4095); + } + + #[test] + fn gf32_encode_1_point_0() { + let encoded = gf32_from_f64(1.0); + let exp = gf32_extract_exponent(encoded); + let mant = gf32_extract_mantissa(encoded); + let sign = gf32_extract_sign(encoded); + assert_eq!(sign, 0, "BUG-003: 1.0 sign"); + assert_eq!(exp, 4095 + 0, "BUG-003: 1.0 exp should be bias (unbiased=0, biased=4095)"); + assert_eq!(mant, 0, "BUG-003: 1.0 mantissa should be 0 (implicit leading 1)"); + } + + #[test] + fn gf32_roundtrip_1() { + let val = 1.0f64; + assert_eq!(gf32_to_f64(gf32_from_f64(val)), val); + } + + #[test] + fn gf32_roundtrip_pi() { + let val = std::f64::consts::PI; + let encoded = gf32_from_f64(val); + let decoded = gf32_to_f64(encoded); + let rel_err = (decoded - val).abs() / val; + assert!(rel_err < 1e-5, "BUG-003: PI roundtrip rel_err = {}", rel_err); + } + + #[test] + fn gf32_roundtrip_negative() { + let val = -42.5f64; + let encoded = gf32_from_f64(val); + let decoded = gf32_to_f64(encoded); + assert_eq!(decoded, val, "BUG-003: negative roundtrip"); + } + + #[test] + fn gf32_large_finite_value() { + let encoded = gf32_from_f64(1e200); + assert!(!gf32_is_inf(encoded), "BUG-003: 1e200 should be finite in GF32"); + let decoded = gf32_to_f64(encoded); + let rel_err = (decoded - 1e200).abs() / 1e200; + assert!(rel_err < 1e-5, "BUG-003: 1e200 roundtrip rel_err = {}", rel_err); + } + + #[test] + fn gf32_inf_passthrough() { + let encoded = gf32_from_f64(f64::INFINITY); + assert!(gf32_is_inf(encoded), "BUG-003: +Inf should pass through"); + assert_eq!(gf32_extract_sign(encoded), 0); + } + + #[test] + fn gf32_zero() { + let encoded = gf32_from_f64(0.0); + assert!(gf32_is_zero(encoded), "BUG-003: 0.0 should be zero"); + let encoded_neg = gf32_from_f64(-0.0); + assert!(gf32_is_zero(encoded_neg), "BUG-003: -0.0 should be zero"); + assert_eq!(gf32_extract_sign(encoded_neg), 1, "BUG-003: -0.0 sign"); + } + + #[test] + fn gf32_nan() { + let encoded = gf32_from_f64(f64::NAN); + assert!(gf32_is_nan(encoded), "BUG-003: NaN should be NaN"); + } + + #[test] + fn gf32_inf() { + let encoded = gf32_from_f64(f64::INFINITY); + assert!(gf32_is_inf(encoded), "BUG-003: +Inf should be Inf"); + assert_eq!(gf32_extract_sign(encoded), 0); + let encoded_neg = gf32_from_f64(f64::NEG_INFINITY); + assert!(gf32_is_inf(encoded_neg), "BUG-003: -Inf should be Inf"); + assert_eq!(gf32_extract_sign(encoded_neg), 1); + } + + #[test] + fn gf32_arithmetic_add() { + let a = gf32_from_f64(3.0); + let b = gf32_from_f64(4.0); + let c = gf32_add(a, b); + let result = gf32_to_f64(c); + assert!((result - 7.0).abs() < 1e-6, "gf32_add: expected 7.0, got {}", result); + } + + #[test] + fn gf32_arithmetic_mul() { + let a = gf32_from_f64(6.0); + let b = gf32_from_f64(7.0); + let c = gf32_mul(a, b); + let result = gf32_to_f64(c); + assert!((result - 42.0).abs() < 1e-6, "gf32_mul: expected 42.0, got {}", result); + } + + #[test] + fn gf32_arithmetic_lt() { + let a = gf32_from_f64(3.0); + let b = gf32_from_f64(4.0); + assert!(gf32_lt(a, b), "3.0 < 4.0"); + assert!(!gf32_lt(b, a), "4.0 not < 3.0"); + } + + #[test] + fn gf32_exp_range_wider_than_f64() { + let small = gf32_from_f64(2f64.powf(-1020.0)); + let decoded_small = gf32_to_f64(small); + assert!(decoded_small > 0.0 && decoded_small.is_finite(), + "BUG-003: 2^-1020 should be representable, got {}", decoded_small); + + let large = gf32_from_f64(1e200); + let decoded_large = gf32_to_f64(large); + assert!(decoded_large.is_finite() && decoded_large > 1e199, + "BUG-003: 1e200 should roundtrip in GF32, got {}", decoded_large); + } + + // ─── #549: GF4/8/12/20/24 arithmetic + roundtrip ─── + + macro_rules! gf_roundtrip_test { + ($name:ident, $from_f32:ident, $to_f32:ident, $mant_bits:expr) => { + #[test] + fn $name() { + let tol = 2.0f32.powf(-($mant_bits as f32)); + for &v in &[0.0f32, -0.0f32, 1.0f32, -1.0f32, 2.0f32, 0.5f32, 3.14f32] { + let enc = $from_f32(v); + let dec = $to_f32(enc); + if v == 0.0 || v == -0.0 { + assert!(dec == 0.0, concat!(stringify!($name), ": {} roundtrip got {}"), v, dec); + } else { + let err = (dec - v).abs() / v.abs(); + assert!(err < tol, concat!(stringify!($name), ": {} roundtrip rel_err={}"), v, err); + } + } + let inf_enc = $from_f32(f32::INFINITY); + let inf_dec = $to_f32(inf_enc); + assert!(inf_dec.is_infinite() && inf_dec.is_sign_positive(), + concat!(stringify!($name), ": +Inf roundtrip")); + let nan_enc = $from_f32(f32::NAN); + assert!($to_f32(nan_enc).is_nan(), concat!(stringify!($name), ": NaN roundtrip")); + } + }; + } + + #[test] + fn gf4_roundtrip() { + let tol = 2.0f32.powf(-(GF4_MANT_BITS as f32)); + assert!(gf4_to_f32(gf4_from_f32(0.0f32)) == 0.0, "gf4 zero"); + assert!(gf4_to_f32(gf4_from_f32(f32::INFINITY)).is_infinite(), "gf4 +Inf"); + assert!(gf4_to_f32(gf4_from_f32(f32::NAN)).is_nan(), "gf4 NaN"); + for &v in &[1.25f32, 1.5f32, 1.75f32, -1.25f32, -1.5f32, -1.75f32] { + let enc = gf4_from_f32(v); + let dec = gf4_to_f32(enc); + let err = (dec - v).abs() / v.abs(); + assert!(err < tol, "gf4: {} roundtrip rel_err={}", v, err); + } + } + + gf_roundtrip_test!(gf8_roundtrip, gf8_from_f32, gf8_to_f32, GF8_MANT_BITS); + gf_roundtrip_test!(gf12_roundtrip, gf12_from_f32, gf12_to_f32, GF12_MANT_BITS); + gf_roundtrip_test!(gf20_roundtrip, gf20_from_f32, gf20_to_f32, GF20_MANT_BITS); + gf_roundtrip_test!(gf24_roundtrip, gf24_from_f32, gf24_to_f32, GF24_MANT_BITS); + + macro_rules! gf_arith_test { + ($name:ident, $from_f32:ident, $to_f32:ident, $add:ident, $sub:ident, $mul:ident, $div:ident, $tol:expr) => { + #[test] + fn $name() { + let a = $from_f32(3.0f32); + let b = $from_f32(4.0f32); + let sum = $to_f32($add(a, b)); + assert!((sum - 7.0).abs() < $tol, "add: expected ~7.0 got {}", sum); + let diff = $to_f32($sub(a, b)); + assert!((diff - (-1.0)).abs() < $tol, "sub: expected ~-1.0 got {}", diff); + let prod = $to_f32($mul(a, b)); + assert!((prod - 12.0).abs() < $tol, "mul: expected ~12.0 got {}", prod); + let quot = $to_f32($div(b, a)); + let q_err = (quot - (4.0f32 / 3.0f32)).abs(); + assert!(q_err < $tol, "div: expected ~1.333 got {} (err={})", quot, q_err); + } + }; + } + + #[test] + fn gf4_arithmetic() { + let a = gf4_from_f32(1.25f32); + let b = gf4_from_f32(1.5f32); + let quot = gf4_to_f32(gf4_div(b, a)); + let q_err = (quot - 1.2).abs(); + assert!(q_err < 0.5, "gf4 div: expected ~1.2 got {} (err={})", quot, q_err); + let neg = gf4_to_f32(gf4_sub(gf4_from_f32(-1.5f32), gf4_from_f32(-1.25f32))); + assert!((neg - (-0.25)).abs() < 0.5, "gf4 neg sub"); + } + + gf_arith_test!(gf8_arithmetic, gf8_from_f32, gf8_to_f32, gf8_add, gf8_sub, gf8_mul, gf8_div, 0.25); + gf_arith_test!(gf12_arithmetic, gf12_from_f32, gf12_to_f32, gf12_add, gf12_sub, gf12_mul, gf12_div, 0.1); + gf_arith_test!(gf20_arithmetic, gf20_from_f32, gf20_to_f32, gf20_add, gf20_sub, gf20_mul, gf20_div, 0.01); + gf_arith_test!(gf24_arithmetic, gf24_from_f32, gf24_to_f32, gf24_add, gf24_sub, gf24_mul, gf24_div, 0.005); + + #[test] + fn gf_all_formats_overflow_to_inf() { + for &val in &[1e10f32, 1e30f32, f32::MAX] { + assert!(gf4_to_f32(gf4_from_f32(val)).is_infinite(), "gf4 overflow"); + assert!(gf8_to_f32(gf8_from_f32(val)).is_infinite(), "gf8 overflow"); + assert!(gf12_to_f32(gf12_from_f32(val)).is_infinite(), "gf12 overflow"); + } + } +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF4 ENCODE/DECODE (1:1:2) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf4_from_f32(x: f32) -> u8 { + let bits: u32 = x.to_bits(); + let sign: u8 = ((bits >> 31) & 1) as u8; + let exp: i32 = ((bits >> 23) & 0xFF) as i32 - 127; + let mant: u32 = bits & 0x007F_FFFF; + + if (bits & 0x7FFF_FFFF) == 0 { + return sign << 3; + } + if (bits & 0x7F80_0000) == 0x7F80_0000 { + let exp_max: u8 = (1 << GF4_EXP_BITS) - 1; + let mant_out: u8 = if mant == 0 { 0 } else { 1 }; + return (sign << 3) | (exp_max << GF4_MANT_BITS) | mant_out; + } + + let gf4_exp_raw: i32 = exp + GF4_EXP_BIAS; + let exp_max: u32 = (1 << GF4_EXP_BITS) - 1; + if gf4_exp_raw >= exp_max as i32 { + return (sign << 3) | ((exp_max as u8) << GF4_MANT_BITS); + } + let gf4_exp: u8 = if gf4_exp_raw < 0 { 0 } else { gf4_exp_raw as u8 }; + + let drop = 23 - GF4_MANT_BITS; + let lower_bits = mant & ((1u32 << drop) - 1); + let halfway = 1u32 << (drop - 1); + let mut gf4_mant: u8 = (mant >> drop) as u8; + let round_up = lower_bits > halfway + || (lower_bits == halfway && (gf4_mant & 1) == 1); + if round_up { + gf4_mant += 1; + if gf4_mant == (1 << GF4_MANT_BITS) { + let new_exp = gf4_exp + 1; + if new_exp >= exp_max as u8 { + return (sign << 3) | (exp_max as u8) << GF4_MANT_BITS; + } + return (sign << 3) | (new_exp << GF4_MANT_BITS); + } + } + + (sign << 3) | (gf4_exp << GF4_MANT_BITS) | gf4_mant +} + +#[no_mangle] +pub extern "C" fn gf4_to_f32(value: u8) -> f32 { + let sign: u32 = ((value as u32) >> 3) & 1; + let exp: u32 = ((value as u32) >> GF4_MANT_BITS) & ((1 << GF4_EXP_BITS) - 1); + let mant: u32 = (value as u32) & ((1u32 << GF4_MANT_BITS) - 1); + let exp_max: u32 = (1 << GF4_EXP_BITS) - 1; + + if exp == exp_max { + if mant == 0 { + return f32::from_bits((sign << 31) | 0x7F80_0000); + } + return f32::from_bits(0x7FC0_0000); + } + if exp == 0 && mant == 0 { + return f32::from_bits(sign << 31); + } + + let ieee_exp: u32 = (((exp as i32) - GF4_EXP_BIAS + 127) as u32) & 0xFF; + let ieee_mant: u32 = mant << (23 - GF4_MANT_BITS); + f32::from_bits((sign << 31) | (ieee_exp << 23) | ieee_mant) +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF8 ENCODE/DECODE (1:3:4) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf8_from_f32(x: f32) -> u8 { + let bits: u32 = x.to_bits(); + let sign: u8 = ((bits >> 31) & 1) as u8; + let exp: i32 = ((bits >> 23) & 0xFF) as i32 - 127; + let mant: u32 = bits & 0x007F_FFFF; + + if (bits & 0x7FFF_FFFF) == 0 { + return sign << 7; + } + if (bits & 0x7F80_0000) == 0x7F80_0000 { + let exp_max: u8 = (1 << GF8_EXP_BITS) - 1; + let mant_out: u8 = if mant == 0 { 0 } else { 1 }; + return (sign << 7) | (exp_max << GF8_MANT_BITS) | mant_out; + } + + let gf8_exp_raw: i32 = exp + GF8_EXP_BIAS; + let exp_max: u32 = (1 << GF8_EXP_BITS) - 1; + if gf8_exp_raw >= exp_max as i32 { + return (sign << 7) | ((exp_max as u8) << GF8_MANT_BITS); + } + let gf8_exp: u8 = if gf8_exp_raw < 0 { 0 } else { gf8_exp_raw as u8 }; + + let drop = 23 - GF8_MANT_BITS; + let lower_bits = mant & ((1u32 << drop) - 1); + let halfway = 1u32 << (drop - 1); + let mut gf8_mant: u8 = (mant >> drop) as u8; + let round_up = lower_bits > halfway + || (lower_bits == halfway && (gf8_mant & 1) == 1); + if round_up { + gf8_mant += 1; + if gf8_mant == (1 << GF8_MANT_BITS) { + let new_exp = gf8_exp + 1; + if new_exp >= exp_max as u8 { + return (sign << 7) | (exp_max as u8) << GF8_MANT_BITS; + } + return (sign << 7) | (new_exp << GF8_MANT_BITS); + } + } + + (sign << 7) | (gf8_exp << GF8_MANT_BITS) | gf8_mant +} + +#[no_mangle] +pub extern "C" fn gf8_to_f32(value: u8) -> f32 { + let sign: u32 = ((value as u32) >> 7) & 1; + let exp: u32 = ((value as u32) >> GF8_MANT_BITS) & ((1 << GF8_EXP_BITS) - 1); + let mant: u32 = (value as u32) & ((1u32 << GF8_MANT_BITS) - 1); + let exp_max: u32 = (1 << GF8_EXP_BITS) - 1; + + if exp == exp_max { + if mant == 0 { + return f32::from_bits((sign << 31) | 0x7F80_0000); + } + return f32::from_bits(0x7FC0_0000); + } + if exp == 0 && mant == 0 { + return f32::from_bits(sign << 31); + } + + let ieee_exp: u32 = (((exp as i32) - GF8_EXP_BIAS + 127) as u32) & 0xFF; + let ieee_mant: u32 = mant << (23 - GF8_MANT_BITS); + f32::from_bits((sign << 31) | (ieee_exp << 23) | ieee_mant) +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF12 ENCODE/DECODE (1:4:7) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf12_from_f32(x: f32) -> u16 { + let bits: u32 = x.to_bits(); + let sign: u16 = ((bits >> 31) & 1) as u16; + let exp: i32 = ((bits >> 23) & 0xFF) as i32 - 127; + let mant: u32 = bits & 0x007F_FFFF; + + if (bits & 0x7FFF_FFFF) == 0 { + return sign << 11; + } + if (bits & 0x7F80_0000) == 0x7F80_0000 { + let exp_max: u16 = (1 << GF12_EXP_BITS) - 1; + let mant_out: u16 = if mant == 0 { 0 } else { 1 }; + return (sign << 11) | (exp_max << GF12_MANT_BITS) | mant_out; + } + + let gf12_exp_raw: i32 = exp + GF12_EXP_BIAS; + let exp_max: u32 = (1 << GF12_EXP_BITS) - 1; + if gf12_exp_raw >= exp_max as i32 { + return (sign << 11) | ((exp_max as u16) << GF12_MANT_BITS); + } + let gf12_exp: u16 = if gf12_exp_raw < 0 { 0 } else { gf12_exp_raw as u16 }; + + let drop = 23 - GF12_MANT_BITS; + let lower_bits = mant & ((1u32 << drop) - 1); + let halfway = 1u32 << (drop - 1); + let mut gf12_mant: u16 = (mant >> drop) as u16; + let round_up = lower_bits > halfway + || (lower_bits == halfway && (gf12_mant & 1) == 1); + if round_up { + gf12_mant += 1; + if gf12_mant == (1 << GF12_MANT_BITS) { + let new_exp = gf12_exp + 1; + if new_exp >= exp_max as u16 { + return (sign << 11) | (exp_max as u16) << GF12_MANT_BITS; + } + return (sign << 11) | (new_exp << GF12_MANT_BITS); + } + } + + (sign << 11) | (gf12_exp << GF12_MANT_BITS) | gf12_mant +} + +#[no_mangle] +pub extern "C" fn gf12_to_f32(value: u16) -> f32 { + let sign: u32 = ((value as u32) >> 11) & 1; + let exp: u32 = ((value as u32) >> GF12_MANT_BITS) & ((1 << GF12_EXP_BITS) - 1); + let mant: u32 = (value as u32) & ((1u32 << GF12_MANT_BITS) - 1); + let exp_max: u32 = (1 << GF12_EXP_BITS) - 1; + + if exp == exp_max { + if mant == 0 { + return f32::from_bits((sign << 31) | 0x7F80_0000); + } + return f32::from_bits(0x7FC0_0000); + } + if exp == 0 && mant == 0 { + return f32::from_bits(sign << 31); + } + + let ieee_exp: u32 = (((exp as i32) - GF12_EXP_BIAS + 127) as u32) & 0xFF; + let ieee_mant: u32 = mant << (23 - GF12_MANT_BITS); + f32::from_bits((sign << 31) | (ieee_exp << 23) | ieee_mant) +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF20 ENCODE/DECODE (1:7:12) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf20_from_f32(x: f32) -> u32 { + let bits: u32 = x.to_bits(); + let sign: u32 = (bits >> 31) & 1; + let exp: i32 = ((bits >> 23) & 0xFF) as i32 - 127; + let mant: u32 = bits & 0x007F_FFFF; + + if (bits & 0x7FFF_FFFF) == 0 { + return sign << 19; + } + if (bits & 0x7F80_0000) == 0x7F80_0000 { + let exp_max: u32 = (1 << GF20_EXP_BITS) - 1; + let mant_out: u32 = if mant == 0 { 0 } else { 1 }; + return (sign << 19) | (exp_max << GF20_MANT_BITS) | mant_out; + } + + let gf20_exp_raw: i32 = exp + GF20_EXP_BIAS; + let exp_max: u32 = (1 << GF20_EXP_BITS) - 1; + if gf20_exp_raw >= exp_max as i32 { + return (sign << 19) | (exp_max << GF20_MANT_BITS); + } + let gf20_exp: u32 = if gf20_exp_raw < 0 { 0 } else { gf20_exp_raw as u32 }; + + let drop: u32 = 23 - GF20_MANT_BITS; + let lower_bits = mant & ((1u32 << drop) - 1); + let halfway = 1u32 << (drop - 1); + let mut gf20_mant: u32 = mant >> drop; + let round_up = lower_bits > halfway + || (lower_bits == halfway && (gf20_mant & 1) == 1); + if round_up { + gf20_mant += 1; + if gf20_mant == (1 << GF20_MANT_BITS) { + let new_exp = gf20_exp + 1; + if new_exp >= exp_max { + return (sign << 19) | (exp_max << GF20_MANT_BITS); + } + return (sign << 19) | (new_exp << GF20_MANT_BITS); + } + } + + (sign << 19) | (gf20_exp << GF20_MANT_BITS) | gf20_mant +} + +#[no_mangle] +pub extern "C" fn gf20_to_f32(value: u32) -> f32 { + let sign: u32 = (value >> 19) & 1; + let exp: u32 = (value >> GF20_MANT_BITS) & ((1 << GF20_EXP_BITS) - 1); + let mant: u32 = value & ((1u32 << GF20_MANT_BITS) - 1); + let exp_max: u32 = (1 << GF20_EXP_BITS) - 1; + + if exp == exp_max { + if mant == 0 { + return f32::from_bits((sign << 31) | 0x7F80_0000); + } + return f32::from_bits(0x7FC0_0000); + } + if exp == 0 && mant == 0 { + return f32::from_bits(sign << 31); + } + + let ieee_exp: u32 = (((exp as i32) - GF20_EXP_BIAS + 127) as u32) & 0xFF; + let ieee_mant: u32 = mant << (23 - GF20_MANT_BITS); + f32::from_bits((sign << 31) | (ieee_exp << 23) | ieee_mant) +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF24 ENCODE/DECODE (1:9:14) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf24_from_f32(x: f32) -> u32 { + let bits: u32 = x.to_bits(); + let sign: u32 = (bits >> 31) & 1; + let exp: i32 = ((bits >> 23) & 0xFF) as i32 - 127; + let mant: u32 = bits & 0x007F_FFFF; + + if (bits & 0x7FFF_FFFF) == 0 { + return sign << 23; + } + if (bits & 0x7F80_0000) == 0x7F80_0000 { + let exp_max: u32 = (1 << GF24_EXP_BITS) - 1; + let mant_out: u32 = if mant == 0 { 0 } else { 1 }; + return (sign << 23) | (exp_max << GF24_MANT_BITS) | mant_out; + } + + let gf24_exp_raw: i32 = exp + GF24_EXP_BIAS; + let exp_max: u32 = (1 << GF24_EXP_BITS) - 1; + if gf24_exp_raw >= exp_max as i32 { + return (sign << 23) | (exp_max << GF24_MANT_BITS); + } + let gf24_exp: u32 = if gf24_exp_raw < 0 { 0 } else { gf24_exp_raw as u32 }; + + let drop: u32 = 23 - GF24_MANT_BITS; + let lower_bits = mant & ((1u32 << drop) - 1); + let halfway = 1u32 << (drop - 1); + let mut gf24_mant: u32 = mant >> drop; + let round_up = lower_bits > halfway + || (lower_bits == halfway && (gf24_mant & 1) == 1); + if round_up { + gf24_mant += 1; + if gf24_mant == (1 << GF24_MANT_BITS) { + let new_exp = gf24_exp + 1; + if new_exp >= exp_max { + return (sign << 23) | (exp_max << GF24_MANT_BITS); + } + return (sign << 23) | (new_exp << GF24_MANT_BITS); + } + } + + (sign << 23) | (gf24_exp << GF24_MANT_BITS) | gf24_mant +} + +#[no_mangle] +pub extern "C" fn gf24_to_f32(value: u32) -> f32 { + let sign: u32 = (value >> 23) & 1; + let exp: u32 = (value >> GF24_MANT_BITS) & ((1 << GF24_EXP_BITS) - 1); + let mant: u32 = value & ((1u32 << GF24_MANT_BITS) - 1); + let exp_max: u32 = (1 << GF24_EXP_BITS) - 1; + + if exp == exp_max { + if mant == 0 { + return f32::from_bits((sign << 31) | 0x7F80_0000); + } + return f32::from_bits(0x7FC0_0000); + } + if exp == 0 && mant == 0 { + return f32::from_bits(sign << 31); + } + + let ieee_exp: u32 = (((exp as i32) - GF24_EXP_BIAS + 127) as u32) & 0xFF; + let ieee_mant: u32 = mant << (23 - GF24_MANT_BITS); + f32::from_bits((sign << 31) | (ieee_exp << 23) | ieee_mant) +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF4 ARITHMETIC (1:1:2) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf4_add(a: u8, b: u8) -> u8 { + gf4_from_f32(gf4_to_f32(a) + gf4_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf4_sub(a: u8, b: u8) -> u8 { + gf4_from_f32(gf4_to_f32(a) - gf4_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf4_mul(a: u8, b: u8) -> u8 { + gf4_from_f32(gf4_to_f32(a) * gf4_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf4_div(a: u8, b: u8) -> u8 { + if gf4_is_zero(b) { return gf4_from_f32(f32::INFINITY); } + gf4_from_f32(gf4_to_f32(a) / gf4_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf4_is_zero(value: u8) -> bool { + (value & 0x7) == 0 +} + +#[no_mangle] +pub extern "C" fn gf4_is_inf(value: u8) -> bool { + let exp = (value >> GF4_MANT_BITS) & ((1 << GF4_EXP_BITS) - 1); + let mant = value & ((1u8 << GF4_MANT_BITS) - 1); + exp == (1 << GF4_EXP_BITS) - 1 && mant == 0 +} + +#[no_mangle] +pub extern "C" fn gf4_is_nan(value: u8) -> bool { + let exp = (value >> GF4_MANT_BITS) & ((1 << GF4_EXP_BITS) - 1); + let mant = value & ((1u8 << GF4_MANT_BITS) - 1); + exp == (1 << GF4_EXP_BITS) - 1 && mant != 0 +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF8 ARITHMETIC (1:3:4) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf8_add(a: u8, b: u8) -> u8 { + gf8_from_f32(gf8_to_f32(a) + gf8_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf8_sub(a: u8, b: u8) -> u8 { + gf8_from_f32(gf8_to_f32(a) - gf8_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf8_mul(a: u8, b: u8) -> u8 { + gf8_from_f32(gf8_to_f32(a) * gf8_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf8_div(a: u8, b: u8) -> u8 { + if gf8_is_zero(b) { return gf8_from_f32(f32::INFINITY); } + gf8_from_f32(gf8_to_f32(a) / gf8_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf8_is_zero(value: u8) -> bool { + (value & 0x7F) == 0 +} + +#[no_mangle] +pub extern "C" fn gf8_is_inf(value: u8) -> bool { + let exp = (value >> GF8_MANT_BITS) & ((1 << GF8_EXP_BITS) - 1); + let mant = value & ((1u8 << GF8_MANT_BITS) - 1); + exp == (1 << GF8_EXP_BITS) - 1 && mant == 0 +} + +#[no_mangle] +pub extern "C" fn gf8_is_nan(value: u8) -> bool { + let exp = (value >> GF8_MANT_BITS) & ((1 << GF8_EXP_BITS) - 1); + let mant = value & ((1u8 << GF8_MANT_BITS) - 1); + exp == (1 << GF8_EXP_BITS) - 1 && mant != 0 +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF12 ARITHMETIC (1:4:7) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf12_add(a: u16, b: u16) -> u16 { + gf12_from_f32(gf12_to_f32(a) + gf12_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf12_sub(a: u16, b: u16) -> u16 { + gf12_from_f32(gf12_to_f32(a) - gf12_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf12_mul(a: u16, b: u16) -> u16 { + gf12_from_f32(gf12_to_f32(a) * gf12_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf12_div(a: u16, b: u16) -> u16 { + if gf12_is_zero(b) { return gf12_from_f32(f32::INFINITY); } + gf12_from_f32(gf12_to_f32(a) / gf12_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf12_is_zero(value: u16) -> bool { + (value & 0x7FF) == 0 +} + +#[no_mangle] +pub extern "C" fn gf12_is_inf(value: u16) -> bool { + let exp = (value >> GF12_MANT_BITS) & ((1 << GF12_EXP_BITS) - 1); + let mant = value & ((1u16 << GF12_MANT_BITS) - 1); + exp == (1 << GF12_EXP_BITS) - 1 && mant == 0 +} + +#[no_mangle] +pub extern "C" fn gf12_is_nan(value: u16) -> bool { + let exp = (value >> GF12_MANT_BITS) & ((1 << GF12_EXP_BITS) - 1); + let mant = value & ((1u16 << GF12_MANT_BITS) - 1); + exp == (1 << GF12_EXP_BITS) - 1 && mant != 0 +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF20 ARITHMETIC (1:7:12) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf20_add(a: u32, b: u32) -> u32 { + gf20_from_f32(gf20_to_f32(a) + gf20_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf20_sub(a: u32, b: u32) -> u32 { + gf20_from_f32(gf20_to_f32(a) - gf20_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf20_mul(a: u32, b: u32) -> u32 { + gf20_from_f32(gf20_to_f32(a) * gf20_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf20_div(a: u32, b: u32) -> u32 { + if gf20_is_zero(b) { return gf20_from_f32(f32::INFINITY); } + gf20_from_f32(gf20_to_f32(a) / gf20_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf20_is_zero(value: u32) -> bool { + (value & 0x7FFFF) == 0 +} + +#[no_mangle] +pub extern "C" fn gf20_is_inf(value: u32) -> bool { + let exp = (value >> GF20_MANT_BITS) & ((1 << GF20_EXP_BITS) - 1); + let mant = value & ((1u32 << GF20_MANT_BITS) - 1); + exp == (1 << GF20_EXP_BITS) - 1 && mant == 0 +} + +#[no_mangle] +pub extern "C" fn gf20_is_nan(value: u32) -> bool { + let exp = (value >> GF20_MANT_BITS) & ((1 << GF20_EXP_BITS) - 1); + let mant = value & ((1u32 << GF20_MANT_BITS) - 1); + exp == (1 << GF20_EXP_BITS) - 1 && mant != 0 +} + +// ═══════════════════════════════════════════════════════════════════════════════════ +// GF24 ARITHMETIC (1:9:14) +// ═══════════════════════════════════════════════════════════════════════════════════ + +#[no_mangle] +pub extern "C" fn gf24_add(a: u32, b: u32) -> u32 { + gf24_from_f32(gf24_to_f32(a) + gf24_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf24_sub(a: u32, b: u32) -> u32 { + gf24_from_f32(gf24_to_f32(a) - gf24_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf24_mul(a: u32, b: u32) -> u32 { + gf24_from_f32(gf24_to_f32(a) * gf24_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf24_div(a: u32, b: u32) -> u32 { + if gf24_is_zero(b) { return gf24_from_f32(f32::INFINITY); } + gf24_from_f32(gf24_to_f32(a) / gf24_to_f32(b)) +} + +#[no_mangle] +pub extern "C" fn gf24_is_zero(value: u32) -> bool { + (value & 0x7FFFFF) == 0 +} + +#[no_mangle] +pub extern "C" fn gf24_is_inf(value: u32) -> bool { + let exp = (value >> GF24_MANT_BITS) & ((1 << GF24_EXP_BITS) - 1); + let mant = value & ((1u32 << GF24_MANT_BITS) - 1); + exp == (1 << GF24_EXP_BITS) - 1 && mant == 0 +} + +#[no_mangle] +pub extern "C" fn gf24_is_nan(value: u32) -> bool { + let exp = (value >> GF24_MANT_BITS) & ((1 << GF24_EXP_BITS) - 1); + let mant = value & ((1u32 << GF24_MANT_BITS) - 1); + exp == (1 << GF24_EXP_BITS) - 1 && mant != 0 +} diff --git a/scripts/githooks/pre-commit b/scripts/githooks/pre-commit index 556f3a75..b558c21a 100755 --- a/scripts/githooks/pre-commit +++ b/scripts/githooks/pre-commit @@ -1,3 +1,46 @@ -#!/bin/sh -set -e -cd "$(git rev-parse --show-toplevel)/bootstrap" && cargo build -q +#!/usr/bin/env bash +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +STAGED=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true) + +echo -e "${YELLOW}[pre-commit] Gate 1/4: NOW freshness (L1)...${NC}" +"$REPO_ROOT/scripts/tri" check-now || { + echo -e "${RED}BLOCKED: docs/NOW.md is stale. Update 'Last updated' date.${NC}" + exit 1 +} + +echo -e "${YELLOW}[pre-commit] Gate 2/4: Seal coverage (L2)...${NC}" +T27_STAGED=$(echo "$STAGED" | grep -E '^specs/.*\.t27$' || true) +if [[ -n "$T27_STAGED" ]]; then + for spec in $T27_STAGED; do + basename_no_ext=$(basename "$spec" .t27) + seal_file="$REPO_ROOT/.trinity/seals/${basename_no_ext}.json" + if [[ ! -f "$seal_file" ]]; then + echo -e "${RED}BLOCKED: $spec has no seal. Run: tri seal --save $spec${NC}" + exit 1 + fi + done +fi + +echo -e "${YELLOW}[pre-commit] Gate 3/4: Cargo check (bootstrap)...${NC}" +(cd "$REPO_ROOT/bootstrap" && cargo check -q 2>/dev/null) || { + echo -e "${RED}BLOCKED: cargo check failed in bootstrap/${NC}" + exit 1 +} + +echo -e "${YELLOW}[pre-commit] Gate 4/4: No new .sh files (L7)...${NC}" +SH_STAGED=$(echo "$STAGED" | grep -E '\.sh$' || true) +if [[ -n "$SH_STAGED" ]]; then + echo -e "${RED}BLOCKED: L7 UNITY — new .sh files detected:${NC}" + echo "$SH_STAGED" + echo -e "${YELLOW}Use tri/t27c instead of raw shell scripts.${NC}" + exit 1 +fi + +echo -e "${GREEN}All 4 gates passed.${NC}" diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh index 8ebfb040..10ec32f5 100755 --- a/scripts/install-git-hooks.sh +++ b/scripts/install-git-hooks.sh @@ -27,48 +27,10 @@ cp "$SCRIPT_DIR/githooks/commit-msg-traceability" "$HOOKS_DIR/commit-msg" chmod +x "$HOOKS_DIR/commit-msg" echo -e "${GREEN}✓ commit-msg hook installed${NC}" -# Install pre-commit hook for L3 PURITY (ASCII-only, English) +# Install pre-commit hook for all 4 constitutional gates echo "" -echo "Installing pre-commit hook (L3 PURITY enforcement)..." -cat > "$HOOKS_DIR/pre-commit" << 'EOF' -#!/usr/bin/env bash -# L3 PURITY Pre-Commit Hook -# Checks for ASCII-only source files and English identifiers - -set -euo pipefail - -# ANSI colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -# Check for non-ASCII characters in tracked source files -# Excludes: docs/.legacy-non-english-docs/, binary files -FILES=$(git diff --cached --name-only --diff-filter=ACM | \ - grep -E '\.(t27|zig|rs|c|h|v|md)$' | \ - grep -v '^docs/\.legacy-non-english-docs/' || true) - -if [ -n "$FILES" ]; then - NON_ASCII=$(git diff --cached --name-only | \ - xargs -I {} sh -c 'file {} | grep -q "ASCII text" || echo {}' || true) - - if [ -n "$NON_ASCII" ]; then - # Check for actual non-ASCII characters - for file in $NON_ASCII; do - if [ -f "$file" ]; then - # Check if file contains non-ASCII (excluding UTF-8 BOM) - if LC_ALL=C grep -q '[^[:print:][:space:]]' "$file" 2>/dev/null; then - echo -e "${YELLOW}⚠️ Warning: $file may contain non-ASCII characters${NC}" - fi - fi - done - fi -fi - -echo -e "${GREEN}✅ L3 PURITY check passed${NC}" -EOF - +echo "Installing pre-commit hook (L1 NOW + L2 Seal + L4 Cargo + L7 No-.sh)..." +cp "$SCRIPT_DIR/githooks/pre-commit" "$HOOKS_DIR/pre-commit" chmod +x "$HOOKS_DIR/pre-commit" echo -e "${GREEN}✓ pre-commit hook installed${NC}" @@ -80,7 +42,7 @@ cat > "$HOOKS_DIR/pre-push" << 'EOF' # L4 TESTABILITY Pre-Push Hook # Warns if .t27 files are being pushed without test/invariant/bench -set -euo pipefill +set -euo pipefail # ANSI colors RED='\033[0;31m' @@ -108,7 +70,7 @@ echo -e "${GREEN}All Git hooks installed successfully!${NC}" echo "" echo "Installed hooks:" echo " - commit-msg: Enforces L1 TRACEABILITY (Closes #N required)" -echo " - pre-commit: Checks L3 PURITY (ASCII-only, English)" +echo " - pre-commit: 4 gates — NOW freshness, seal coverage, cargo check, no .sh (L1/L2/L4/L7)" echo " - pre-push: Warns about L4 TESTABILITY (test/invariant/bench)" echo "" echo "To skip hooks (not recommended):" diff --git a/scripts/tri b/scripts/tri index ff217a2c..faa18e34 100755 --- a/scripts/tri +++ b/scripts/tri @@ -1,40 +1,18 @@ #!/usr/bin/env bash -<<<<<<< Updated upstream -set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -T27C="${TRI_T27C:-}" -if [[ -z "$T27C" ]]; then - for p in "$ROOT/bootstrap/target/release/t27c" "$ROOT/bootstrap/target/debug/t27c"; do - [[ -x "$p" ]] && T27C="$p" && break - done -fi -[[ -n "${T27C:-}" && -x "$T27C" ]] || { - echo "tri: t27c not found. Run: cd bootstrap && cargo build --release" >&2 - exit 1 -} -<<<<<<< Updated upstream -exec "$T27C" --repo-root "$ROOT" "$@" -======= -# Exec shim only: all logic lives in `t27c` (Rust). See SOUL.md Article VIII (NO-PYTHON / NO-SHELL). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" T27C="${TRI_T27C:-$REPO_ROOT/bootstrap/target/release/t27c}" if [[ ! -x "$T27C" && -x "$REPO_ROOT/bootstrap/target/debug/t27c" ]]; then - T27C="$REPO_ROOT/bootstrap/target/debug/t27c" + T27C="$REPO_ROOT/bootstrap/target/debug/t27c" fi if [[ ! -x "$T27C" ]]; then - echo "TOXIC: t27c not found. Run: cd bootstrap && cargo build --release" >&2 - echo "TOXIC: or set TRI_T27C to your t27c executable." >&2 - exit 1 + echo "tri: t27c not found. Run: cd bootstrap && cargo build --release" >&2 + exit 1 fi -exec "$T27C" --repo-root "$REPO_ROOT" "$@" ->>>>>>> Stashed changes -======= -# Don't add --repo-root for commands that have their own repo-root option -if [[ "$1" =~ ^(check-now|typecheck|check)$ ]]; then - exec "$T27C" "$@" +REPO_CMPTS="suite|validate-conformance|validate-gen-headers|check-now|doc-all|analyze|ci" +if [[ $# -gt 0 && "$1" =~ ^($REPO_CMPTS)$ ]]; then + exec "$T27C" "$1" --repo-root "$REPO_ROOT" "${@:2}" else - exec "$T27C" --repo-root "$ROOT" "$@" + exec "$T27C" "$@" fi ->>>>>>> Stashed changes diff --git a/specs/cli/railway.t27 b/specs/cli/railway.t27 new file mode 100644 index 00000000..a8e1a89a --- /dev/null +++ b/specs/cli/railway.t27 @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: CC0-1.0 +// CLI-RAILWAY-543: tri railway command family for trios-trainer-igla +// 3-seed Gate-2 ONE SHOT deploy. + +/** + * Module: cli.railway + * + * Defines the surface and semantics of the `tri railway` subcommand family. + * + * Drives the Railway GraphQL API (backboard.railway.com/graphql/v2) to + * deploy the trios-trainer-igla Gate-2 push. The single ONE SHOT is: + * + * tri railway up --seeds 43,44,45 + * + * Which creates (or upserts) three Railway services bound to the + * trainer image, sets TRIOS_SEED / TRIOS_LEDGER_PUSH / TRIOS_TARGET_BPB, + * and returns a JSON envelope of deployment ids. + * + * Constitutional alignment: + * - R5 honesty : `up` is `--dry-run` by default; `--confirm` required + * before any mutation hits the Railway API. + * - R7 triplet : `status` emits canonical R7 lines once seed rows + * land in assertions/seed_results.jsonl. + * - R9 embargo : `up` refuses if HEAD SHA matches an entry in + * assertions/embargo.txt. + * - NO-MUTATION-WITHOUT-SKILL : every mutation requires active skill. + * - ASCII only : this spec is ASCII, no Cyrillic anywhere. + */ + +module cli.railway; + +use ledger.row; +use embargo.list; + +// ---------------------------------------------------------------------- +// Constants +// ---------------------------------------------------------------------- + +const DEFAULT_API_ENDPOINT : str = "https://backboard.railway.com/graphql/v2"; +const DEFAULT_PROJECT_ID : str = "e4fe33bb-3b09-4842-9782-7d2dea1abc9b"; +const DEFAULT_IMAGE_REF : str = "ghcr.io/ghashtag/trios-trainer-igla:latest"; +const DEFAULT_TARGET_BPB : f64 = 1.85; +const DEFAULT_STEPS : u64 = 30_000; +const GATE2_SEED_QUORUM : usize = 3; +const STATE_BINDING_PATH : str = ".trinity/state/railway-binding.json"; +const EMBARGO_PATH : str = "assertions/embargo.txt"; +const DEFAULT_TIMEOUT_SEC : u64 = 30; +const HTTP_OK : u16 = 200; +const HTTP_UNAUTHORIZED : u16 = 401; +const TRINITY_ANCHOR : f64 = 3.0; +const PHI : f64 = 1.618033988749895; + +// Env variable names exported to each service. +const ENV_SEED : str = "TRIOS_SEED"; +const ENV_LEDGER_PUSH : str = "TRIOS_LEDGER_PUSH"; +const ENV_TARGET_BPB : str = "TRIOS_TARGET_BPB"; +const ENV_STEPS : str = "TRIOS_STEPS"; +const ENV_RUST_LOG : str = "RUST_LOG"; + +// The canonical Gate-2 seed set. Any `up` that does not contain all three +// fails fast at plan-time; Gate-2 requires a 3-seed pass. +const GATE2_SEEDS : [u64] = [43, 44, 45]; + +// ---------------------------------------------------------------------- +// Types +// ---------------------------------------------------------------------- + +/** + * Local persistence of the Railway binding (project UUID, endpoint, + * optional explicit image). Written by `tri railway link`. + */ +pub struct RailwayBinding { + project_id : str, + endpoint : str, + image : Option, + linked_at : str, // ISO-8601 + linked_by : str, // "agent:" +} + +/** + * A single service plan -- the input to `serviceCreate` / `variableUpsert` + * / `serviceInstanceDeployV2`. `up` produces one plan per seed. + */ +pub struct ServicePlan { + name : str, // "trainer-seed-" + seed : u64, + image : str, + target_bpb : f64, + steps : u64, + vars : Map, // TRIOS_* + RUST_LOG +} + +/** + * Result of a single `serviceInstanceDeployV2` call. + */ +pub struct DeployResult { + service_name : str, + service_id : Option, // None iff dry-run + deployment_id: Option, // None iff dry-run + status : str, // "planned" | "created" | "deploying" | "failed" + message : Option, +} + +/** + * Top-level outcome of `tri railway up`. Success iff every seed is at + * least "created" (and, when not dry-run, "deploying"). + */ +pub struct UpOutcome { + project_id : str, + dry_run : bool, + results : [DeployResult], + all_started : bool, +} + +// ---------------------------------------------------------------------- +// Plan construction (pure, no I/O) +// ---------------------------------------------------------------------- + +/** + * Produce the ServicePlan for a single seed. Deterministic -- given the + * same inputs, the output is byte-identical. This is the property + * `bench cli_railway_plan_builder_determinism` pins. + */ +fn build_service_plan(seed: u64, image: &str, target_bpb: f64, steps: u64) -> ServicePlan { + let mut vars = Map::new(); + vars.insert(ENV_SEED, seed.to_string()); + vars.insert(ENV_LEDGER_PUSH, "1"); + vars.insert(ENV_TARGET_BPB, target_bpb.to_string()); + vars.insert(ENV_STEPS, steps.to_string()); + vars.insert(ENV_RUST_LOG, "info"); + return ServicePlan { + name: "trainer-seed-" + seed.to_string(), + seed: seed, + image: image.to_string(), + target_bpb: target_bpb, + steps: steps, + vars: vars, + }; +} + +/** + * Produce one plan per requested seed. The order mirrors the input + * slice so output is stable for tests and logs. + */ +fn build_plans(seeds: &[u64], image: &str, target_bpb: f64, steps: u64) -> [ServicePlan] { + return seeds.iter().map(|&s| build_service_plan(s, image, target_bpb, steps)).collect(); +} + +/** + * Gate-2 precondition: the requested seed set must be a superset of the + * canonical {43, 44, 45}. Missing any one of those MUST fail fast before + * any Railway mutation -- Gate-2 cannot pass with fewer than 3 distinct + * seeds. + */ +fn is_valid_gate2_seed_set(seeds: &[u64]) -> bool { + for required in GATE2_SEEDS { + if !seeds.contains(&required) { + return false; + } + } + return true; +} + +// ---------------------------------------------------------------------- +// GraphQL envelope builders (pure, no I/O) +// ---------------------------------------------------------------------- + +/** + * Minimal `me { id email }` query used by `tri railway login --token` + * to validate that RAILWAY_TOKEN is syntactically and semantically + * valid before anything else runs. + */ +fn build_login_query() -> str { + return "{ \"query\": \"query { me { id email } }\" }"; +} + +/** + * The GraphQL body for `serviceCreate` bound to an explicit project and + * image. `tri railway up` emits one of these per seed. + */ +fn build_service_create_body(project_id: &str, plan: &ServicePlan) -> str { + // NOTE: the .t27 renders this as a single JSON string at gen time. + // The Rust backend uses serde_json::to_string for true escaping. + return json!({ + "query": "mutation($input: ServiceCreateInput!) { serviceCreate(input: $input) { id name } }", + "variables": { + "input": { + "projectId": project_id, + "name": plan.name, + "source": { "image": plan.image } + } + } + }); +} + +// ---------------------------------------------------------------------- +// Embargo guard (R9) +// ---------------------------------------------------------------------- + +/** + * Refuse to deploy if the current HEAD SHA is in the embargo file. + * Runs BEFORE any mutation. Returns true iff the SHA is embargoed; + * the caller MUST treat true as a hard abort. + */ +fn head_is_embargoed(embargo_lines: &[str], head_sha: &str) -> bool { + let needle = head_sha.to_lowercase(); + for line in embargo_lines { + let entry = line.trim().to_lowercase(); + if entry.is_empty() { continue; } + if entry == needle { return true; } + // Any matching 7-char prefix is enough -- R9 applies to both the + // full SHA and its short form. + if needle.len() >= 7 && entry.len() >= 7 + && entry[0..7] == needle[0..7] { + return true; + } + } + return false; +} + +// ---------------------------------------------------------------------- +// TDD Tests (TDD-INSIDE-SPEC, ADR-003) +// ---------------------------------------------------------------------- + +test cli_railway_plan_for_seed_43 + given seed = 43 + and image = "ghcr.io/ghashtag/trios-trainer-igla:latest" + and plan = build_service_plan(seed, image, 1.85, 30_000) + then plan.name == "trainer-seed-43" + and plan.seed == 43 + and plan.vars.get("TRIOS_SEED") == "43" + and plan.vars.get("TRIOS_LEDGER_PUSH") == "1" + and plan.vars.get("TRIOS_TARGET_BPB") == "1.85" + +test cli_railway_plan_for_seed_44 + given plan = build_service_plan(44, "img", 1.85, 30_000) + then plan.name == "trainer-seed-44" + and plan.vars.get("TRIOS_SEED") == "44" + +test cli_railway_plan_for_seed_45 + given plan = build_service_plan(45, "img", 1.85, 30_000) + then plan.name == "trainer-seed-45" + and plan.vars.get("TRIOS_SEED") == "45" + +test cli_railway_build_plans_preserves_order + given seeds = [43, 44, 45] + and plans = build_plans(seeds, "img", 1.85, 30_000) + then plans.len() == 3 + and plans[0].seed == 43 + and plans[1].seed == 44 + and plans[2].seed == 45 + +test cli_railway_gate2_seed_set_accepts_canonical + given seeds = [43, 44, 45] + then is_valid_gate2_seed_set(seeds) == true + +test cli_railway_gate2_seed_set_accepts_superset + given seeds = [43, 44, 45, 46] + then is_valid_gate2_seed_set(seeds) == true + +test cli_railway_gate2_seed_set_rejects_missing_seed + given seeds = [43, 44] + then is_valid_gate2_seed_set(seeds) == false + +test cli_railway_gate2_seed_set_rejects_empty + given seeds = [] + then is_valid_gate2_seed_set(seeds) == false + +test cli_railway_embargo_refuses_full_match + given embargo = ["477e3377"] + and sha = "477e3377" + then head_is_embargoed(embargo, sha) == true + +test cli_railway_embargo_refuses_prefix_match + given embargo = ["477e3377deadbeef"] + and sha = "477e3377" + then head_is_embargoed(embargo, sha) == true + +test cli_railway_embargo_accepts_clean_sha + given embargo = ["477e3377", "b3ee6a36"] + and sha = "2446855" + then head_is_embargoed(embargo, sha) == false + +test cli_railway_login_query_shape + then build_login_query().contains("me { id email }") == true + +// ---------------------------------------------------------------------- +// Invariants +// ---------------------------------------------------------------------- + +invariant cli_railway_seed_quorum_is_three + assert GATE2_SEED_QUORUM == 3 + +invariant cli_railway_canonical_seeds_are_43_44_45 + assert GATE2_SEEDS == [43, 44, 45] + +invariant cli_railway_target_below_champion + // The deploy target must be strictly below the current champion BPB + // (champion = 2.2393 @ 27K seed=43 sha=2446855). + assert DEFAULT_TARGET_BPB < 2.2393 + +invariant cli_railway_phi_anchor_holds + // The TRINITY anchor is the constitutional reason this CLI exists + // (without phi^2 + phi^-2 = 3 there is no IGLA RACE). + given lhs = (PHI * PHI) + (1.0 / (PHI * PHI)) + assert (lhs - TRINITY_ANCHOR) < 1.0e-10 + assert (TRINITY_ANCHOR - lhs) < 1.0e-10 + +invariant cli_railway_embargo_refusal_is_mandatory + // Whenever the embargo list matches the HEAD SHA, up() MUST abort. + // This is R9 and cannot be relaxed by any flag. + given embargo = ["477e3377"] + and sha = "477e3377" + assert head_is_embargoed(embargo, sha) == true + +invariant cli_railway_service_naming_is_stable + // The service name for a given seed is a pure function of the seed. + // Tests and status lookups depend on this. + given plan_a = build_service_plan(43, "img", 1.85, 30_000) + and plan_b = build_service_plan(43, "img", 1.85, 30_000) + assert plan_a.name == plan_b.name + +// ---------------------------------------------------------------------- +// Benchmarks +// ---------------------------------------------------------------------- + +bench cli_railway_plan_builder_determinism + measure: nanoseconds to build a 3-seed plan set + target: < 50_000 + +bench cli_railway_graphql_body_size + measure: bytes per build_service_create_body call + target: < 2_048 diff --git a/specs/isa/ternary_deque.t27 b/specs/isa/ternary_deque.t27 index 9db941c9..ea77621f 100644 --- a/specs/isa/ternary_deque.t27 +++ b/specs/isa/ternary_deque.t27 @@ -299,6 +299,28 @@ module TernaryDeque { assert deque_is_empty(count) + test deque_wraparound_circular + var data : [4]i32 = undefined; + var front : usize = 0; + var back : usize = 0; + var count : usize = 0; + deque_init(&data, &front, &back, &count); + + deque_push_back(&data, &front, &back, &count, TRIT_POS); + deque_push_back(&data, &front, &back, &count, TRIT_NEG); + deque_push_back(&data, &front, &back, &count, TRIT_ZERO); + deque_push_back(&data, &front, &back, &count, TRIT_POS); + + var v1 = deque_pop_front(&data, &front, &back, &count); + assert v1 == TRIT_POS + var v2 = deque_pop_front(&data, &front, &back, &count); + assert v2 == TRIT_NEG + + deque_push_back(&data, &front, &back, &count, TRIT_NEG); + deque_push_back(&data, &front, &back, &count, TRIT_ZERO); + + assert count == 4 + // 404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 // 4. TDD - Invariants // 457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 diff --git a/specs/numeric/pellis_verify.t27 b/specs/numeric/pellis_verify.t27 new file mode 100644 index 00000000..5212eac1 --- /dev/null +++ b/specs/numeric/pellis_verify.t27 @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +// t27/specs/numeric/pellis_verify.t27 +// Phase 2 (issue #289): GMP-backed Pellis verification spec +// Verifies Pellis closed-form alpha^-1 against mpmath 100-digit reference +// SSOT for release gates remains specs/*.t27 + tri / t27c +// +// Anchor (L5): phi^2 + phi^-2 = 3 | TRINITY + +module PellisVerify { + use math::constants; + use physics::pellis-formulas; + + const PHI : f64 = constants::PHI; + const PHI_SQ : f64 = PHI * PHI; + const PHI_INV : f64 = 1.0 / PHI; + + // Pre-registered Pellis alpha^-1 closed form (row 31, FORMULA_TABLE.md) + // 360/phi^2 - 2/phi^3 + (3*phi)^-5 + // Fixed to 50 digits for pre-registration checkpoint + const PELLIS_ALPHA_INV : f64 = 360.0 / PHI_SQ - 2.0 / (PHI * PHI_SQ) + 1.0 / ((3.0 * PHI) * (3.0 * PHI) * (3.0 * PHI) * (3.0 * PHI) * (3.0 * PHI)); + + // CODATA 2022 alpha^-1 reference value + const CODATA_2022_ALPHA_INV : f64 = 137.035999177; + + // Pre-registered 50-digit checkpoint (from mpmath at dps=100) + const PELLIS_PREREG_50_DIGITS : string = "137.03599916476639345065182598341596570459733761749"; + + // Tolerance for f64 comparison (phi-derived values are approximate) + const F64_TOLERANCE : f64 = 1e-6; + + // 123456789101112131415161718192021222324252627282930 + // 1. Closed-form computation + // 313233343536373839404142434445464748495051525354555657585960616263646566 + + fn compute_pellis_term1() -> f64 { + return 360.0 / PHI_SQ; + } + + fn compute_pellis_term2() -> f64 { + return 2.0 / (PHI * PHI_SQ); + } + + fn compute_pellis_term3() -> f64 { + var three_phi : f64 = 3.0 * PHI; + var result : f64 = 1.0; + var i : usize = 0; + while (i < 5) { + result = result / three_phi; + i = i + 1; + } + return result; + } + + fn compute_pellis_full() -> f64 { + return compute_pellis_term1() - compute_pellis_term2() + compute_pellis_term3(); + } + + fn pellis_vs_codata_error() -> f64 { + return abs(PELLIS_ALPHA_INV - CODATA_2022_ALPHA_INV); + } + + // 676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 + // 2. TDD - Tests + // 126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 + + test pellis_term1_positive + then compute_pellis_term1() > 100.0 + and compute_pellis_term1() < 200.0 + + test pellis_term2_small + then compute_pellis_term2() > 0.0 + and compute_pellis_term2() < 1.0 + + test pellis_term3_tiny + then compute_pellis_term3() > 0.0 + and compute_pellis_term3() < 0.01 + + test pellis_full_matches_constant + then abs(compute_pellis_full() - PELLIS_ALPHA_INV) < F64_TOLERANCE + + test pellis_near_alpha_inverse + then PELLIS_ALPHA_INV > 130.0 + and PELLIS_ALPHA_INV < 145.0 + + test pellis_codata_relative_error + var rel_err : f64 = pellis_vs_codata_error() / CODATA_2022_ALPHA_INV; + then rel_err < 0.001 + + test pellis_pre_registered_checkpoint_exists + then PELLIS_PREREG_50_DIGITS != "" + + // 178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 + // 3. Invariants + // 223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 + + invariant pellis_always_positive + then PELLIS_ALPHA_INV > 0.0 + + invariant pellis_codata_order_of_magnitude + then PELLIS_ALPHA_INV > 100.0 + and PELLIS_ALPHA_INV < 200.0 + + invariant pellis_preregistered_checkpoint_fixed + then PELLIS_PREREG_50_DIGITS == "137.03599916476639345065182598341596570459733761749" + + invariant term1_dominates + then compute_pellis_term1() > compute_pellis_term2() + + invariant closed_form_self_consistent + then abs(compute_pellis_full() - PELLIS_ALPHA_INV) < F64_TOLERANCE + + // 261262263264265266267268269270271272273274275276277278279280281282283284285 + // 4. Benchmarks + // 286287288289290291292293294295296297298299300301302303304305306307308309310 + + bench pellis_compute_latency + var x = compute_pellis_full() + target_cycles 100 + + bench pellis_term_decomposition + var t1 = compute_pellis_term1() + var t2 = compute_pellis_term2() + var t3 = compute_pellis_term3() + target_cycles 300 + + bench pellis_vs_codata_check + var err = pellis_vs_codata_error() + target_cycles 100 + + bench pellis_f64_roundtrip + var v = PELLIS_ALPHA_INV + target_cycles 50 + + bench pre_registered_lookup + var s = PELLIS_PREREG_50_DIGITS + target_cycles 50 +}