ghocentric-ghost-engine 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ghocentric_ghost_engine-0.1.1/PKG-INFO +41 -0
- ghocentric_ghost_engine-0.1.1/README.md +33 -0
- ghocentric_ghost_engine-0.1.1/ghocentric_ghost_engine.egg-info/PKG-INFO +41 -0
- ghocentric_ghost_engine-0.1.1/ghocentric_ghost_engine.egg-info/SOURCES.txt +16 -0
- ghocentric_ghost_engine-0.1.1/ghocentric_ghost_engine.egg-info/dependency_links.txt +1 -0
- ghocentric_ghost_engine-0.1.1/ghocentric_ghost_engine.egg-info/top_level.txt +1 -0
- ghocentric_ghost_engine-0.1.1/ghost/__init__.py +43 -0
- ghocentric_ghost_engine-0.1.1/ghost/engine.py +77 -0
- ghocentric_ghost_engine-0.1.1/pyproject.toml +19 -0
- ghocentric_ghost_engine-0.1.1/setup.cfg +4 -0
- ghocentric_ghost_engine-0.1.1/tests/test_environment_isolation.py +23 -0
- ghocentric_ghost_engine-0.1.1/tests/test_explicit_run_control.py +24 -0
- ghocentric_ghost_engine-0.1.1/tests/test_external_input.py +23 -0
- ghocentric_ghost_engine-0.1.1/tests/test_npc_actor_memory.py +41 -0
- ghocentric_ghost_engine-0.1.1/tests/test_npc_emotional_modulation.py +40 -0
- ghocentric_ghost_engine-0.1.1/tests/test_npc_intent_decay.py +36 -0
- ghocentric_ghost_engine-0.1.1/tests/test_npc_intent_processing.py +21 -0
- ghocentric_ghost_engine-0.1.1/tests/test_public_api.py +28 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ghocentric-ghost-engine
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Ghost cognitive engine
|
|
5
|
+
Author: Shane Heckathorn
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# ghocentric-ghost-engine
|
|
10
|
+
|
|
11
|
+
A lightweight internal state reasoning engine (proof-of-architecture).
|
|
12
|
+
|
|
13
|
+
This package contains the core Ghost Engine, designed to experiment with
|
|
14
|
+
stateful reasoning, internal memory, symbolic processing, and emergent behavior.
|
|
15
|
+
It is intentionally minimal and focused on architecture rather than polish.
|
|
16
|
+
|
|
17
|
+
INSTALLATION
|
|
18
|
+
|
|
19
|
+
pip install ghocentric-ghost-engine
|
|
20
|
+
|
|
21
|
+
BASIC USAGE
|
|
22
|
+
|
|
23
|
+
from ghost import engine
|
|
24
|
+
|
|
25
|
+
Note: This is an early-stage engine. APIs may change as the architecture evolves.
|
|
26
|
+
|
|
27
|
+
PROJECT STRUCTURE
|
|
28
|
+
|
|
29
|
+
ghost/ core engine modules
|
|
30
|
+
tests/ internal test cases
|
|
31
|
+
npc_demo.py simple demonstration script
|
|
32
|
+
pyproject.toml build and packaging configuration
|
|
33
|
+
|
|
34
|
+
STATUS
|
|
35
|
+
|
|
36
|
+
This project is currently in v0.1.x and should be considered a
|
|
37
|
+
proof-of-concept. Stability is not yet guaranteed.
|
|
38
|
+
|
|
39
|
+
LICENSE
|
|
40
|
+
|
|
41
|
+
MIT License
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ghocentric-ghost-engine
|
|
2
|
+
|
|
3
|
+
A lightweight internal state reasoning engine (proof-of-architecture).
|
|
4
|
+
|
|
5
|
+
This package contains the core Ghost Engine, designed to experiment with
|
|
6
|
+
stateful reasoning, internal memory, symbolic processing, and emergent behavior.
|
|
7
|
+
It is intentionally minimal and focused on architecture rather than polish.
|
|
8
|
+
|
|
9
|
+
INSTALLATION
|
|
10
|
+
|
|
11
|
+
pip install ghocentric-ghost-engine
|
|
12
|
+
|
|
13
|
+
BASIC USAGE
|
|
14
|
+
|
|
15
|
+
from ghost import engine
|
|
16
|
+
|
|
17
|
+
Note: This is an early-stage engine. APIs may change as the architecture evolves.
|
|
18
|
+
|
|
19
|
+
PROJECT STRUCTURE
|
|
20
|
+
|
|
21
|
+
ghost/ core engine modules
|
|
22
|
+
tests/ internal test cases
|
|
23
|
+
npc_demo.py simple demonstration script
|
|
24
|
+
pyproject.toml build and packaging configuration
|
|
25
|
+
|
|
26
|
+
STATUS
|
|
27
|
+
|
|
28
|
+
This project is currently in v0.1.x and should be considered a
|
|
29
|
+
proof-of-concept. Stability is not yet guaranteed.
|
|
30
|
+
|
|
31
|
+
LICENSE
|
|
32
|
+
|
|
33
|
+
MIT License
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ghocentric-ghost-engine
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Ghost cognitive engine
|
|
5
|
+
Author: Shane Heckathorn
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# ghocentric-ghost-engine
|
|
10
|
+
|
|
11
|
+
A lightweight internal state reasoning engine (proof-of-architecture).
|
|
12
|
+
|
|
13
|
+
This package contains the core Ghost Engine, designed to experiment with
|
|
14
|
+
stateful reasoning, internal memory, symbolic processing, and emergent behavior.
|
|
15
|
+
It is intentionally minimal and focused on architecture rather than polish.
|
|
16
|
+
|
|
17
|
+
INSTALLATION
|
|
18
|
+
|
|
19
|
+
pip install ghocentric-ghost-engine
|
|
20
|
+
|
|
21
|
+
BASIC USAGE
|
|
22
|
+
|
|
23
|
+
from ghost import engine
|
|
24
|
+
|
|
25
|
+
Note: This is an early-stage engine. APIs may change as the architecture evolves.
|
|
26
|
+
|
|
27
|
+
PROJECT STRUCTURE
|
|
28
|
+
|
|
29
|
+
ghost/ core engine modules
|
|
30
|
+
tests/ internal test cases
|
|
31
|
+
npc_demo.py simple demonstration script
|
|
32
|
+
pyproject.toml build and packaging configuration
|
|
33
|
+
|
|
34
|
+
STATUS
|
|
35
|
+
|
|
36
|
+
This project is currently in v0.1.x and should be considered a
|
|
37
|
+
proof-of-concept. Stability is not yet guaranteed.
|
|
38
|
+
|
|
39
|
+
LICENSE
|
|
40
|
+
|
|
41
|
+
MIT License
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ghocentric_ghost_engine.egg-info/PKG-INFO
|
|
4
|
+
ghocentric_ghost_engine.egg-info/SOURCES.txt
|
|
5
|
+
ghocentric_ghost_engine.egg-info/dependency_links.txt
|
|
6
|
+
ghocentric_ghost_engine.egg-info/top_level.txt
|
|
7
|
+
ghost/__init__.py
|
|
8
|
+
ghost/engine.py
|
|
9
|
+
tests/test_environment_isolation.py
|
|
10
|
+
tests/test_explicit_run_control.py
|
|
11
|
+
tests/test_external_input.py
|
|
12
|
+
tests/test_npc_actor_memory.py
|
|
13
|
+
tests/test_npc_emotional_modulation.py
|
|
14
|
+
tests/test_npc_intent_decay.py
|
|
15
|
+
tests/test_npc_intent_processing.py
|
|
16
|
+
tests/test_public_api.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ghost
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ghost public API (pip-facing).
|
|
3
|
+
This file defines the ONLY supported entry points.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .engine import GhostEngine
|
|
7
|
+
|
|
8
|
+
_ENGINE = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def init():
|
|
12
|
+
"""Initialize a new Ghost engine."""
|
|
13
|
+
global _ENGINE
|
|
14
|
+
_ENGINE = GhostEngine()
|
|
15
|
+
return _ENGINE
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def step(input_data=None):
|
|
19
|
+
"""Advance the engine by one cycle."""
|
|
20
|
+
if _ENGINE is None:
|
|
21
|
+
raise RuntimeError("Ghost not initialized. Call ghost.init() first.")
|
|
22
|
+
return _ENGINE.step(input_data)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def state():
|
|
26
|
+
"""Return a mutable view of the current state."""
|
|
27
|
+
if _ENGINE is None:
|
|
28
|
+
raise RuntimeError("Ghost not initialized. Call ghost.init() first.")
|
|
29
|
+
return _ENGINE.state()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def snapshot():
|
|
33
|
+
"""Return an immutable snapshot of the current state."""
|
|
34
|
+
if _ENGINE is None:
|
|
35
|
+
raise RuntimeError("Ghost not initialized. Call ghost.init() first.")
|
|
36
|
+
return _ENGINE.snapshot()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def reset():
|
|
40
|
+
"""Reset the engine."""
|
|
41
|
+
global _ENGINE
|
|
42
|
+
_ENGINE = GhostEngine()
|
|
43
|
+
return _ENGINE
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
class GhostEngine:
|
|
2
|
+
def __init__(self, context: dict | None = None):
|
|
3
|
+
"""
|
|
4
|
+
Core Ghost engine.
|
|
5
|
+
Monolithic, self-contained v0.1 implementation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Initialize context if not provided
|
|
9
|
+
if context is None:
|
|
10
|
+
context = {}
|
|
11
|
+
|
|
12
|
+
self._ctx = context
|
|
13
|
+
|
|
14
|
+
# Required baseline state
|
|
15
|
+
self._ctx.setdefault("cycles", 0)
|
|
16
|
+
self._ctx.setdefault("input", None)
|
|
17
|
+
self._ctx.setdefault("last_step", None)
|
|
18
|
+
|
|
19
|
+
def step(self, input_data=None):
|
|
20
|
+
"""
|
|
21
|
+
Advance the Ghost engine by one cycle.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
ctx = self._ctx
|
|
25
|
+
ctx["cycles"] += 1
|
|
26
|
+
|
|
27
|
+
# Ensure npc bucket exists
|
|
28
|
+
npc = ctx.setdefault("npc", {})
|
|
29
|
+
npc.setdefault("threat_level", 0.0)
|
|
30
|
+
npc.setdefault("last_intent", None)
|
|
31
|
+
npc.setdefault("actors", {})
|
|
32
|
+
|
|
33
|
+
# --- PROCESS INPUT FIRST ---
|
|
34
|
+
received_threat = False
|
|
35
|
+
|
|
36
|
+
if input_data:
|
|
37
|
+
ctx["raw_input"] = input_data
|
|
38
|
+
ctx["input"] = input_data
|
|
39
|
+
|
|
40
|
+
if input_data.get("source") == "npc_engine":
|
|
41
|
+
intent = input_data.get("intent")
|
|
42
|
+
intensity = float(input_data.get("intensity", 0.0))
|
|
43
|
+
actor = input_data.get("actor", "unknown")
|
|
44
|
+
|
|
45
|
+
if intent == "threat":
|
|
46
|
+
received_threat = True
|
|
47
|
+
|
|
48
|
+
# Actor memory
|
|
49
|
+
actor_bucket = npc["actors"].setdefault(
|
|
50
|
+
actor,
|
|
51
|
+
{"threat_count": 0}
|
|
52
|
+
)
|
|
53
|
+
actor_bucket["threat_count"] += 1
|
|
54
|
+
|
|
55
|
+
# Mood modulation
|
|
56
|
+
mood = ctx.get("state", {}).get("mood", 0.5)
|
|
57
|
+
mood_multiplier = 0.5 + mood # 0.5 → 1.5
|
|
58
|
+
|
|
59
|
+
threat_gain = intensity * mood_multiplier
|
|
60
|
+
npc["threat_level"] += threat_gain
|
|
61
|
+
npc["last_intent"] = "threat"
|
|
62
|
+
|
|
63
|
+
# --- DECAY ONLY IF NO NEW THREAT ---
|
|
64
|
+
if not received_threat:
|
|
65
|
+
DECAY_RATE = 0.15
|
|
66
|
+
npc["threat_level"] = max(0.0, npc["threat_level"] - DECAY_RATE)
|
|
67
|
+
|
|
68
|
+
return ctx
|
|
69
|
+
|
|
70
|
+
def state(self):
|
|
71
|
+
"""Return the live engine state (mutable)."""
|
|
72
|
+
return self._ctx
|
|
73
|
+
|
|
74
|
+
def snapshot(self):
|
|
75
|
+
"""Return an immutable snapshot of engine state."""
|
|
76
|
+
import copy
|
|
77
|
+
return copy.deepcopy(self._ctx)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ghocentric-ghost-engine"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Ghost cognitive engine"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name="Shane Heckathorn" }
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
dependencies = []
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["ghost*"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# tests/test_environment_isolation.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import copy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_no_environment_side_effects():
|
|
9
|
+
"""
|
|
10
|
+
Importing and initializing Ghost must not mutate
|
|
11
|
+
global environment variables or sys.path.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
env_before = dict(os.environ)
|
|
15
|
+
path_before = list(sys.path)
|
|
16
|
+
|
|
17
|
+
import ghost
|
|
18
|
+
|
|
19
|
+
# Explicit init is allowed, but must be clean
|
|
20
|
+
ghost.init()
|
|
21
|
+
|
|
22
|
+
assert dict(os.environ) == env_before
|
|
23
|
+
assert list(sys.path) == path_before
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_no_implicit_execution():
|
|
6
|
+
"""
|
|
7
|
+
Ghost must not mutate state unless explicitly stepped.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ghost.engine import GhostEngine
|
|
11
|
+
|
|
12
|
+
engine = GhostEngine()
|
|
13
|
+
|
|
14
|
+
# Capture initial engine state
|
|
15
|
+
initial_state = engine.state()
|
|
16
|
+
|
|
17
|
+
# Wait to catch any background mutation
|
|
18
|
+
time.sleep(0.2)
|
|
19
|
+
|
|
20
|
+
# Capture state again without stepping
|
|
21
|
+
later_state = engine.state()
|
|
22
|
+
|
|
23
|
+
# State must be identical
|
|
24
|
+
assert later_state == initial_state
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
def test_external_input_passthrough():
|
|
2
|
+
"""
|
|
3
|
+
External input passed to step() must be recorded in state.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ghost.engine import GhostEngine
|
|
7
|
+
|
|
8
|
+
engine = GhostEngine()
|
|
9
|
+
|
|
10
|
+
event = {
|
|
11
|
+
"source": "npc_engine",
|
|
12
|
+
"intent": "threat",
|
|
13
|
+
"actor": "player",
|
|
14
|
+
"intensity": 0.5,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
engine.step(event)
|
|
18
|
+
state = engine.state()
|
|
19
|
+
|
|
20
|
+
assert "input" in state
|
|
21
|
+
assert state["input"] is not None
|
|
22
|
+
assert isinstance(state["input"], dict)
|
|
23
|
+
assert state["input"] == event
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
def test_actor_memory_tracks_threat_counts():
|
|
2
|
+
"""
|
|
3
|
+
Ghost must track per-actor threat counts independently.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ghost.engine import GhostEngine
|
|
7
|
+
|
|
8
|
+
engine = GhostEngine()
|
|
9
|
+
|
|
10
|
+
engine.step({
|
|
11
|
+
"source": "npc_engine",
|
|
12
|
+
"actor": "player",
|
|
13
|
+
"intent": "threat",
|
|
14
|
+
"intensity": 0.7,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
engine.step({
|
|
18
|
+
"source": "npc_engine",
|
|
19
|
+
"actor": "player",
|
|
20
|
+
"intent": "threat",
|
|
21
|
+
"intensity": 0.4,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
engine.step({
|
|
25
|
+
"source": "npc_engine",
|
|
26
|
+
"actor": "guard",
|
|
27
|
+
"intent": "threat",
|
|
28
|
+
"intensity": 0.9,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
state = engine.state()
|
|
32
|
+
|
|
33
|
+
assert "npc" in state
|
|
34
|
+
assert "actors" in state["npc"]
|
|
35
|
+
|
|
36
|
+
# Per-actor memory exists
|
|
37
|
+
assert "player" in state["npc"]["actors"]
|
|
38
|
+
assert "guard" in state["npc"]["actors"]
|
|
39
|
+
|
|
40
|
+
assert state["npc"]["actors"]["player"]["threat_count"] == 2
|
|
41
|
+
assert state["npc"]["actors"]["guard"]["threat_count"] == 1
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
def test_emotional_modulation_affects_threat_gain():
|
|
2
|
+
"""
|
|
3
|
+
Same threat input, different mood → different threat accumulation.
|
|
4
|
+
Higher mood (anxious) should amplify threat gain.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ghost.engine import GhostEngine
|
|
8
|
+
|
|
9
|
+
# ---- Calm mood engine ----
|
|
10
|
+
calm_engine = GhostEngine()
|
|
11
|
+
|
|
12
|
+
# Inject calm mood into engine state
|
|
13
|
+
calm_state = calm_engine.state()
|
|
14
|
+
calm_state.setdefault("state", {})
|
|
15
|
+
calm_state["state"]["mood"] = 0.1 # calm / low arousal
|
|
16
|
+
|
|
17
|
+
calm_engine.step({
|
|
18
|
+
"source": "npc_engine",
|
|
19
|
+
"intent": "threat",
|
|
20
|
+
"intensity": 1.0,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
calm_threat = calm_engine.state()["npc"]["threat_level"]
|
|
24
|
+
|
|
25
|
+
# ---- Anxious mood engine ----
|
|
26
|
+
anxious_engine = GhostEngine()
|
|
27
|
+
|
|
28
|
+
anxious_state = anxious_engine.state()
|
|
29
|
+
anxious_state.setdefault("state", {})
|
|
30
|
+
anxious_state["state"]["mood"] = 0.9 # anxious / high arousal
|
|
31
|
+
|
|
32
|
+
anxious_engine.step({
|
|
33
|
+
"source": "npc_engine",
|
|
34
|
+
"intent": "threat",
|
|
35
|
+
"intensity": 1.0,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
anxious_threat = anxious_engine.state()["npc"]["threat_level"]
|
|
39
|
+
|
|
40
|
+
assert anxious_threat > calm_threat
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
def test_threat_decays_without_new_input():
|
|
2
|
+
"""
|
|
3
|
+
Threat level should decay over time if no new threat input is received.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ghost.engine import GhostEngine
|
|
7
|
+
|
|
8
|
+
engine = GhostEngine()
|
|
9
|
+
|
|
10
|
+
# --- Spike threat ---
|
|
11
|
+
engine.step({
|
|
12
|
+
"source": "npc_engine",
|
|
13
|
+
"intent": "threat",
|
|
14
|
+
"intensity": 1.0,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
s1 = engine.state()
|
|
18
|
+
threat_after_spike = s1["npc"]["threat_level"]
|
|
19
|
+
|
|
20
|
+
# --- Advance cycles with no input (decay only) ---
|
|
21
|
+
engine.step()
|
|
22
|
+
engine.step()
|
|
23
|
+
engine.step()
|
|
24
|
+
|
|
25
|
+
s2 = engine.state()
|
|
26
|
+
threat_after_decay = s2["npc"]["threat_level"]
|
|
27
|
+
|
|
28
|
+
# --- Contracts ---
|
|
29
|
+
assert "npc" in s1
|
|
30
|
+
assert "npc" in s2
|
|
31
|
+
|
|
32
|
+
assert "threat_level" in s1["npc"]
|
|
33
|
+
assert "threat_level" in s2["npc"]
|
|
34
|
+
|
|
35
|
+
# Core invariant: decay must reduce threat without new input
|
|
36
|
+
assert threat_after_decay < threat_after_spike
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
def test_npc_intent_is_classified():
|
|
2
|
+
"""
|
|
3
|
+
NPC intent input should be classified and reflected in engine state.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ghost.engine import GhostEngine
|
|
7
|
+
|
|
8
|
+
engine = GhostEngine()
|
|
9
|
+
|
|
10
|
+
engine.step({
|
|
11
|
+
"source": "npc_engine",
|
|
12
|
+
"intent": "threat",
|
|
13
|
+
"intensity": 0.8
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
state = engine.state()
|
|
17
|
+
|
|
18
|
+
# --- Contracts ---
|
|
19
|
+
assert "npc" in state
|
|
20
|
+
assert state["npc"]["last_intent"] == "threat"
|
|
21
|
+
assert state["npc"]["threat_level"] > 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
def test_public_api_surface():
|
|
2
|
+
"""
|
|
3
|
+
Ghost exposes a stable, minimal public API.
|
|
4
|
+
This is the pip-facing contract.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ghost
|
|
8
|
+
|
|
9
|
+
assert hasattr(ghost, "init")
|
|
10
|
+
assert hasattr(ghost, "step")
|
|
11
|
+
assert hasattr(ghost, "reset")
|
|
12
|
+
assert hasattr(ghost, "state")
|
|
13
|
+
assert hasattr(ghost, "snapshot")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_public_api_behavior():
|
|
17
|
+
"""
|
|
18
|
+
Public API functions must work end-to-end without importing internals.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import ghost
|
|
22
|
+
|
|
23
|
+
ghost.init()
|
|
24
|
+
ghost.step()
|
|
25
|
+
state = ghost.state()
|
|
26
|
+
|
|
27
|
+
assert isinstance(state, dict)
|
|
28
|
+
assert "cycles" in state
|