[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "morgan-brain" version = "0.2.0" description = "A project-scoped memory for your AI tools, consolidated into facts by a local model." requires-python = ">=3.12" # An SPDX expression, not a file reference: the text lives at the repository root. license = "Apache-2.0" dependencies = [ "pydantic>=2.9", "pydantic-settings>=2.6", "httpx>=0.27", "openai>=1.0", "structlog>=24.4", "sqlite-vec>=0.1.9", # The MCP server is a primary surface, not an extra. Capped below 2.0: mcp 2.0 swaps # httpx for an incompatible httpx2 package. "mcp>=1.0,<2.0", ] [project.optional-dependencies] dev = [ "pytest>=8.3", "pytest-asyncio>=0.24", "hypothesis>=6.0", # Upper-bounded on purpose: a linter/type-checker minor release adds rules, so an open # range makes CI fail on someone else's release date. Raise the bound deliberately. "ruff>=0.16,<0.17", "mypy>=1.13,<2.0", ] [project.scripts] morgan = "morgan_brain.surfaces.cli.__main__:main" morgan-mcp = "morgan_brain.surfaces.mcp_server:main" [tool.hatch.build.targets.wheel] packages = ["morgan_brain"] [tool.black] line-length = 100 target-version = ["py312"] [tool.ruff] line-length = 100 target-version = "py312" # Prose is not source: the fenced snippets in the design documents and the agent # definitions are illustrations, and reformatting them changes what they illustrate. extend-exclude = ["docs", ".claude"] # The rule set is declared explicitly on purpose. Ruff's *default* selection # changes between releases, so a repo that relies on it has a lint gate that # silently moves under it -- CI turns red on a linter release rather than on a # code change. Pinning the families here (and the ruff version range in the # dev extra) makes the gate a property of this repo, not of whichever ruff a # runner happened to resolve. [tool.ruff.lint] select = [ "E", "W", "F", # pycodestyle + pyflakes "I", # import ordering "UP", # pyupgrade -- keep the 3.12 idioms current "B", "BLE", # bugbear + blind-except "C4", # comprehensions "DTZ", # naive datetimes: a bi-temporal store must not have any "ASYNC", # blocking calls on the async hot path "S", # bandit "SIM", "TRY", "FURB", "RUF", "PLE", # pylint errors only; the refactor/convention sets are noise here ] ignore = [ # FastAPI expresses dependency injection as a call in the default argument. # It is the framework's documented idiom, not an accidental shared mutable. "B008", # Ruff wants every raise to carry a short message and a separate exception # class. Morgan's errors are read by humans in logs; the long-form message # is the useful part. "TRY003", # `class X(str, Enum)` is deliberate, not a 3.12 oversight. StrEnum changes # what `str(member)` returns ("user" instead of "Role.USER"), and these enums # are serialised onto the event bus, into SQLite, and into provider payloads. # Migrating them is a wire-format change that needs its own tested commit, # not a lint autofix. "UP042", # Ambiguous-unicode. The rule guards against homoglyph smuggling; here it # only ever fires on deliberate typography -- en dashes in prose, the real # minus sign in the calibration formulae, x in "4.4x fewer tokens" -- and on # the Cyrillic fixtures that exist precisely to prove the FTS5 tokeniser # handles a non-Latin script. Flagging those is noise, not a finding. "RUF001", "RUF002", "RUF003", ] [tool.ruff.lint.per-file-ignores] # Tests assert, spawn subprocesses with a known argv, and bind throwaway # sockets. Those are the point of the test, not a finding. "tests/**" = ["S101", "S603", "S607", "S104", "S105", "S106", "S108"] [tool.mypy] python_version = "3.12" strict = true # pydantic builds a model's __init__ at runtime; the plugin tells mypy what it builds, including # BaseSettings' own keywords such as _env_file. plugins = ["pydantic.mypy"] # The MCP SDK ships no type information of its own. [[tool.mypy.overrides]] module = ["mcp.*"] ignore_missing_imports = true # The plugin's defaults type every __init__ argument as Any and accept unknown keywords. These # keep a model's constructor checked as strictly as without the plugin. [tool.pydantic-mypy] init_typed = true init_forbid_extra = true [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] [tool.bandit] # Production code only. Bandit reads pytest's idiom as a vulnerability -- `assert` is B101, # the CLI integration tests' subprocess calls are B603/B607, and the "0.0.0.0" literal in # the tests that prove Morgan *refuses* that bind is B104. exclude_dirs = ["tests"]