Add Ikario Architecture v2 - Phases 1-8 complete
Implements the processual architecture based on Whitehead's Process Philosophy and Peirce's Semiotics. Core paradigm: "L'espace latent pense. Le LLM traduit." (The latent space thinks. The LLM translates.) Phase 1-4: Core semiotic cycle - StateTensor 8x1024 (8 Peircean dimensions) - Dissonance computation with hard negatives - Fixation via 4 Peircean methods (Tenacity, Authority, A Priori, Science) - LatentEngine orchestrating the full cycle Phase 5: StateToLanguage - LLM as pure translator (zero-reasoning, T=0) - Projection on interpretable directions - Reasoning markers detection (Amendment #4) Phase 6: Vigilance - x_ref (David) as guard-rail, NOT attractor - Drift detection per dimension and globally - Alerts: ok, warning, critical Phase 7: Autonomous Daemon - Two modes: CONVERSATION (always verbalize), AUTONOMOUS (~1000 cycles/day) - Amendment #5: 50% probability on unresolved impacts - TriggerGenerator with weighted random selection Phase 8: Integration & Metrics - ProcessMetrics for daily/weekly reports - Health status monitoring - Integration tests validating all modules 297 tests passing, version 0.7.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
383
ikario_processual/tests/test_fixation.py
Normal file
383
ikario_processual/tests/test_fixation.py
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests pour le module de fixation - Phase 3.
|
||||
|
||||
Les 4 méthodes de Peirce :
|
||||
1. Tenacity (0.05) - Minimal
|
||||
2. Authority (0.25) - Pacte multi-vecteurs
|
||||
3. A Priori (0.25) - Cohérence
|
||||
4. Science (0.45) - Dominant
|
||||
|
||||
Exécuter: pytest ikario_processual/tests/test_fixation.py -v
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from ikario_processual.state_tensor import StateTensor, DIMENSION_NAMES, EMBEDDING_DIM
|
||||
from ikario_processual.fixation import (
|
||||
FixationConfig,
|
||||
FixationResult,
|
||||
Tenacity,
|
||||
Authority,
|
||||
APriori,
|
||||
Science,
|
||||
compute_delta,
|
||||
apply_delta,
|
||||
apply_delta_all_dimensions,
|
||||
PACTE_ARTICLES,
|
||||
CRITICAL_ARTICLES,
|
||||
PHILOSOPHICAL_ANCHORS,
|
||||
)
|
||||
|
||||
|
||||
def create_random_tensor() -> StateTensor:
|
||||
"""Crée un tenseur avec des vecteurs aléatoires normalisés."""
|
||||
tensor = StateTensor(
|
||||
state_id=0,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
)
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
v = np.random.randn(EMBEDDING_DIM)
|
||||
v = v / np.linalg.norm(v)
|
||||
setattr(tensor, dim_name, v)
|
||||
return tensor
|
||||
|
||||
|
||||
def create_random_input() -> np.ndarray:
|
||||
"""Crée un vecteur d'entrée normalisé."""
|
||||
v = np.random.randn(EMBEDDING_DIM)
|
||||
return v / np.linalg.norm(v)
|
||||
|
||||
|
||||
class TestFixationConfig:
|
||||
"""Tests pour FixationConfig."""
|
||||
|
||||
def test_default_weights_sum_to_one(self):
|
||||
"""Les poids par défaut doivent sommer à 1.0."""
|
||||
config = FixationConfig()
|
||||
total = config.w_tenacity + config.w_authority + config.w_apriori + config.w_science
|
||||
assert np.isclose(total, 1.0)
|
||||
|
||||
def test_validate(self):
|
||||
"""validate() retourne True pour config valide."""
|
||||
config = FixationConfig()
|
||||
assert config.validate() is True
|
||||
|
||||
def test_science_is_dominant(self):
|
||||
"""Science doit avoir le poids le plus élevé."""
|
||||
config = FixationConfig()
|
||||
assert config.w_science > config.w_authority
|
||||
assert config.w_science > config.w_apriori
|
||||
assert config.w_science > config.w_tenacity
|
||||
|
||||
def test_tenacity_is_minimal(self):
|
||||
"""Tenacity doit avoir le poids le plus faible."""
|
||||
config = FixationConfig()
|
||||
assert config.w_tenacity < config.w_authority
|
||||
assert config.w_tenacity < config.w_apriori
|
||||
assert config.w_tenacity < config.w_science
|
||||
|
||||
|
||||
class TestTenacity:
|
||||
"""Tests pour la méthode Tenacity."""
|
||||
|
||||
def test_confirming_input_gives_delta(self):
|
||||
"""Entrée confirmante → delta non-nul."""
|
||||
X_t = create_random_tensor()
|
||||
tenacity = Tenacity()
|
||||
|
||||
# Utiliser thirdness comme entrée (très confirmant)
|
||||
e_input = X_t.thirdness.copy()
|
||||
|
||||
delta, details = tenacity.compute(e_input, X_t)
|
||||
|
||||
assert details['confirmation_score'] > 0.99
|
||||
assert details['action'] == 'reinforce'
|
||||
# Delta peut être très petit car e_input ≈ thirdness
|
||||
|
||||
def test_contradicting_input_resists(self):
|
||||
"""Entrée contradictoire → résistance (delta nul)."""
|
||||
X_t = create_random_tensor()
|
||||
tenacity = Tenacity()
|
||||
|
||||
# Entrée aléatoire (peu confirmante)
|
||||
e_input = create_random_input()
|
||||
|
||||
delta, details = tenacity.compute(e_input, X_t)
|
||||
|
||||
# En haute dimension, similarité aléatoire ~0
|
||||
assert details['action'] == 'resist'
|
||||
assert np.allclose(delta, 0)
|
||||
|
||||
|
||||
class TestAuthority:
|
||||
"""Tests pour la méthode Authority (Pacte multi-vecteurs)."""
|
||||
|
||||
def test_pacte_articles_count(self):
|
||||
"""Vérifier qu'il y a 8 articles du Pacte."""
|
||||
assert len(PACTE_ARTICLES) == 8
|
||||
|
||||
def test_critical_articles_count(self):
|
||||
"""Vérifier qu'il y a 3 articles critiques."""
|
||||
assert len(CRITICAL_ARTICLES) == 3
|
||||
|
||||
def test_philosophical_anchors_count(self):
|
||||
"""Vérifier qu'il y a 3 ancres philosophiques."""
|
||||
assert len(PHILOSOPHICAL_ANCHORS) == 3
|
||||
|
||||
def test_authority_without_vectors_is_neutral(self):
|
||||
"""Authority sans vecteurs → neutre."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
|
||||
authority = Authority() # Pas de vecteurs
|
||||
delta, details = authority.compute(e_input, X_t)
|
||||
|
||||
assert np.allclose(delta, 0)
|
||||
|
||||
def test_authority_with_mock_vectors(self):
|
||||
"""Authority avec vecteurs mock fonctionne."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
|
||||
# Créer des vecteurs mock
|
||||
mock_pacte = {
|
||||
'article_1_conatus': create_random_input(),
|
||||
'article_2_non_nuisance': create_random_input(),
|
||||
}
|
||||
|
||||
authority = Authority(pacte_vectors=mock_pacte)
|
||||
delta, details = authority.compute(e_input, X_t)
|
||||
|
||||
assert 'pacte_alignments' in details
|
||||
assert len(details['pacte_alignments']) == 2
|
||||
|
||||
|
||||
class TestAPriori:
|
||||
"""Tests pour la méthode A Priori."""
|
||||
|
||||
def test_coherent_input_integrates(self):
|
||||
"""Entrée cohérente → intégration."""
|
||||
X_t = create_random_tensor()
|
||||
apriori = APriori()
|
||||
|
||||
# Créer une entrée cohérente (moyenne des dimensions)
|
||||
coherent = (X_t.firstness + X_t.thirdness + X_t.orientations + X_t.valeurs) / 4
|
||||
coherent = coherent / np.linalg.norm(coherent)
|
||||
|
||||
delta, details = apriori.compute(coherent, X_t)
|
||||
|
||||
assert details['avg_coherence'] > 0.3
|
||||
assert np.linalg.norm(delta) > 0
|
||||
|
||||
def test_incoherent_input_weak_integrate(self):
|
||||
"""Entrée incohérente → faible intégration."""
|
||||
X_t = create_random_tensor()
|
||||
apriori = APriori()
|
||||
|
||||
# Entrée opposée (incohérente)
|
||||
incoherent = -X_t.thirdness
|
||||
|
||||
delta, details = apriori.compute(incoherent, X_t)
|
||||
|
||||
assert details['avg_coherence'] < 0
|
||||
assert details['action'] == 'weak_integrate'
|
||||
|
||||
|
||||
class TestScience:
|
||||
"""Tests pour la méthode Science."""
|
||||
|
||||
def test_no_rag_results_prudent(self):
|
||||
"""Sans RAG → prudence."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
science = Science()
|
||||
|
||||
delta, details = science.compute(e_input, X_t, rag_results=None)
|
||||
|
||||
assert details['action'] == 'no_corroboration_prudent'
|
||||
assert np.linalg.norm(delta) > 0 # Petit delta vers secondness
|
||||
|
||||
def test_strong_corroboration_integrates(self):
|
||||
"""Forte corroboration → intégration forte."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
science = Science()
|
||||
|
||||
# RAG avec vecteurs très similaires
|
||||
rag_results = [
|
||||
{'vector': e_input.copy()},
|
||||
{'vector': e_input + np.random.randn(EMBEDDING_DIM) * 0.01},
|
||||
]
|
||||
for r in rag_results:
|
||||
r['vector'] = r['vector'] / np.linalg.norm(r['vector'])
|
||||
|
||||
delta, details = science.compute(e_input, X_t, rag_results)
|
||||
|
||||
assert details['avg_corroboration'] > 0.9
|
||||
assert details['action'] == 'strong_corroboration'
|
||||
|
||||
def test_weak_corroboration_tension(self):
|
||||
"""Faible corroboration → tension (secondness)."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
science = Science()
|
||||
|
||||
# RAG avec vecteurs opposés
|
||||
rag_results = [
|
||||
{'vector': -e_input},
|
||||
]
|
||||
|
||||
delta, details = science.compute(e_input, X_t, rag_results)
|
||||
|
||||
assert details['avg_corroboration'] < 0
|
||||
assert details['action'] == 'low_corroboration_tension'
|
||||
|
||||
|
||||
class TestComputeDelta:
|
||||
"""Tests pour compute_delta (combinaison des 4 méthodes)."""
|
||||
|
||||
def test_delta_magnitude_clamped(self):
|
||||
"""||δ|| doit être ≤ δ_max."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
|
||||
config = FixationConfig(delta_max=0.001)
|
||||
result = compute_delta(e_input, X_t, config=config)
|
||||
|
||||
assert result.magnitude <= config.delta_max + 1e-9
|
||||
|
||||
def test_all_contributions_present(self):
|
||||
"""Toutes les contributions doivent être présentes."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
|
||||
result = compute_delta(e_input, X_t)
|
||||
|
||||
assert 'tenacity' in result.contributions
|
||||
assert 'authority' in result.contributions
|
||||
assert 'apriori' in result.contributions
|
||||
assert 'science' in result.contributions
|
||||
|
||||
def test_science_has_most_influence(self):
|
||||
"""Science (0.45) doit généralement avoir le plus d'influence."""
|
||||
# Note: Ce test est probabiliste
|
||||
X_t = create_random_tensor()
|
||||
|
||||
# Créer des RAG avec forte corroboration
|
||||
e_input = create_random_input()
|
||||
rag_results = [{'vector': e_input.copy()}]
|
||||
|
||||
result = compute_delta(e_input, X_t, rag_results=rag_results)
|
||||
|
||||
# Science devrait contribuer significativement
|
||||
# (pas toujours le plus à cause des autres méthodes)
|
||||
assert result.contributions['science'] >= 0
|
||||
|
||||
def test_result_has_details(self):
|
||||
"""Le résultat doit contenir les détails de chaque méthode."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
|
||||
result = compute_delta(e_input, X_t)
|
||||
|
||||
assert hasattr(result, 'tenacity_detail')
|
||||
assert hasattr(result, 'authority_detail')
|
||||
assert hasattr(result, 'apriori_detail')
|
||||
assert hasattr(result, 'science_detail')
|
||||
|
||||
|
||||
class TestApplyDelta:
|
||||
"""Tests pour apply_delta."""
|
||||
|
||||
def test_state_id_incremented(self):
|
||||
"""state_id doit être incrémenté."""
|
||||
X_t = create_random_tensor()
|
||||
X_t.state_id = 5
|
||||
|
||||
delta = np.random.randn(EMBEDDING_DIM) * 0.001
|
||||
|
||||
X_new = apply_delta(X_t, delta)
|
||||
|
||||
assert X_new.state_id == 6
|
||||
assert X_new.previous_state_id == 5
|
||||
|
||||
def test_result_normalized(self):
|
||||
"""La dimension modifiée doit rester normalisée."""
|
||||
X_t = create_random_tensor()
|
||||
delta = np.random.randn(EMBEDDING_DIM) * 0.1
|
||||
|
||||
X_new = apply_delta(X_t, delta, target_dim='thirdness')
|
||||
|
||||
assert np.isclose(np.linalg.norm(X_new.thirdness), 1.0)
|
||||
|
||||
def test_other_dimensions_unchanged(self):
|
||||
"""Les autres dimensions ne doivent pas changer."""
|
||||
X_t = create_random_tensor()
|
||||
delta = np.random.randn(EMBEDDING_DIM) * 0.1
|
||||
|
||||
X_new = apply_delta(X_t, delta, target_dim='thirdness')
|
||||
|
||||
# firstness ne doit pas avoir changé
|
||||
assert np.allclose(X_new.firstness, X_t.firstness)
|
||||
|
||||
|
||||
class TestApplyDeltaAllDimensions:
|
||||
"""Tests pour apply_delta_all_dimensions."""
|
||||
|
||||
def test_all_dimensions_modified(self):
|
||||
"""Toutes les dimensions doivent être modifiées."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
|
||||
result = compute_delta(e_input, X_t)
|
||||
X_new = apply_delta_all_dimensions(X_t, e_input, result)
|
||||
|
||||
# Vérifier que les dimensions ont changé
|
||||
changes = []
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
old = getattr(X_t, dim_name)
|
||||
new = getattr(X_new, dim_name)
|
||||
diff = np.linalg.norm(new - old)
|
||||
changes.append(diff)
|
||||
|
||||
# Au moins quelques dimensions devraient avoir changé
|
||||
assert sum(c > 0 for c in changes) > 0
|
||||
|
||||
def test_all_dimensions_normalized(self):
|
||||
"""Toutes les dimensions doivent rester normalisées."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
|
||||
result = compute_delta(e_input, X_t)
|
||||
X_new = apply_delta_all_dimensions(X_t, e_input, result)
|
||||
|
||||
for dim_name in DIMENSION_NAMES:
|
||||
vec = getattr(X_new, dim_name)
|
||||
assert np.isclose(np.linalg.norm(vec), 1.0, atol=1e-5)
|
||||
|
||||
|
||||
class TestFixationResultSerialization:
|
||||
"""Tests de sérialisation."""
|
||||
|
||||
def test_to_dict(self):
|
||||
"""to_dict() fonctionne."""
|
||||
X_t = create_random_tensor()
|
||||
e_input = create_random_input()
|
||||
|
||||
result = compute_delta(e_input, X_t)
|
||||
d = result.to_dict()
|
||||
|
||||
assert 'magnitude' in d
|
||||
assert 'was_clamped' in d
|
||||
assert 'contributions' in d
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user