-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
197 lines (185 loc) · 8.54 KB
/
Copy pathpyproject.toml
File metadata and controls
197 lines (185 loc) · 8.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
[project]
name = "mayak"
version = "1.0.0"
description = "Reusable AI-friendly FastAPI backend template (Mayak)."
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.13"
dependencies = [
"orjson==3.11.9",
"pydantic==2.13.4",
"fastapi==0.141.1",
# Imported directly by middleware.py and exception_handlers.py. Declared explicitly rather
# than relied on through FastAPI's pin, so a FastAPI bump cannot silently move it.
"starlette==1.4.1",
"uvicorn[standard]==0.52.1",
"sqlalchemy==2.0.51",
"alembic==1.19.0",
"openai==2.53.0",
"python-dotenv==1.2.2",
"pydantic-settings==2.14.2",
# LangChain, and only the two packages the kernel actually imports: langchain-core for the
# message types LLMService speaks, langchain-openai for ChatOpenAI. langgraph was here too and
# was imported by nothing — a vertical that wants graph orchestration adds it deliberately.
"langchain-core==1.5.3",
"langchain-openai==1.4.1",
# Utilities
"tenacity==9.1.4",
"psycopg[binary,pool]==3.3.4",
]
# No [project.optional-dependencies].test group. It duplicated the dev group below line for line,
# and `uv sync` installs dev by default — so `uv sync --extra test` in CI read like the thing that
# brought the test dependencies in while actually changing nothing. One declaration, one meaning.
[dependency-groups]
dev = [
"pytest==9.1.1",
"pytest-asyncio==1.4.0",
"pytest-cov==7.1.0",
"httpx==0.28.1",
"ruff==0.16.1",
"mypy==2.3.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["project"]
[tool.uv]
package = true
[tool.ruff]
target-version = "py313"
line-length = 100
extend-exclude = [
".venv",
"alembic/versions",
]
[tool.ruff.lint]
# Conservative kernel rule set. Mirrors ruff's defaults so the existing kernel
# codebase passes; verticals opt into I / B / UP / SIM / RUF when ready.
select = [
"E", # pycodestyle errors
"F", # pyflakes (unused imports, undefined names)
# A swallowed exception is invisible to every other gate: mypy is happy, tests pass, and the
# failure surfaces later as missing data. bandit does detect it (B110) but scores it LOW, and
# CI runs bandit with -ll, so it never blocked anything.
"S110", # try-except-pass
"S112", # try-except-continue
# Async mistakes that every other gate is blind to. Measured: a blocking sleep, a detached
# task and a blocking subprocess inside async code all passed the narrow workset loop,
# quality-gates and bandit. These four codes catch three of them and report
# zero findings on the current kernel, so nothing existing needs fixing.
#
# Named one by one rather than selecting the whole ASYNC family, so that adding a code is a
# decision someone made rather than a family that grew. ASYNC240 was excluded until 2026-08-24
# for a concrete reason that no longer exists: it fired on
# project/infrastructure/api/middleware.py, where a microsecond Path().stat() sized a
# FileResponse and would have had to become anyio.Path to satisfy the rule. That middleware is
# pure ASGI now and reads Content-Length off the response headers instead, so there is no
# filesystem call on the request path at all and the rule reports nothing. It is selected below
# rather than merely un-excluded, which is what stops the stat coming back.
# Reproduce: `uv run ruff check --select ASYNC240 project tests ai_context ai_query scripts`.
#
# A synchronous httpx client in async code takes two of these plus a validator, because no
# single rule sees both call shapes. Measured with a probe file:
# `with httpx.Client() as c: c.get(...)` -> ASYNC212
# `httpx.Client().post(...)` -> nothing in ruff; caught by
# scripts/validate_runtime_ownership.py
# ASYNC210 covers neither — it only recognises module-level httpx.get / requests.get.
"RUF006", # asyncio.create_task result not stored — the task can be garbage collected
"ASYNC210", # blocking HTTP call inside an async function
"ASYNC212", # blocking method on a synchronous HTTP client inside an async function
"ASYNC221", # blocking subprocess call inside an async function
"ASYNC240", # blocking pathlib call inside an async function
"ASYNC251", # time.sleep inside an async function
]
ignore = [
"E501", # line-too-long — the formatter handles wrapping where it can; long URLs and log messages stay on one line.
]
[tool.mypy]
python_version = "3.13"
disallow_untyped_defs = true
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unreachable = true
check_untyped_defs = true
disallow_incomplete_defs = true
no_implicit_optional = true
strict_equality = true
pretty = true
show_error_codes = true
# tests/functional is a second import root: pytest runs it with `pythonpath = . ../..`, so its
# modules import `settings` and `utils` as top-level names. The three settings below let mypy see
# the suite the way it actually runs — without them, checking it produces seven import-not-found
# errors, and then "Source file found twice under different module names" for every file.
mypy_path = "tests/functional"
explicit_package_bases = true
namespace_packages = true
# TODO: ratchet — measured by the mypy-scope-extension task, 2026-05-20; remeasured 2026-08-24.
# Known debt: scripts/, ai_context/, and ai_query/ use dict[str, object] schemas
# extensively (architecture_rules, file_policy_index, change_map, etc.). Strict mypy surfaced
# ~193 errors across these modules on 2026-05-20; removing this override block and running
# `uv run mypy scripts ai_context ai_query` on 2026-08-24 gives "Found 161 errors in 19 files
# (checked 45 source files)" — reproduce it yourself before trusting either number, the codebase
# moves. Tightening requires migrating the schemas to TypedDict — a multi-week effort. Until then,
# suppress only the systematic codes here; new-style violations (name-defined, import errors,
# unreachable, etc.) remain enforced, so this gate still catches regressions on those.
#
# The type gate is materially weaker here than over the application kernel. Compare
# `find scripts ai_context ai_query -name '*.py' | xargs wc -l`, all of it running under these
# eleven suppressions, against `find project -name '*.py' | xargs wc -l` under full strict mypy
# with none: measured 2026-08-24, the suppressed half is roughly twice the size of the checked
# half. Twice the code, a hole in the check.
#
# No line counts are written here, and that is deliberate. This sentence carried "~6,600" for the
# kernel until 2026-08-24, when the real figure was 6,855 — off by the wrong hundred because it had
# been rounded by hand instead of pasted from the command beside it. It was then corrected to 6,855
# and was wrong again within the hour, at 6,859, because the very edit that corrected it added four
# comment lines under project/. A count of anything a contributor routinely adds to is stale before
# the commit that writes it lands, and no gate rereads a comment. The ratio is what the reader needs
# and the ratio survives; run the two commands for today's numbers.
#
# Ratchet order, mechanical first: no-untyped-def and var-annotated need only annotations, no
# schema change — drop those two from the list first and fix what mypy then reports. attr-defined,
# index and call-overload are the ones that actually need the TypedDict migration; do those last,
# together, since splitting a dict[str, object] into a TypedDict tends to move all three at once.
[[tool.mypy.overrides]]
module = ["scripts.*", "ai_context.*", "ai_query.*"]
disable_error_code = [
"attr-defined",
"index",
"arg-type",
"call-overload",
"assignment",
"operator",
"no-any-return",
"misc",
"var-annotated",
"union-attr",
"no-untyped-def",
]
# backoff is installed only in the functional-tests image (tests/functional/requirements.txt), so
# the development venv that runs mypy has no stubs for it and never will.
[[tool.mypy.overrides]]
module = ["backoff.*"]
ignore_missing_imports = true
[tool.coverage.run]
source = ["project"]
branch = true
omit = [
"alembic/versions/*",
"project/infrastructure/persistence/orm_models.py",
"project/launcher/main.py",
"**/__init__.py",
]
[tool.coverage.report]
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"^\\s*\\.\\.\\.\\s*$",
]