diff --git a/src/idea194_attractorforge_verified_attractor/compiler.py b/src/idea194_attractorforge_verified_attractor/compiler.py index fd4e827..e301629 100644 --- a/src/idea194_attractorforge_verified_attractor/compiler.py +++ b/src/idea194_attractorforge_verified_attractor/compiler.py @@ -67,6 +67,16 @@ def _merkle_root(items: list[str]) -> str: return level[0] +def _fingerprint(spec: AttractorSpec, gain: tuple[float, ...], controller_hash: str) -> str: + """Deterministic fingerprint for quick admission/indexing. + + Combines a few stable, low-entropy fields and hashes them. This is + intentionally small and deterministic so it can be exchanged over DTN. + """ + payload = _json({"spec_id": spec.spec_id, "kind": spec.kind, "gain_len": len(gain), "ctrl": controller_hash}) + return _hash_text(payload) + + class AttractorCompiler: def compile(self, spec: AttractorSpec) -> CompilationResult: spec = spec.normalized() @@ -80,6 +90,7 @@ class AttractorCompiler: estimated_basin_radius=spec.basin_radius, convergence_rate_bound=spec.convergence_rate, controller_hash=_hash_text(_json({"gain": gain, "target": spec.target_state, "kind": spec.kind})), + attractor_fingerprint=_fingerprint(spec, gain, _hash_text(_json({"gain": gain, "target": spec.target_state, "kind": spec.kind}))), dt=spec.dt, ) return CompilationResult(controller=controller, plan_cert=plan_cert, plan_delta=plan_delta, basin_sketch=basin_sketch) diff --git a/src/idea194_attractorforge_verified_attractor/models.py b/src/idea194_attractorforge_verified_attractor/models.py index 909d7d4..f2ae756 100644 --- a/src/idea194_attractorforge_verified_attractor/models.py +++ b/src/idea194_attractorforge_verified_attractor/models.py @@ -65,6 +65,7 @@ class BasinSketch: estimated_basin_radius: float convergence_rate_bound: float controller_hash: str + attractor_fingerprint: str dt: float diff --git a/tests/test_compiler.py b/tests/test_compiler.py index 7b13a73..b5a2208 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -23,6 +23,9 @@ def test_motion_template_compiles_and_replays() -> None: assert replay.matched assert result.plan_cert.spec_id == spec.spec_id assert result.basin_sketch.controller_hash + # fingerprint should be a deterministic hex sha256 (64 chars) + assert isinstance(result.basin_sketch.attractor_fingerprint, str) + assert len(result.basin_sketch.attractor_fingerprint) == 64 assert abs(replay.final_state[0]) < 0.8 assert abs(replay.final_state[1]) < 0.8