[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "selvedge" version = "0.3.14" description = "Decision provenance for AI-coded codebases: the why, and what was already tried and rejected." readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [ { name = "Mason Delan", email = "masondelan@gmail.com" }, ] keywords = ["mcp", "ai", "changelog", "codebase", "tracking", "devtools"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Version Control", ] dependencies = [ # Upper bound is load-bearing, not caution. mcp 2.0.0 (2026-07-28) removed # `mcp.server.fastmcp` and renamed every ToolAnnotations field from # camelCase to snake_case, so an unbounded `mcp>=1.0.0` resolves to a # release where `selvedge-server` fails at import. Lift this only together # with the 2.x migration. "mcp>=1.0.0,<2.0.0", # `tomllib` is stdlib from 3.11. On the declared 3.10 floor a PRODUCTION # install had no TOML parser at all, so `.selvedge/config.toml` was # silently ignored — including a user narrowing the hook's watch globs, # who then kept getting blocked on files they had explicitly opted out of. # CI could not see this: the 3.10 job installs [dev], and pytest drags # tomli in transitively. "tomli>=2.0.0; python_version < '3.11'", "click>=8.0.0", "rich>=13.0.0", ] [project.optional-dependencies] # Optional semantic recall (v0.3.9.1): local static embeddings for # `selvedge index` + `prior_attempts --fuzzy`. model2vec over # sentence-transformers on purpose — no torch stack, ~30 MB model, # sub-second cold start; size and cold-start matter more than embedding # quality for one-sentence reasoning strings. Core never imports this. semantic = [ "model2vec>=0.3.0", ] dev = [ "pytest>=9.1.1", "pytest-asyncio>=1.4.0", "pytest-cov>=7.1.0", "ruff>=0.16.2", "mypy>=2.3.0", "hatchling", # Dev-only (NOT a runtime dependency): gives scripts/schema_tax.py the real # tokenizer so the CI MCP-footprint gate matches the published figure. "tiktoken>=0.13.0", ] [project.scripts] selvedge = "selvedge.cli:cli" selvedge-server = "selvedge.server:main" selvedge-hook = "selvedge.hooks.cli:main" [project.urls] Homepage = "https://selvedge.sh" Documentation = "https://selvedge.sh" Repository = "https://github.com/masondelan/selvedge" Issues = "https://github.com/masondelan/selvedge/issues" Changelog = "https://github.com/masondelan/selvedge/blob/main/CHANGELOG.md" [tool.hatch.build.targets.wheel] packages = ["selvedge"] [tool.hatch.build.targets.sdist] # Ship only what an end user of the PyPI package needs. The previous blanket # `docs/` + `CLAUDE.md` over-shipped the website HTML, brand PNGs (~1.5 MB), # sitemap/robots, marketing-templates, and the agent-instructions file to PyPI. # Website artifacts belong to the website repo, not the wheel/sdist. # Patterns are ANCHORED with a leading slash. Hatchling reads these with # gitignore semantics, where an unanchored pattern matches at any depth — so # bare `selvedge/` also matched `features/src/selvedge/` and bare `README.md` # matched every README in the tree, which is how six unlisted files were # already shipping to PyPI. The enumeration below only means what its comment # claims while every entry stays anchored. include = [ "/selvedge/", "/README.md", "/LICENSE", "/pyproject.toml", # End-user docs only — explicitly enumerated so new internal docs can't # silently ride along into the package. "/docs/getting-started.md", "/docs/coding-agents.md", "/docs/telemetry.md", ] exclude = [ ".selvedge/", "*.db", "*.db-journal", "*.db-shm", "*.db-wal", ] [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] [tool.coverage.run] branch = true source = ["selvedge"] # `source` already scopes coverage to the package, so nothing needs omitting. # (This previously listed selvedge/_version.py, which does not exist — the # entry was vestigial and no exemptions were actually in effect.) omit = [] [tool.coverage.report] # CI gate. test.yml runs plain `pytest --cov=selvedge`, so this value is the # only threshold in play — there is no --cov-fail-under flag to keep in step. fail_under = 85 show_missing = true skip_covered = false exclude_lines = [ "pragma: no cover", "if __name__ == .__main__.:", "raise NotImplementedError", "if TYPE_CHECKING:", ] [tool.ruff] target-version = "py310" line-length = 100 extend-exclude = ["build", "dist", ".selvedge"] [tool.ruff.lint] # Conservative starter set — extend later, but everything here must stay green. select = [ "E", # pycodestyle errors "F", # pyflakes "W", # pycodestyle warnings "I", # isort (import order) "B", # flake8-bugbear (likely-buggy patterns) "UP", # pyupgrade (modern syntax) ] ignore = [ "E501", # line length — handled by formatter; existing code has long docstrings "B008", # function calls in argument defaults — Click idiom ] [tool.ruff.lint.per-file-ignores] "tests/*" = ["B011"] # assert False is fine in tests [tool.mypy] python_version = "3.10" files = ["selvedge"] # Pragmatic strict mode — full --strict is too noisy on a codebase that # uses dict-of-dict storage shapes. Enable the high-value flags explicitly. strict_equality = true warn_redundant_casts = true warn_unused_ignores = true warn_unreachable = true no_implicit_optional = true check_untyped_defs = true disallow_untyped_decorators = true # Ratchet step 1: require signatures everywhere except the Click command # callbacks in `selvedge.cli`. Measured — this costs zero errors today, and it # stops new untyped functions landing in the 26 modules that are already clean. # Step 2 is annotating cli.py's ~31 callbacks, which is mechanical (every one # is `no-untyped-def`, none needs inference) and belongs in its own change. [[tool.mypy.overrides]] module = "selvedge.*" disallow_untyped_defs = true disallow_incomplete_defs = true [[tool.mypy.overrides]] module = "selvedge.cli" disallow_untyped_defs = false disallow_incomplete_defs = false [[tool.mypy.overrides]] module = "mcp.*" ignore_missing_imports = true [[tool.mypy.overrides]] # tomllib is stdlib only on 3.11+; mypy runs with python_version = "3.10" # where neither it nor the optional tomli backport resolves. The hook's # import dance handles absence at runtime. model2vec is the semantic # extra's backend — optional by design, never installed in the lint env. module = ["tomllib", "tomli", "model2vec"] ignore_missing_imports = true