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>
384 lines
12 KiB
Python
384 lines
12 KiB
Python
#!/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"])
|