Skip to content

Commit ca58919

Browse files
committed
refactor: decouple brain from eq/ via core.types, add standalone DAG enforcement
1 parent d60a76e commit ca58919

17 files changed

Lines changed: 292 additions & 53 deletions

File tree

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ Data flows through four stages in strict forward-only order:
3131
config:
3232
look: neo
3333
theme: neutral
34+
flowchart:
35+
nodeSpacing: 60
36+
rankSpacing: 40
3437
---
3538
flowchart TB
3639
%% Forward-only data flow. Solid edges = pipeline data products.
@@ -60,6 +63,9 @@ flowchart TB
6063
NAV["nav/<br/>JPS/A* · DDA LOS · heightmaps · waypoints"]
6164
UTIL["util/<br/>4-tier logging · forensics"]
6265
66+
%% Invisible edges to force vertical stacking
67+
EQ --- NAV --- UTIL
68+
6369
EQ -.-> BRAIN
6470
EQ -.-> R
6571
NAV -.-> R
@@ -74,10 +80,11 @@ flowchart TB
7480
class PR,US,GP brain
7581
class EQ,NAV,UTIL support
7682
77-
%% 0–1: PR→US, US→GP | 2–4: P→BRAIN, BRAIN→R, R→M | 5–9: support edges
83+
%% 0–1: PR→US, US→GP | 2–3: invisible stacking | 4–6: P→BRAIN, BRAIN→R, R→M | 7–11: support edges
7884
linkStyle 0,1 stroke:#2E5E7E,stroke-width:1.5px
79-
linkStyle 2,3,4 stroke:#2E5E7E,stroke-width:2px
80-
linkStyle 5,6,7,8,9 stroke:#B66A2C,stroke-width:1.5px
85+
linkStyle 2,3 stroke:none
86+
linkStyle 4,5,6 stroke:#2E5E7E,stroke-width:2px
87+
linkStyle 7,8,9,10,11 stroke:#B66A2C,stroke-width:1.5px
8188
```
8289

8390
No module imports upward. The dependency graph is a DAG, and each layer is independently understandable.
@@ -133,6 +140,9 @@ The GOAP planner's cost functions draw directly from this learned data. Rest cos
133140
config:
134141
look: neo
135142
theme: neutral
143+
flowchart:
144+
nodeSpacing: 60
145+
rankSpacing: 40
136146
---
137147
flowchart TD
138148
%% One encounter signal drives four learning systems at distinct cadences.
@@ -149,7 +159,7 @@ flowchart TD
149159
%% SM receives from both CO and PE. Sightings are not combat-exit-driven.
150160
SM["Spatial Memory<br/>50-unit heat map · 4-hour decay<br/><i>cadence: continuous</i>"]
151161
152-
WL["Weight Tuner<br/>15-factor finite-difference gradient · ±20% bounded<br/><i>cadence: per defeat · ~100 to converge</i>"]
162+
WL["Weight Tuner<br/>15-factor gradient descent · ±20% bounded<br/><i>cadence: per defeat · ~100 to converge</i>"]
153163
154164
SS["Session Scorecard<br/>7-dimension performance grading<br/>roam radius · add limit · mana conservation level<br/><i>cadence: every 30 minutes</i>"]
155165

docs/architecture.md

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,57 @@ Data flows through four stages in strict forward-only order:
1919
config:
2020
look: neo
2121
theme: neutral
22+
flowchart:
23+
nodeSpacing: 60
24+
rankSpacing: 40
2225
---
2326
flowchart TB
24-
subgraph PIPELINE["Main Pipeline: data product flow"]
27+
subgraph PIPELINE["Main Pipeline · 10 Hz forward-only data flow"]
2528
direction LR
2629
P["perception/<br/>GameState · SpawnData<br/><i>immutable snapshot, each tick</i>"]
27-
B["brain/<br/>rules · scoring · planner<br/>AgentContext · adaptation"]
28-
R["routines/<br/>state-machine routines<br/>enter · tick · exit"]
30+
31+
subgraph BRAIN["brain/"]
32+
direction TB
33+
PR["Priority Rules<br/><i>safety envelope · 4 modules</i>"]
34+
US["Utility Scoring<br/><i>phase-gated selection</i>"]
35+
GP["GOAP Planner<br/><i>learned cost functions</i>"]
36+
PR --> US --> GP
37+
end
38+
39+
R["routines/<br/>state-machine behaviors<br/>enter · tick · exit"]
2940
M["motor/<br/>action interface"]
3041
31-
P --> B --> R --> M
42+
P -->|"frozen snapshots"| BRAIN
43+
BRAIN -->|"selected routine + params"| R
44+
R -->|"motor commands"| M
3245
end
3346
3447
EQ["eq/<br/>terrain · spells · zones"]
35-
NAV["nav/<br/>A* · heightmaps · waypoints"]
48+
NAV["nav/<br/>JPS/A* · DDA LOS · heightmaps · waypoints"]
3649
UTIL["util/<br/>4-tier logging · forensics"]
3750
38-
EQ -.-> B
51+
%% Invisible edges to force vertical stacking
52+
EQ --- NAV --- UTIL
53+
54+
EQ -.-> BRAIN
3955
EQ -.-> R
4056
NAV -.-> R
41-
UTIL -.-> B
57+
UTIL -.-> BRAIN
4258
UTIL -.-> R
4359
4460
classDef pipeline fill:#EAF4FB,stroke:#2E5E7E,stroke-width:2px,color:#1F3A4A
45-
classDef focal fill:#C8DFF0,stroke:#2E5E7E,stroke-width:2.5px,color:#1F3A4A
61+
classDef brain fill:#C8DFF0,stroke:#2E5E7E,stroke-width:2px,color:#1F3A4A
4662
classDef support fill:#FAF8F5,stroke:#C07A45,stroke-width:1px,color:#1F3A4A
4763
4864
class P,R,M pipeline
49-
class B focal
65+
class PR,US,GP brain
5066
class EQ,NAV,UTIL support
5167
52-
linkStyle 0,1,2 stroke:#2E5E7E,stroke-width:2px
53-
linkStyle 3,4,5,6,7 stroke:#B66A2C,stroke-width:1.5px
68+
%% 0–1: PR→US, US→GP | 2–3: invisible stacking | 4–6: P→BRAIN, BRAIN→R, R→M | 7–11: support edges
69+
linkStyle 0,1 stroke:#2E5E7E,stroke-width:1.5px
70+
linkStyle 2,3 stroke:none
71+
linkStyle 4,5,6 stroke:#2E5E7E,stroke-width:2px
72+
linkStyle 7,8,9,10,11 stroke:#B66A2C,stroke-width:1.5px
5473
```
5574

5675
No module imports upward. This is the primary architectural invariant: it prevents circular imports, keeps layers independently verifiable, and makes the dependency graph a DAG. This pipeline has absorbed every architectural addition since the pipeline decomposition (priority rules, utility scoring, learning loops, and goal-oriented planning) without structural change.
@@ -95,9 +114,12 @@ The brain operates a three-layer decision stack. Each layer adds capability whil
95114
config:
96115
look: neo
97116
theme: neutral
117+
flowchart:
118+
nodeSpacing: 60
119+
rankSpacing: 40
98120
---
99121
flowchart TD
100-
ER["Emergency Rules<br/>FLEE · DEATH_RECOVERY · FEIGN_DEATH<br/><b>structural veto: evaluates first, every tick</b>"]
122+
ER["Emergency Rules<br/>FLEE · FEIGN_DEATH · EVADE<br/><b>structural veto: evaluates first, every tick</b>"]
101123
102124
GP["GOAP Planner<br/>A* on goal-state space · 3-8 steps<br/>learned cost functions · spawn prediction"]
103125
@@ -302,6 +324,9 @@ All learning feeds the GOAP planner's cost model. Learned encounter durations be
302324
config:
303325
look: neo
304326
theme: neutral
327+
flowchart:
328+
nodeSpacing: 60
329+
rankSpacing: 40
305330
---
306331
flowchart TD
307332
CO(["Combat Outcomes<br/>duration · mana spent · HP lost<br/>survival · pet death · extra npcs encountered"])
@@ -313,7 +338,7 @@ flowchart TD
313338
WL["Weight Tuner<br/>15-factor finite-difference gradient<br/><i>cadence: per defeat</i>"]
314339
SS["Session Scorecard<br/>7-dimension grading<br/><i>cadence: every 30 minutes</i>"]
315340
316-
DS["Decision System / AgentContext<br/>GOAP cost model · target scoring<br/>utility weights · rule thresholds"]
341+
DS["Decision System / AgentContext<br/>GOAP cost model · target scoring<br/>utility weights · rule thresholds · spawn positioning"]
317342
318343
CO --> EH
319344
CO -->|"defeat location"| SM

docs/evolution.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ The most important property of this progression is that no stage replaces a prev
349349
config:
350350
look: neo
351351
theme: neutral
352+
flowchart:
353+
nodeSpacing: 60
354+
rankSpacing: 40
352355
---
353356
flowchart TD
354357
%% Additive structural evolution. Each stage layered onto the previous without rework.

scripts/check_import_dag.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
"""Standalone import DAG enforcement -- runs in CI alongside lint and typecheck.
3+
4+
Verifies the architectural invariant: data flows forward through the pipeline
5+
(perception -> brain -> routines -> motor) and no module imports upward.
6+
Brain decision modules must not import environment-specific code (eq/).
7+
8+
Exit code 0 = all constraints satisfied.
9+
Exit code 1 = violations found (printed to stderr).
10+
11+
Usage:
12+
python3 scripts/check_import_dag.py
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import ast
18+
import sys
19+
from pathlib import Path
20+
21+
SRC = Path(__file__).resolve().parent.parent / "src"
22+
23+
24+
def _collect_imports(filepath: Path) -> list[str]:
25+
"""Parse a Python file and return all absolute imported module paths."""
26+
try:
27+
tree = ast.parse(filepath.read_text(encoding="utf-8"), filename=str(filepath))
28+
except SyntaxError:
29+
return []
30+
modules: list[str] = []
31+
for node in ast.walk(tree):
32+
if isinstance(node, ast.Import):
33+
for alias in node.names:
34+
modules.append(alias.name)
35+
elif isinstance(node, ast.ImportFrom):
36+
if node.module and node.level == 0:
37+
modules.append(node.module)
38+
return modules
39+
40+
41+
def _imports_for_package(package: str) -> dict[str, list[str]]:
42+
pkg_dir = SRC / package
43+
if not pkg_dir.is_dir():
44+
return {}
45+
result: dict[str, list[str]] = {}
46+
for py_file in pkg_dir.rglob("*.py"):
47+
rel = str(py_file.relative_to(SRC))
48+
result[rel] = _collect_imports(py_file)
49+
return result
50+
51+
52+
def check_routines_no_decision() -> list[str]:
53+
"""Routines must not import brain.decision, brain.rules, or brain.runner."""
54+
forbidden = {"brain.decision", "brain.rules", "brain.runner"}
55+
violations = []
56+
for filepath, imports in _imports_for_package("routines").items():
57+
for imp in imports:
58+
for prefix in forbidden:
59+
if imp == prefix or imp.startswith(prefix + "."):
60+
if "base.py" in filepath and "brain.rules.survival" in imp:
61+
continue
62+
violations.append(f"{filepath}: imports {imp}")
63+
return violations
64+
65+
66+
def check_motor_no_brain() -> list[str]:
67+
"""Motor must not import brain or routines."""
68+
forbidden_prefixes = {"brain", "routines"}
69+
violations = []
70+
for filepath, imports in _imports_for_package("motor").items():
71+
for imp in imports:
72+
if imp.split(".")[0] in forbidden_prefixes:
73+
violations.append(f"{filepath}: imports {imp}")
74+
return violations
75+
76+
77+
def check_brain_no_runtime() -> list[str]:
78+
"""Brain must not import runtime.server or runtime.orchestrator."""
79+
forbidden = {"runtime.server", "runtime.orchestrator"}
80+
violations = []
81+
for filepath, imports in _imports_for_package("brain").items():
82+
for imp in imports:
83+
for prefix in forbidden:
84+
if imp == prefix or imp.startswith(prefix + "."):
85+
violations.append(f"{filepath}: imports {imp}")
86+
return violations
87+
88+
89+
def check_brain_decision_no_eq() -> list[str]:
90+
"""Brain analytical modules must not import eq/ (environment-specific).
91+
92+
brain/runner/ is the orchestration layer and may import eq/ for
93+
startup configuration. brain/rules/ bridges environment concepts to
94+
decision logic and may import eq.loadout (spell lookups). All other
95+
brain subdirectories (scoring, goap, learning, world) must use
96+
core.types abstractions only.
97+
"""
98+
# Analytical subdirectories that must be environment-free
99+
pure_dirs = {"brain/scoring", "brain/goap", "brain/learning", "brain/world"}
100+
violations = []
101+
for filepath, imports in _imports_for_package("brain").items():
102+
in_pure = any(filepath.startswith(d) for d in pure_dirs)
103+
if not in_pure:
104+
continue
105+
for imp in imports:
106+
if imp.split(".")[0] == "eq":
107+
violations.append(f"{filepath}: imports {imp}")
108+
return violations
109+
110+
111+
def check_perception_no_upper() -> list[str]:
112+
"""Perception must not import brain, routines, or runtime."""
113+
forbidden_prefixes = {"brain", "routines", "runtime"}
114+
allowed_exceptions = {("perception/log_parser.py", "nav.zone_graph")}
115+
violations = []
116+
for filepath, imports in _imports_for_package("perception").items():
117+
for imp in imports:
118+
if imp.split(".")[0] in forbidden_prefixes:
119+
if (filepath, imp) not in allowed_exceptions:
120+
violations.append(f"{filepath}: imports {imp}")
121+
return violations
122+
123+
124+
def main() -> int:
125+
checks = [
126+
("routines -> decision", check_routines_no_decision),
127+
("motor -> brain/routines", check_motor_no_brain),
128+
("brain -> runtime", check_brain_no_runtime),
129+
("brain decision -> eq", check_brain_decision_no_eq),
130+
("perception -> upper layers", check_perception_no_upper),
131+
]
132+
133+
all_violations: list[str] = []
134+
for label, check_fn in checks:
135+
violations = check_fn()
136+
if violations:
137+
all_violations.append(f"\n{label} ({len(violations)} violations):")
138+
for v in violations:
139+
all_violations.append(f" {v}")
140+
141+
if all_violations:
142+
print("Import DAG violations found:", file=sys.stderr)
143+
for line in all_violations:
144+
print(line, file=sys.stderr)
145+
return 1
146+
147+
print(f"Import DAG: all {len(checks)} constraints satisfied.")
148+
return 0
149+
150+
151+
if __name__ == "__main__":
152+
sys.exit(main())

src/brain/learning/encounters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from dataclasses import dataclass
1515
from pathlib import Path
1616

17-
from eq.strings import normalize_mob_name
17+
from core.types import normalize_entity_name as normalize_mob_name
1818

1919
log = logging.getLogger(__name__)
2020

src/brain/learning/zone.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pathlib import Path
1717

1818
from core.types import Disposition
19-
from eq.strings import normalize_mob_name
19+
from core.types import normalize_entity_name as normalize_mob_name
2020

2121
log = logging.getLogger(__name__)
2222

src/brain/rules/combat.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from brain.rules.skip_log import SkipLog
1212
from brain.scoring.curves import linear
1313
from core.features import flags
14-
from core.types import LootMode, PlanType, Point
14+
from core.types import Con, LootMode, PlanType, Point
15+
from core.types import normalize_entity_name as normalize_mob_name
1516
from eq.loadout import Spell, SpellRole, get_spell_by_role
16-
from eq.strings import normalize_mob_name
17-
from perception.combat_eval import Con, con_color
17+
from perception.combat_eval import con_color
1818
from perception.state import GameState
1919
from routines.acquire import AcquireRoutine
2020
from routines.combat import CombatRoutine

src/brain/rules/navigation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ def _should_travel(
5858
# Yield to ACQUIRE if a targetable npc is very close on the path.
5959
# Better to pull properly than walk face-first into threat.
6060
if flags.pull and ctx.pet.alive:
61-
from perception.combat_eval import Con, con_color
61+
from core.types import Con
62+
from perception.combat_eval import con_color
6263

6364
for sp in state.spawns:
6465
if sp.is_npc and sp.hp_current > 0 and sp.owner_id == 0:

src/brain/runner/tick_handlers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
import time
1111
from typing import TYPE_CHECKING
1212

13-
from core.types import PlanType, Point
13+
from core.types import Con, PlanType, Point
14+
from core.types import normalize_entity_name as normalize_mob_name
1415
from eq.loadout import configure_loadout
15-
from eq.strings import normalize_mob_name
16-
from perception.combat_eval import Con, con_color
16+
from perception.combat_eval import con_color
1717
from perception.queries import is_pet
1818
from util.event_schemas import LevelUpEvent
1919
from util.structured_log import log_event

src/brain/scoring/target.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@
1818
from typing import TYPE_CHECKING
1919

2020
from brain.scoring.curves import bell, inverse_logistic, polynomial
21-
from core.types import Point
22-
from eq.strings import normalize_mob_name
21+
from core.types import Con, Disposition, Point
22+
from core.types import normalize_entity_name as normalize_mob_name
2323
from nav.geometry import heading_to
24-
from perception.combat_eval import Con, Disposition
2524
from perception.state import SpawnData
2625
from util.log_tiers import VERBOSE
2726

0 commit comments

Comments
 (0)