[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "hypermnesia-mcp-viz" version = "3.2.0" description = "Visualization and graph MCP server for Cortex — neural graph, methodology map, workflow graph, and trace UI extracted from the Cortex memory engine" readme = "README.md" license = "MIT" requires-python = ">=3.10" authors = [ { name = "Clement Deust", email = "admin@ai-architect.tools" }, ] keywords = ["mcp", "codex", "gemini-cli", "claude-code", "visualization", "graph", "cortex"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries", ] dependencies = [ "fastmcp>=2.0.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "numpy>=1.24.0", # PostgreSQL read-path drivers. NOT optional: the standalone HTTP # server's only store is MemoryReader, which imports psycopg at module # top (no SQLite fallback), and main() calls _get_store() BEFORE it # binds the socket. Without these the server crashes at startup — before # it can serve even the UI skeleton — which presents to the user as "the # visualization won't open / isn't running". They were previously under # the optional [data] extra, so any install that didn't request the extra # produced a server that could never start. source: reproduced 2026-07-02 # (ModuleNotFoundError: No module named 'psycopg' at memory_read.py:41, # raised from _get_store() in http_standalone.py:main before _bind_server). "psycopg[binary]>=3.1", "psycopg-pool>=3.2", "pgvector>=0.3", ] [project.urls] Homepage = "https://github.com/cdeust/cortex-viz" Repository = "https://github.com/cdeust/cortex-viz" [project.scripts] hypermnesia-mcp-viz = "cortex_viz.__main__:main" [project.optional-dependencies] # CPU-only graph viz path — moved verbatim from the Cortex memory engine # (hypermnesia-mcp, formerly published as neuro-cortex-memory). # Precomputed igraph layout + Datashader tile rendering + Apache Arrow # streaming for client-side hit-test. Enables /api/tile + /api/quadtree + # /api/recompute_layout. No GPU dependency. ~200 MB (numba + llvmlite). viz-tile = [ "igraph>=0.11", "datashader>=0.16.3", "pyarrow>=15.0", "pandas>=2.2", "cachetools>=5.0", # Floor is the fix version, not the API floor: 12.3.0 closes 13 advisories # that all land on the image decoders this extra feeds (PYSEC-2026-2253..2257, # -3451..3454, -3493..3496 — OOB read, TGA/JPEG2000 memory disclosure, PDF # decompression-bomb DoS). Pillow 12.3.0 requires Python >=3.10, which this # project already requires. source: osv.dev, resolved 2026-07-28. "Pillow>=12.3.0", ] # The viz read contract: hypermnesia-mcp-viz reads Cortex's PG store + ~/.claude # artifacts directly. These read-path drivers were PROMOTED to core # ``dependencies`` (see the note there) because the server cannot start # without them. The ``data`` extra remains as an idempotent feature selector # for host manifests that explicitly request the PostgreSQL read path. data = [ "psycopg[binary]>=3.1", "psycopg-pool>=3.2", "pgvector>=0.3", ] # Associative community detection for the brain view. Leiden with the # CPM (Constant Potts Model) objective — resolution-limit-free, so it # does not silently merge small topical communities the way modularity # does past sqrt(2m) edges (Fortunato & Barthelemy, PNAS 2007). Kept a # separate extra (NOT folded into viz-tile's ~200 MB datashader stack) # because it is a small, independent capability; the brain view # degrades to per-kind colouring when it is absent. # source: Traag, V.A., Waltman, L. & van Eck, N.J. (2019), "From # Louvain to Leiden: guaranteeing well-connected communities", # Scientific Reports 9:5233 (Leiden algorithm). # source: Traag, V.A., Van Dooren, P. & Nesterov, Y. (2011), "Narrow # scope for resolution-limit-free community detection", Phys. Rev. E # 84:016114 (CPM objective). community = [ "igraph>=0.11", "leidenalg>=0.10", ] dev = [ "pytest>=8.0.0", "pytest-cov>=5.0.0", "pytest-asyncio>=0.23.0", "pytest-timeout>=2.3.0", # scripts/mutation_check.sh — rules/coding-standards.md §12. "mutmut>=3.6.0", # The lint gate (issue #45). Floor AND ceiling: a ruff minor release # adds rules, so an open range would fail CI on a commit that changed # nothing. Bumping it is a deliberate edit with the new findings fixed # in the same PR — the same contract dependabot already applies to npm. "ruff>=0.15.0,<0.17.0", "jsonschema>=4.23,<5", ] [tool.hatch.build.targets.wheel] packages = ["cortex_viz"] [tool.hatch.build.targets.wheel.force-include] "ui" = "cortex_viz/ui" [tool.ruff] # `scratchpad*` are untracked local working directories; a local `ruff format .` # reformatting a throwaway script is noise, and CI never sees them anyway. extend-exclude = [".claude", "scratchpad", "scratchpad-session"] [tool.ruff.lint] # The gate enforced by the `lint` job in .github/workflows/ci.yml (issue #45). # Ruff's default set is E4/E7/E9 + F — syntax and undefined names only, which # the tree already passed. That is not "maximally strict with warnings" # (OpenSSF `warnings_strict`), so the set below is widened to the rule # families whose findings are actionable on this codebase. `ALL` is not the # target: it reports 6369 here, dominated by docstring-style (D), annotation # completeness (ANN) and assert-in-test (S101) findings that would be answered # with a blanket ignore, which is a preference dressed as a gate. select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort — import ordering "N", # pep8-naming "UP", # pyupgrade — the floor is Python 3.10 "B", # flake8-bugbear — real bug patterns "C4", # flake8-comprehensions "SIM", # flake8-simplify "RUF", # ruff-specific ] ignore = [ # RUF100 (unused-noqa) is only sound when the enabled set IS the full set. # Under a curated `select` it does not find stale suppressions, it finds # suppressions for rules this config does not enable: enabling it deleted # 52 of the 60 it flagged — SLF001 (45), S603 (4), BLE001 (2), S606 (1) — # each a reviewed decision, four of them security ones, that would have # come back as fresh findings the next time anyone ran `--select S`. "RUF100", # SIM105 (try-except-pass -> contextlib.suppress) is withheld by ruff's own # fixer whenever the handler carries a comment, because the rewrite would # discard it. Measured here: 42 sites, 0 fixable even under # `--unsafe-fixes` — i.e. every one of them states its named failure mode # inside the handler, which is what rules/coding-standards.md §3.2 and §9 # require and what a bare `contextlib.suppress(Exception)` would erase. # Enforcing the rule would mean deleting the justifications the gate exists # to protect. "SIM105", ] # The only two confusables this codebase uses, both deliberately and both # more correct than their ASCII lookalike: `×` for tile dimensions (256×256) # and `–` for ranges in prose. Naming them keeps RUF001/RUF002/RUF003 armed # against the homoglyphs the rule actually exists for (Cyrillic а, Greek ο). allowed-confusables = ["×", "–"] [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] timeout = 300 timeout_method = "thread" # An exception raised inside __del__ is UNRAISABLE: the interpreter prints it # and continues, so a leaked resource surfaces as a warning on whichever test # happened to trigger the collection — never as a failure. That is how issue # #113 stayed green for months (an asyncio subprocess transport finalized after # its loop had closed, reported against an unrelated graph-build test). Failing # on it is the only way this class of leak is visible; the suite passes clean, # so the gate costs nothing until something regresses. filterwarnings = ["error::pytest.PytestUnraisableExceptionWarning"] [tool.coverage.run] source = ["cortex_viz"] [tool.coverage.report] show_missing = true fail_under = 80 [tool.mutmut] # Mutation testing (mutmut 3.x) — rules/coding-standards.md §12. Line/branch # coverage proves a line RAN; only a killed mutant proves a test would FAIL if # the code were wrong. The committed scope is the containment primitive, the # smallest module on which every path guard in this repository depends; a # per-change run repoints `only_mutate` and the test selection at the files the # change touched (see scripts/mutation_check.sh). # # `source_paths` must contain the root the sources live under — mutmut only # copies those into `mutants/`, and a source outside them is silently never # mutated, which reports "0 survivors" for a run that mutated nothing. # `use_setproctitle` is off because the macOS default segfaults the runner. source_paths = ["cortex_viz"] only_mutate = ["cortex_viz/shared/path_containment.py"] pytest_add_cli_args_test_selection = ["tests/test_path_containment.py"] use_setproctitle = false