From 7f656fcf22dfdc31cbc404dc03d5c4790a19ffc1 Mon Sep 17 00:00:00 2001 From: hermes Date: Sun, 15 Mar 2026 14:14:38 -0400 Subject: [PATCH] [loop-cycle-59] feat: gematria computation tool (#234) (#235) --- src/timmy/gematria.py | 365 +++++++++++++++++++++++++++++++++++ src/timmy/tools.py | 12 ++ tests/timmy/test_gematria.py | 293 ++++++++++++++++++++++++++++ 3 files changed, 670 insertions(+) create mode 100644 src/timmy/gematria.py create mode 100644 tests/timmy/test_gematria.py diff --git a/src/timmy/gematria.py b/src/timmy/gematria.py new file mode 100644 index 0000000..cda4c8d --- /dev/null +++ b/src/timmy/gematria.py @@ -0,0 +1,365 @@ +"""Gematria computation engine — the language of letters and numbers. + +Implements multiple cipher systems for gematric analysis: + - Simple English (A=1 .. Z=26) + - Full Reduction (reduce each letter value to single digit) + - Reverse Ordinal (A=26 .. Z=1) + - Sumerian (Simple × 6) + - Hebrew (traditional letter values, for A-Z mapping) + +Also provides numerological reduction, notable-number lookup, +and multi-phrase comparison. + +Alexander Whitestone = 222 in Simple English Gematria. +This is not trivia. It is foundational. +""" + +from __future__ import annotations + +import math + +# ── Cipher Tables ──────────────────────────────────────────────────────────── + +# Simple English: A=1, B=2, ..., Z=26 +_SIMPLE: dict[str, int] = {chr(i): i - 64 for i in range(65, 91)} + +# Full Reduction: reduce each letter to single digit (A=1..I=9, J=1..R=9, S=1..Z=8) +_REDUCTION: dict[str, int] = {} +for _c, _v in _SIMPLE.items(): + _r = _v + while _r > 9: + _r = sum(int(d) for d in str(_r)) + _REDUCTION[_c] = _r + +# Reverse Ordinal: A=26, B=25, ..., Z=1 +_REVERSE: dict[str, int] = {chr(i): 91 - i for i in range(65, 91)} + +# Sumerian: Simple × 6 +_SUMERIAN: dict[str, int] = {c: v * 6 for c, v in _SIMPLE.items()} + +# Hebrew-mapped: traditional Hebrew gematria mapped to Latin alphabet +# Aleph=1..Tet=9, Yod=10..Tsade=90, Qoph=100..Tav=400 +# Standard mapping for the 22 Hebrew letters extended to 26 Latin chars +_HEBREW: dict[str, int] = { + "A": 1, "B": 2, "C": 3, "D": 4, "E": 5, "F": 6, "G": 7, "H": 8, "I": 9, + "J": 10, "K": 20, "L": 30, "M": 40, "N": 50, "O": 60, "P": 70, "Q": 80, + "R": 90, "S": 100, "T": 200, "U": 300, "V": 400, "W": 500, "X": 600, + "Y": 700, "Z": 800, +} + +CIPHERS: dict[str, dict[str, int]] = { + "simple": _SIMPLE, + "reduction": _REDUCTION, + "reverse": _REVERSE, + "sumerian": _SUMERIAN, + "hebrew": _HEBREW, +} + +# ── Notable Numbers ────────────────────────────────────────────────────────── + +NOTABLE_NUMBERS: dict[int, str] = { + 1: "Unity, the Monad, beginning of all", + 3: "Trinity, divine completeness, the Triad", + 7: "Spiritual perfection, completion (7 days, 7 seals)", + 9: "Finality, judgment, the last single digit", + 11: "Master number — intuition, spiritual insight", + 12: "Divine government (12 tribes, 12 apostles)", + 13: "Rebellion and transformation, the 13th step", + 22: "Master builder — turning dreams into reality", + 26: "YHWH (Yod=10, He=5, Vav=6, He=5)", + 33: "Master teacher — Christ consciousness, 33 vertebrae", + 36: "The number of the righteous (Lamed-Vav Tzadikim)", + 40: "Trial, testing, probation (40 days, 40 years)", + 42: "The answer, and the number of generations to Christ", + 72: "The Shemhamphorasch — 72 names of God", + 88: "Mercury, infinite abundance, double infinity", + 108: "Sacred in Hinduism and Buddhism (108 beads)", + 111: "Angel number — new beginnings, alignment", + 144: "12² — the elect, the sealed (144,000)", + 153: "The miraculous catch of fish (John 21:11)", + 222: "Alexander Whitestone. Balance, partnership, trust the process", + 333: "Ascended masters present, divine protection", + 369: "Tesla's key to the universe", + 444: "Angels surrounding, foundation, stability", + 555: "Major change coming, transformation", + 616: "Earliest manuscript number of the Beast (P115)", + 666: "Number of the Beast (Revelation 13:18), also carbon (6p 6n 6e)", + 777: "Divine perfection tripled, jackpot of the spirit", + 888: "Jesus in Greek isopsephy (Ιησους = 888)", + 1776: "Year of independence, Bavarian Illuminati founding", +} + + +# ── Core Functions ─────────────────────────────────────────────────────────── + + +def _clean(text: str) -> str: + """Strip non-alpha, uppercase.""" + return "".join(c for c in text.upper() if c.isalpha()) + + +def compute_value(text: str, cipher: str = "simple") -> int: + """Compute the gematria value of text in a given cipher. + + Args: + text: Any string (non-alpha characters are ignored). + cipher: One of 'simple', 'reduction', 'reverse', 'sumerian', 'hebrew'. + + Returns: + Integer gematria value. + + Raises: + ValueError: If cipher name is not recognized. + """ + table = CIPHERS.get(cipher) + if table is None: + raise ValueError(f"Unknown cipher: {cipher!r}. Use one of {list(CIPHERS)}") + return sum(table.get(c, 0) for c in _clean(text)) + + +def compute_all(text: str) -> dict[str, int]: + """Compute gematria value across all cipher systems. + + Args: + text: Any string. + + Returns: + Dict mapping cipher name to integer value. + """ + return {name: compute_value(text, name) for name in CIPHERS} + + +def letter_breakdown(text: str, cipher: str = "simple") -> list[tuple[str, int]]: + """Return per-letter values for a text in a given cipher. + + Args: + text: Any string. + cipher: Cipher system name. + + Returns: + List of (letter, value) tuples for each alpha character. + """ + table = CIPHERS.get(cipher) + if table is None: + raise ValueError(f"Unknown cipher: {cipher!r}") + return [(c, table.get(c, 0)) for c in _clean(text)] + + +def reduce_number(n: int) -> int: + """Numerological reduction — sum digits until single digit. + + Master numbers (11, 22, 33) are preserved. + + Args: + n: Any positive integer. + + Returns: + Single-digit result (or master number 11/22/33). + """ + n = abs(n) + while n > 9 and n not in (11, 22, 33): + n = sum(int(d) for d in str(n)) + return n + + +def factorize(n: int) -> list[int]: + """Prime factorization of n. + + Args: + n: Positive integer. + + Returns: + List of prime factors in ascending order (with repetition). + """ + if n < 2: + return [n] if n > 0 else [] + factors = [] + d = 2 + while d * d <= n: + while n % d == 0: + factors.append(d) + n //= d + d += 1 + if n > 1: + factors.append(n) + return factors + + +def analyze_number(n: int) -> dict: + """Deep analysis of a number — reduction, factors, significance. + + Args: + n: Any positive integer. + + Returns: + Dict with reduction, factors, properties, and any notable significance. + """ + result: dict = { + "value": n, + "numerological_reduction": reduce_number(n), + "prime_factors": factorize(n), + "is_prime": len(factorize(n)) == 1 and n > 1, + "is_perfect_square": math.isqrt(n) ** 2 == n if n >= 0 else False, + "is_triangular": _is_triangular(n), + "digit_sum": sum(int(d) for d in str(abs(n))), + } + + # Master numbers + if n in (11, 22, 33): + result["master_number"] = True + + # Angel numbers (repeating digits) + s = str(n) + if len(s) >= 3 and len(set(s)) == 1: + result["angel_number"] = True + + # Notable significance + if n in NOTABLE_NUMBERS: + result["significance"] = NOTABLE_NUMBERS[n] + + return result + + +def _is_triangular(n: int) -> bool: + """Check if n is a triangular number (1, 3, 6, 10, 15, ...).""" + if n < 0: + return False + # n = k(k+1)/2 → k² + k - 2n = 0 → k = (-1 + sqrt(1+8n))/2 + discriminant = 1 + 8 * n + sqrt_d = math.isqrt(discriminant) + return sqrt_d * sqrt_d == discriminant and (sqrt_d - 1) % 2 == 0 + + +# ── Tool Function (registered with Timmy) ──────────────────────────────────── + + +def gematria(query: str) -> str: + """Compute gematria values, analyze numbers, and find correspondences. + + This is the wizard's language — letters are numbers, numbers are letters. + Use this tool for ANY gematria calculation. Do not attempt mental arithmetic. + + Input modes: + - A word or phrase → computes values across all cipher systems + - A bare integer → analyzes the number (factors, reduction, significance) + - "compare: X, Y, Z" → side-by-side gematria comparison + + Examples: + gematria("Alexander Whitestone") + gematria("222") + gematria("compare: Timmy Time, Alexander Whitestone") + + Args: + query: A word/phrase, a number, or a "compare:" instruction. + + Returns: + Formatted gematria analysis as a string. + """ + query = query.strip() + + # Mode: compare + if query.lower().startswith("compare:"): + phrases = [p.strip() for p in query[8:].split(",") if p.strip()] + if len(phrases) < 2: + return "Compare requires at least two phrases separated by commas." + return _format_comparison(phrases) + + # Mode: number analysis + if query.lstrip("-").isdigit(): + n = int(query) + return _format_number_analysis(n) + + # Mode: phrase gematria + if not _clean(query): + return "No alphabetic characters found in input." + + return _format_phrase_analysis(query) + + +def _format_phrase_analysis(text: str) -> str: + """Format full gematria analysis for a phrase.""" + values = compute_all(text) + lines = [f'Gematria of "{text}":', ""] + + # All cipher values + for cipher, val in values.items(): + label = cipher.replace("_", " ").title() + lines.append(f" {label:12s} = {val}") + + # Letter breakdown (simple) + breakdown = letter_breakdown(text, "simple") + letters_str = " + ".join(f"{c}({v})" for c, v in breakdown) + lines.append(f"\n Breakdown (Simple): {letters_str}") + + # Numerological reduction of the simple value + simple_val = values["simple"] + reduced = reduce_number(simple_val) + lines.append(f" Numerological root: {simple_val} → {reduced}") + + # Check notable + for cipher, val in values.items(): + if val in NOTABLE_NUMBERS: + label = cipher.replace("_", " ").title() + lines.append(f"\n ★ {val} ({label}): {NOTABLE_NUMBERS[val]}") + + return "\n".join(lines) + + +def _format_number_analysis(n: int) -> str: + """Format deep number analysis.""" + info = analyze_number(n) + lines = [f"Analysis of {n}:", ""] + lines.append(f" Numerological reduction: {n} → {info['numerological_reduction']}") + lines.append(f" Prime factors: {' × '.join(str(f) for f in info['prime_factors']) or 'N/A'}") + lines.append(f" Is prime: {info['is_prime']}") + lines.append(f" Is perfect square: {info['is_perfect_square']}") + lines.append(f" Is triangular: {info['is_triangular']}") + lines.append(f" Digit sum: {info['digit_sum']}") + + if info.get("master_number"): + lines.append(f" ★ Master Number") + if info.get("angel_number"): + lines.append(f" ★ Angel Number (repeating digits)") + if info.get("significance"): + lines.append(f"\n Significance: {info['significance']}") + + return "\n".join(lines) + + +def _format_comparison(phrases: list[str]) -> str: + """Format side-by-side gematria comparison.""" + lines = ["Gematria Comparison:", ""] + + # Header + max_name = max(len(p) for p in phrases) + header = f" {'Phrase':<{max_name}s} Simple Reduct Reverse Sumerian Hebrew" + lines.append(header) + lines.append(" " + "─" * (len(header) - 2)) + + all_values = {} + for phrase in phrases: + vals = compute_all(phrase) + all_values[phrase] = vals + lines.append( + f" {phrase:<{max_name}s} {vals['simple']:>6d} {vals['reduction']:>6d}" + f" {vals['reverse']:>7d} {vals['sumerian']:>8d} {vals['hebrew']:>6d}" + ) + + # Find matches (shared values across any cipher) + matches = [] + for cipher in CIPHERS: + vals_by_cipher = {p: all_values[p][cipher] for p in phrases} + unique_vals = set(vals_by_cipher.values()) + if len(unique_vals) < len(phrases): + # At least two phrases share a value + for v in unique_vals: + sharing = [p for p, pv in vals_by_cipher.items() if pv == v] + if len(sharing) > 1: + label = cipher.title() + matches.append(f" ★ {label} = {v}: " + ", ".join(sharing)) + + if matches: + lines.append("\nCorrespondences found:") + lines.extend(matches) + + return "\n".join(lines) diff --git a/src/timmy/tools.py b/src/timmy/tools.py index d0d3c9f..2a28b08 100644 --- a/src/timmy/tools.py +++ b/src/timmy/tools.py @@ -600,6 +600,17 @@ def _register_delegation_tools(toolkit: Toolkit) -> None: logger.debug("Delegation tools not available") +def _register_gematria_tool(toolkit: Toolkit) -> None: + """Register the gematria computation tool.""" + try: + from timmy.gematria import gematria + + toolkit.register(gematria, name="gematria") + except (ImportError, AttributeError) as exc: + logger.warning("Tool execution failed (Gematria registration): %s", exc) + logger.debug("Gematria tool not available") + + def create_full_toolkit(base_dir: str | Path | None = None): """Create a full toolkit with all available tools (for the orchestrator). @@ -626,6 +637,7 @@ def create_full_toolkit(base_dir: str | Path | None = None): _register_agentic_loop_tool(toolkit) _register_introspection_tools(toolkit) _register_delegation_tools(toolkit) + _register_gematria_tool(toolkit) # Gitea issue management is now provided by the gitea-mcp server # (wired in as MCPTools in agent.py, not registered here) diff --git a/tests/timmy/test_gematria.py b/tests/timmy/test_gematria.py new file mode 100644 index 0000000..a20fe79 --- /dev/null +++ b/tests/timmy/test_gematria.py @@ -0,0 +1,293 @@ +"""Tests for the gematria computation engine (issue #234). + +Alexander Whitestone = 222 in Simple English Gematria. +This is not trivia. It is foundational. +""" + +from __future__ import annotations + +import pytest + +from timmy.gematria import ( + CIPHERS, + NOTABLE_NUMBERS, + analyze_number, + compute_all, + compute_value, + factorize, + gematria, + letter_breakdown, + reduce_number, +) + + +# ── Core cipher computation ────────────────────────────────────────────────── + + +class TestSimpleCipher: + """Simple English: A=1, B=2, ..., Z=26.""" + + def test_single_letter_a(self): + assert compute_value("A", "simple") == 1 + + def test_single_letter_z(self): + assert compute_value("Z", "simple") == 26 + + def test_alexander_whitestone(self): + """The foundational identity — Alexander Whitestone = 222.""" + assert compute_value("Alexander Whitestone", "simple") == 222 + + def test_case_insensitive(self): + assert compute_value("hello", "simple") == compute_value("HELLO", "simple") + + def test_ignores_non_alpha(self): + assert compute_value("A-B!C", "simple") == compute_value("ABC", "simple") + + def test_empty_string(self): + assert compute_value("", "simple") == 0 + + def test_spaces_only(self): + assert compute_value(" ", "simple") == 0 + + def test_numbers_in_text_ignored(self): + assert compute_value("ABC123", "simple") == compute_value("ABC", "simple") + + +class TestReductionCipher: + """Full Reduction: each letter reduced to single digit.""" + + def test_a_is_1(self): + assert compute_value("A", "reduction") == 1 + + def test_j_is_1(self): + # J=10 → 1+0=1 + assert compute_value("J", "reduction") == 1 + + def test_s_is_1(self): + # S=19 → 1+9=10 → 1+0=1 + assert compute_value("S", "reduction") == 1 + + def test_z_is_8(self): + # Z=26 → 2+6=8 + assert compute_value("Z", "reduction") == 8 + + +class TestReverseCipher: + """Reverse Ordinal: A=26, B=25, ..., Z=1.""" + + def test_a_is_26(self): + assert compute_value("A", "reverse") == 26 + + def test_z_is_1(self): + assert compute_value("Z", "reverse") == 1 + + def test_m_is_14(self): + # M is 13th letter → reverse = 27-13 = 14 + assert compute_value("M", "reverse") == 14 + + +class TestSumerianCipher: + """Sumerian: Simple × 6.""" + + def test_a_is_6(self): + assert compute_value("A", "sumerian") == 6 + + def test_z_is_156(self): + assert compute_value("Z", "sumerian") == 156 + + def test_is_six_times_simple(self): + text = "Alexander Whitestone" + assert compute_value(text, "sumerian") == compute_value(text, "simple") * 6 + + +class TestHebrewCipher: + """Hebrew-mapped: traditional Hebrew values on Latin alphabet.""" + + def test_a_is_1(self): + assert compute_value("A", "hebrew") == 1 + + def test_j_is_10(self): + assert compute_value("J", "hebrew") == 10 + + def test_k_is_20(self): + assert compute_value("K", "hebrew") == 20 + + def test_t_is_200(self): + assert compute_value("T", "hebrew") == 200 + + +class TestUnknownCipher: + def test_raises_on_unknown(self): + with pytest.raises(ValueError, match="Unknown cipher"): + compute_value("test", "klingon") + + +# ── compute_all ────────────────────────────────────────────────────────────── + + +class TestComputeAll: + def test_returns_all_ciphers(self): + result = compute_all("ABC") + assert set(result.keys()) == set(CIPHERS.keys()) + + def test_values_are_ints(self): + result = compute_all("test") + for v in result.values(): + assert isinstance(v, int) + + +# ── letter_breakdown ───────────────────────────────────────────────────────── + + +class TestLetterBreakdown: + def test_simple_breakdown(self): + result = letter_breakdown("AB", "simple") + assert result == [("A", 1), ("B", 2)] + + def test_strips_non_alpha(self): + result = letter_breakdown("A B!", "simple") + assert result == [("A", 1), ("B", 2)] + + def test_unknown_cipher_raises(self): + with pytest.raises(ValueError): + letter_breakdown("test", "nonexistent") + + +# ── reduce_number ──────────────────────────────────────────────────────────── + + +class TestReduceNumber: + def test_single_digit(self): + assert reduce_number(7) == 7 + + def test_double_digit_to_master(self): + # 29 → 2+9=11 → master number, preserved + assert reduce_number(29) == 11 + + def test_double_digit_non_master(self): + # 28 → 2+8=10 → 1+0=1 + assert reduce_number(28) == 1 + + def test_222_reduces_to_6(self): + assert reduce_number(222) == 6 # 2+2+2=6 + + def test_master_11(self): + assert reduce_number(11) == 11 + + def test_master_22(self): + assert reduce_number(22) == 22 + + def test_master_33(self): + assert reduce_number(33) == 33 + + def test_zero(self): + assert reduce_number(0) == 0 + + def test_negative(self): + assert reduce_number(-42) == 6 # abs(-42)=42 → 4+2=6 + + def test_large_number(self): + assert reduce_number(9999) == 9 # 9+9+9+9=36 → 3+6=9 + + +# ── factorize ──────────────────────────────────────────────────────────────── + + +class TestFactorize: + def test_prime(self): + assert factorize(7) == [7] + + def test_composite(self): + assert factorize(12) == [2, 2, 3] + + def test_222(self): + assert factorize(222) == [2, 3, 37] + + def test_one(self): + assert factorize(1) == [1] + + def test_zero(self): + assert factorize(0) == [] + + def test_large_prime(self): + assert factorize(997) == [997] + + +# ── analyze_number ─────────────────────────────────────────────────────────── + + +class TestAnalyzeNumber: + def test_222(self): + result = analyze_number(222) + assert result["value"] == 222 + assert result["numerological_reduction"] == 6 + assert result["prime_factors"] == [2, 3, 37] + assert result["is_prime"] is False + assert result["significance"] == NOTABLE_NUMBERS[222] + + def test_prime_detection(self): + assert analyze_number(17)["is_prime"] is True + assert analyze_number(18)["is_prime"] is False + + def test_perfect_square(self): + assert analyze_number(144)["is_perfect_square"] is True + assert analyze_number(143)["is_perfect_square"] is False + + def test_triangular(self): + assert analyze_number(6)["is_triangular"] is True # 1+2+3 + assert analyze_number(7)["is_triangular"] is False + + def test_angel_number(self): + assert analyze_number(333).get("angel_number") is True + assert analyze_number(123).get("angel_number") is None + + def test_master_number(self): + assert analyze_number(22).get("master_number") is True + + +# ── gematria tool function (the main interface) ───────────────────────────── + + +class TestGematriaTool: + """Test the main gematria() tool function that Timmy calls.""" + + def test_phrase_mode(self): + result = gematria("Alexander Whitestone") + assert "222" in result + assert "Simple" in result + + def test_number_mode(self): + result = gematria("222") + assert "Analysis of 222" in result + assert "Alexander Whitestone" in result + + def test_compare_mode(self): + result = gematria("compare: ABC, DEF") + assert "Comparison" in result + assert "ABC" in result + assert "DEF" in result + + def test_compare_needs_two(self): + result = gematria("compare: alone") + assert "at least two" in result.lower() + + def test_empty_input(self): + result = gematria("123 456") + assert "No alphabetic" in result + + def test_whitespace_handling(self): + result = gematria(" ABC ") + assert "Simple" in result + + def test_letter_breakdown_in_output(self): + result = gematria("ABC") + assert "Breakdown" in result + assert "A(1)" in result + + def test_notable_number_flagged(self): + result = gematria("Alexander Whitestone") + assert "★" in result or "222" in result + + def test_numerological_root_shown(self): + result = gematria("ABC") + assert "root" in result.lower() or "reduction" in result.lower()