-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathpyproject.toml
More file actions
478 lines (460 loc) · 24.1 KB
/
Copy pathpyproject.toml
File metadata and controls
478 lines (460 loc) · 24.1 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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
[build-system]
requires = ["setuptools==82.0.1"]
build-backend = "setuptools.build_meta"
[project]
name = "graphlink"
dynamic = ["version"]
description = "A local-first, graph-based AI workspace for branching reasoning, tool use, and multi-provider workflows."
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
dependencies = [
"anthropic",
"beautifulsoup4",
# Qt-removal complete (doc/QT_REMOVAL_PLAN.md, R7.6b): Python backend +
# native pywebview shell. Do not add Qt dependencies - tests/
# test_no_qt_anywhere.py fails the build if any manifest declares one.
"fastapi",
"uvicorn",
# uvicorn has no built-in WebSocket implementation; without one every
# ws:// upgrade is rejected and this app - whose entire client/server
# contract is backend/app.py's /ws endpoint - simply does not work.
# See requirements.in's own comment for how this shipped unnoticed and
# which gate finally caught it.
"websockets",
"pywebview",
"ddgs",
"matplotlib",
"mutagen",
"ollama",
"openai>=1.0.0",
"Pillow",
"pypdf",
"python-docx",
"requests",
"tiktoken",
# ADR-014 stage 14.1: backend/plugin_sdk.py parses plugin.toml via
# stdlib tomllib, which is only available from Python 3.11 onward -
# this repo's own requires-python floor is 3.10. tomli is the
# conditional fallback for that gap (CI/dev both already run 3.12+, so
# this branch is dead in practice today, but is real correctness for
# the declared support floor, not speculative).
'tomli; python_version < "3.11"',
]
# llama-cpp-python is lazily imported (api_provider._load_llama_cpp_class) only when
# Llama.cpp local mode is actually used - Ollama is the built-in local path, so it is an
# opt-in extra rather than a hard dependency: it is a heavy native build with no wheels
# for newer Python versions, so requiring it would break installs for most users.
[project.optional-dependencies]
llamacpp = ["llama-cpp-python"]
# ADR-015 stage 15.7: pytest/pytest-cov/ruff/mypy are dev-only tooling, not
# a runtime dependency of the shipped app - deliberately NOT in requirements.in
# (the prod lockfile). CI installs them ad hoc (see .github/workflows/ci.yml),
# same pattern ruff already used before this stage; this extra exists so a
# contributor gets the identical set via one `pip install -e .[dev]`.
dev = ["pytest", "pytest-cov", "pytest-xdist", "ruff", "mypy", "hypothesis", "mutmut"]
[tool.pytest.ini_options]
# ADR-015 stage 15.3: --strict-markers rejects a typo'd/unregistered
# @pytest.mark.X outright rather than silently no-op'ing it; --strict-config
# does the same for an unrecognized ini option. filterwarnings promotes the
# three "an API is going away" classes to hard failures - the actual signal
# a pytest warnings policy exists to catch (a dependency bump silently
# breaking at the next major version) - rather than a blanket "error" on
# every warning category. A blanket policy also promotes ResourceWarning,
# and this codebase has a real, PRE-EXISTING, wide surface of those
# (subprocess Popen pipes across pycoder REPLs and out-of-process plugin
# workers, tempdirs, sockets - closed by GC rather than explicitly, each a
# distinct message shape pytest's unraisable-exception hook wraps
# differently). Chasing that surface to zero is a real, separate body of
# work, not "add a filterwarnings policy" - see ADR-015 §2's own "no
# big-bang cleanup" instruction for ruff, which applies equally here.
# -n auto --dist worksteal: the suite is parallel-safe (every test isolates
# into tmp_path/monkeypatch) but ran single-process for its whole life -
# 10+ minutes of wall clock on a 12-core machine. Parallel: ~80s locally,
# and CI's 4-core runners get the same multiplier. worksteal (not the
# default load) because the tail is a handful of 10s+ real-venv sandbox
# tests that would otherwise strand one worker long after the rest drain.
# Debugging escape hatch: append -n0 to run serial (breakpoints, -s, or
# the perf suite - CI passes -n0 there explicitly, since its timing
# assertions must not share a loaded machine with 11 sibling workers).
addopts = "--strict-markers --strict-config -n auto --dist worksteal"
# --strict-markers means every marker has to be declared here or the run
# fails, which is the point: this one is load-bearing, not decorative.
#
# posix_permissions marks the 9 tests that assert restrictive file mode bits
# (0600) on chats.db, the knowledge store, DB backups and session.dat - the
# secrets-at-rest file. All 9 begin with `if sys.platform == "win32":
# pytest.skip("chmod is a no-op on Windows")`, and python-tests is the ONLY
# job in ci.yml that runs pytest at all, pinned to windows-latest for DPAPI.
# So until the posix-permissions job was added they skipped in CI and ran
# nowhere else: nine assertions about the permissions of a file holding API
# keys, executing on no machine anyone checked. The marker is what lets that
# job select exactly them on ubuntu, without dragging in the ~18 other tests
# in the same files that genuinely require Windows.
markers = [
"posix_permissions: asserts POSIX file mode bits; skipped on Windows, run by the posix-permissions CI job",
]
filterwarnings = [
"error::DeprecationWarning",
"error::PendingDeprecationWarning",
"error::FutureWarning",
# stdlib's own asyncio.get_event_loop_policy() deprecation (3.14 slates
# removal) - backend/tests/test_builder.py's one call is a documented
# no-op probe (see that test's own comment), not a real usage to migrate.
"ignore:'asyncio.get_event_loop_policy' is deprecated.*:DeprecationWarning",
# 2026-09-04 audit: the three error:: lines above cannot see a starlette
# deprecation at all. StarletteDeprecationWarning subclasses UserWarning,
# not DeprecationWarning - so the one policy written to catch "a
# dependency bump silently breaking at the next major version" was
# structurally blind to the dependency that has already announced one.
# Named by its defining module (starlette.exceptions) rather than
# starlette.testclient, which re-exports it but emits the warning below
# on import - pytest resolves this path while loading the config.
"error::starlette.exceptions.StarletteDeprecationWarning",
# The one starlette deprecation live today, recorded rather than
# silently passed. Every run emits it; the whole TestClient surface
# depends on it (test_ws_origin, test_http_trust_boundary,
# test_security_invariants, test_auth, test_session_lifecycle and four
# more). Clearing it means moving the test client onto httpx2, which is
# a hash-locked dependency change and its own reviewed piece of work,
# not a warnings-policy edit. Until then the error:: line above means a
# SECOND starlette deprecation fails the build instead of hiding behind
# this one.
"ignore:Using `httpx` with `starlette.testclient` is deprecated.*:starlette.exceptions.StarletteDeprecationWarning",
]
# Hang diagnosis. pytest's built-in faulthandler dumps a full traceback of
# every thread if any single test runs longer than this, naming the exact
# test and line it is stuck on. Zero new dependencies (stdlib faulthandler,
# already wired up by pytest itself) - deliberately chosen over adding
# pytest-timeout, since requirements.txt is a hash-pinned pip-compile
# lockfile and this gets the diagnosis without touching the dependency set.
#
# Added after a real incident: an approval-gate regression (deliberately
# injected during a mutation-testing audit) made one async test park
# forever on an asyncio Future that nothing would ever resolve. The suite
# had no per-test bound, so it hung with zero output - indistinguishable
# from tooling flakiness, and it burned significant time to diagnose by
# hand. It also MASKED the real signal: 5 other tests correctly caught
# that same regression, but their clean failures were never reported
# because the run never finished.
#
# 60s is far above any legitimate test here (the entire suite runs in
# ~50s; the slowest single test is a few seconds) and far below CI's own
# 20-minute job timeout, so a real hang now self-identifies long before
# the job is killed. Note this DUMPS and continues rather than failing the
# test outright - the goal is turning a silent hang into a located one.
faulthandler_timeout = 60
# ADR-015 stage 15.4: measures backend/ + graphlink_plugins/ (the app's real
# domain logic - contracts/ is build-time codegen, already gated by its own
# tests/test_generated_artifacts.py drift checks, not by runtime coverage).
# `omit` excludes the test files themselves - without it, their own
# near-100%-by-definition self-coverage dilutes into the total and quietly
# inflates the number (measured 2026-08-11: ~97% WITH test files counted in
# the source set vs the real ~90.4% production-code figure below once
# they're excluded - a mistake caught by actually reading the per-file
# breakdown, not by trusting the headline percentage). Wired into CI's
# pytest step (.github/workflows/ci.yml), deliberately NOT via this file's
# own addopts - baking --cov into addopts would gate every local targeted
# single-file run (`pytest backend/tests/test_foo.py`) against the
# WHOLE-suite floor, which is meaningless for a file that only exercises a
# slice of the codebase. fail_under=85 sits with real margin below the
# ~90.4% measured at landing (coverage 7.15.2) - it exists to catch an
# actual coverage collapse, not to gate every incremental dip.
[tool.coverage.run]
# EVERY shipped package plus EVERY shipped loose module - kept in step with
# pyproject's own [tool.setuptools] manifest by
# tests/test_coverage_scope.py, which fails when the two drift.
#
# It used to be just backend + graphlink_plugins. Two of the four shipped
# PACKAGES were outside the floor entirely, and so were all 23 root modules -
# including api_provider.py, the code that talks to every model endpoint.
# Measured at the time of widening: 21,606 statements, 86% covered, so the
# 85% floor holds unchanged. provider_runtime is the weak spot inside it
# (ollama_scan 8%, llama_cpp_scan 10%, gemini_transport 29%) and that is
# precisely the point - those numbers were invisible before, and a
# regression in them now counts against the floor.
#
# The root modules are named WITHOUT a .py suffix. coverage's `source` takes
# packages and directories, and silently ignores anything else - the first
# version of this list wrote "api_provider.py" and measured exactly nothing
# new (statement count stayed at 18,763 instead of rising to 21,606). A
# config that looks right and gates nothing is the failure this list was
# widened to fix, so: names, not paths.
source = [
"backend", "graphlink_plugins", "provider_runtime", "settings_store", "contracts",
"api_provider", "graphlink_artifact_agent", "graphlink_audio",
"graphlink_chart_data", "graphlink_chart_rendering", "graphlink_chat_agent",
"graphlink_desktop", "graphlink_execution_guard", "graphlink_grid_view_settings",
"graphlink_memory", "graphlink_migrations", "graphlink_model_catalog",
"graphlink_navigation_pins", "graphlink_note_agent", "graphlink_process_env",
"graphlink_prompts", "graphlink_scratch_dirs", "graphlink_secrets",
"graphlink_settings_store", "graphlink_task_config", "graphlink_token_estimator",
"graphlink_version", "graphlink_wire_schema",
]
omit = ["*/tests/*", "*/__pycache__/*"]
[tool.coverage.report]
show_missing = true
fail_under = 85
# ADR-022 stage 22.4: scoped to 4 pure, platform-independent modules only -
# not the whole codebase. Mutation testing cost scales with mutant count x
# test-suite runtime; a curated, already-property-tested slice keeps a first
# run tractable and its score meaningful rather than noisy. Deliberately
# excludes graphlink_secrets.py (DPAPI is Windows-only - see backend/tests/
# test_backend_secrets_at_rest_properties.py's own comment) so the nightly
# job that runs this (.github/workflows/nightly-mutation.yml) can stay on
# ubuntu-latest - not a cost optimization alone: mutmut 3.x calls
# os.fork() directly and cannot execute on Windows at all.
[tool.mutmut]
source_paths = [
"graphlink_wire_schema.py",
"graphlink_migrations.py",
"graphlink_chart_data.py",
"backend/secret_scrub.py",
]
# mutation_tests/ is this ADR's own property-test suite, deliberately
# self-contained (no conftest.py importing backend/api_provider) so it
# doubles as mutmut's kill-suite without dragging the app's heavy import
# chain into every forked mutant's sandbox. backend/__init__.py is copied
# alongside it only because `from backend import secret_scrub` needs the
# package marker to exist in the sandbox - it is itself trivial (a
# docstring + one constant, no submodule imports), confirmed by reading it,
# not assumed.
also_copy = ["mutation_tests/", "backend/__init__.py"]
pytest_add_cli_args_test_selection = ["mutation_tests/"]
# Disables per-mutant xdist: pyproject.toml's own `-n auto --dist worksteal`
# addopts apply inside every forked mutant too (the sandbox copies this
# file), and mutmut's own --max-children fan-out is the only parallelism
# dimension this needs - stacking both multiplies process count for no
# benefit.
pytest_add_cli_args = ["-n0"]
[project.gui-scripts]
# R7.6b: was `graphlink_app:main`, the Qt entry point, deleted at the cutover.
# graphlink_desktop.main() is the pywebview shell that replaced it - it boots
# the FastAPI backend and opens the SPA in a native window.
graphlink = "graphlink_desktop:main"
[project.urls]
Homepage = "https://github.com/dovvnloading/Graphlink"
# R7.6b (Qt-removal cutover): graphlink_app/ is gone, and with it the entire
# package-dir indirection this section used to need. Every module now lives at
# its real import path, so setuptools needs no mapping: py-modules lists the
# loose top-level files (auto-discovery only finds real packages), and
# packages.find picks up backend/ and graphlink_plugins/ from the repo root.
#
# This also closes the two packaging gaps R7.2 recorded as accepted-for-now:
# the 17 relocated modules are listed again (they were unlistable while
# package_dir[""] had to stay "graphlink_app"), and `version = { attr = ... }`
# below now resolves, because attr: consults package_dir[""] - which no longer
# points somewhere graphlink_version.py isn't.
#
# contracts/ is deliberately absent: it is build-time codegen for the frontend
# contract (npm run check:schema shells out to it), not runtime application code.
[tool.setuptools]
py-modules = [
"api_provider",
"graphlink_artifact_agent",
"graphlink_audio",
"graphlink_chart_data",
"graphlink_chart_rendering",
"graphlink_chat_agent",
"graphlink_desktop",
"graphlink_execution_guard",
"graphlink_grid_view_settings",
"graphlink_memory",
"graphlink_migrations",
"graphlink_model_catalog",
"graphlink_navigation_pins",
"graphlink_note_agent",
"graphlink_process_env",
"graphlink_prompts",
"graphlink_scratch_dirs",
"graphlink_secrets",
"graphlink_settings_store",
"graphlink_task_config",
"graphlink_token_estimator",
"graphlink_version",
"graphlink_wire_schema",
]
[tool.setuptools.packages.find]
where = ["."]
include = [
"backend*",
"graphlink_plugins*",
"provider_runtime*",
"settings_store*",
]
# Auto-discovery here is namespace-aware (PEP 420), so backend/tests/ and
# backend/evals/ are picked up as real packages even without __init__.py in
# every directory - confirmed by direct find_namespace_packages() probe: it
# returns backend.tests, backend.tests.perf, backend.evals, and
# backend.evals.fixtures* even though backend/tests/ itself has no
# __init__.py. Without this exclude, the built wheel ships the entire test
# and eval-harness source tree (fixtures, conftest, perf baselines) as
# installed packages - dev-only content with no reason to reach an end
# consumer's site-packages. graphlink_plugins* and backend's real
# subpackages (api, domain, providers, ...) are unaffected.
exclude = [
"backend.tests",
"backend.tests.*",
"backend.evals",
"backend.evals.*",
]
[tool.setuptools.dynamic]
version = { attr = "graphlink_version.APP_VERSION" }
# ADR-015 stage 15.3: a scoped starting rule set, not a big-bang cleanup (the
# ADR's own explicit instruction) - F (pyflakes: undefined names, unused
# imports, bad call arity) and E9 (pycodestyle syntax errors) are the class
# compileall cannot catch and this stage exists to close; E/W (the rest of
# pycodestyle - line length, whitespace style) are deliberately deferred to a
# later ratchet rather than reformatting this entire codebase's existing
# comment-heavy style in one pass. Ratchet up (more rule categories, fewer
# ignores) in a later stage, never down.
[tool.ruff]
line-length = 120
target-version = "py310"
# No `exclude` needed: web_ui/ is a JS/TS tree ruff's own .py/.pyi file
# discovery already skips, and graphlink_app/ (once excluded here for its own
# Qt-era .py sources) has held zero .py files since the R7.6b cutover deleted
# them - both entries were excluding directories ruff would never have
# scanned anyway.
[tool.ruff.lint]
# T201 (no `print`) added 2026-08-12. ADR-016 stage 16.1's own text claimed
# "ruff's T201 enforces the ban" from the day it shipped, but T201 was never
# actually in this select list - so CI's `ruff check .` would have waved
# through a new print() in any shipped module. An audit caught the claim and
# the config disagreeing; this closes the gap rather than softening the claim.
#
# Enabling it required no code changes: all 28 existing print() calls live in
# five deliberate command-line entry points (see per-file-ignores below),
# none in shipped runtime code. That is exactly the state the ban is meant to
# preserve - structured logging (backend/observability.py) everywhere that
# actually runs inside the app.
select = ["E9", "F", "T201"]
[tool.mypy]
# ADR-015 stage 15.5: scoped to the modules already fully annotated
# (contracts/ plus the 3 backend/ modules ADR-015 names: events.py,
# token_counter.py, graphlink_process_env.py), not the whole backend/ tree -
# a raw `mypy backend/` run at landing found 642 errors across 34 of 188
# files, exactly the "big-bang cleanup" this ADR's own philosophy (see
# [tool.ruff]'s comment above) rejects. Ratchet this list wider as more
# modules earn real annotations - never narrower.
files = [
# 2026-09-04: everything. This list began as four files and was widened a
# module at a time as each one earned it; the last sweep took the tree from
# 179 errors to zero, so the ratchet's end state is simply "all of it".
#
# Kept as an explicit list of roots rather than ".", so that a new
# top-level directory has to be added here deliberately rather than
# arriving unchecked - and so this stays the record of what is covered.
"backend",
"contracts",
"graphlink_plugins",
"provider_runtime",
"settings_store",
"tests",
"tools",
"api_provider.py",
"graphlink_artifact_agent.py",
"graphlink_audio.py",
"graphlink_chart_data.py",
"graphlink_chart_rendering.py",
"graphlink_chat_agent.py",
"graphlink_desktop.py",
"graphlink_execution_guard.py",
"graphlink_grid_view_settings.py",
"graphlink_memory.py",
"graphlink_migrations.py",
"graphlink_model_catalog.py",
"graphlink_navigation_pins.py",
"graphlink_note_agent.py",
"graphlink_process_env.py",
"graphlink_prompts.py",
"graphlink_scratch_dirs.py",
"graphlink_secrets.py",
"graphlink_settings_store.py",
"graphlink_task_config.py",
"graphlink_token_estimator.py",
"graphlink_version.py",
"graphlink_wire_schema.py",
# 2026-09-05: the three directories a coverage check found sitting outside
# the gate - 14 files nobody was checking. plugins/ held six real errors
# (an unguarded Optional and two unnarrowed node-state reads in the System
# Prompt plugin); mutation_tests/ and tests_e2e/ were already clean.
"mutation_tests",
"plugins",
"tests_e2e",
]
# plugins/ is eight sibling directories each containing a `plugin.py`, with no
# __init__.py anywhere - the layout the plugin loader discovers by path. Without
# these two, mypy maps all eight onto one module name and refuses to check any
# of them ("Duplicate module named 'plugin'"). With them, the module name comes
# from the path relative to the repo root, so they stay distinct.
namespace_packages = true
explicit_package_bases = true
ignore_missing_imports = true
# Check the files in `files` fully; use everything they import for type
# information without reporting errors inside it. Without this, listing
# backend/session_save.py pulls its whole import closure - autosave,
# chat_library, assets, api_provider - into the gate and reports 141 errors in
# modules nobody put on the ratchet, which would mean the only way to add a
# clean module is to first clean everything it touches. The ratchet is meant
# to move one module at a time; this is what lets it.
#
# It does not weaken what is already listed: every module in `files` is still
# checked in full, and the followed modules were reporting zero errors of
# their own before this line existed, because the only ones being followed
# were themselves listed.
follow_imports = "silent"
[[tool.mypy.overrides]]
# The Review Lens plugin's own modules, checked BODY AND ALL.
#
# Listing a file under `files` above is not the same as checking it. mypy
# skips the body of any function with no annotations at all by default, and
# every function in review_engine.py is unannotated - so the largest file in
# this plugin sat inside the gate with all thirty of its bodies unchecked,
# and said "Success: no issues found". Turning the flag on here found a real
# defect immediately (a double `.get()` whose isinstance check narrowed the
# wrong lookup, in looks_like_a_review).
#
# Scoped to these modules rather than set globally: --check-untyped-defs
# across the whole tree is its own sweep, and this ratchet moves a module at
# a time (see [tool.mypy]'s own comment).
module = [
"graphlink_plugins.review_lens.*",
"backend.agent_dispatch.code_review",
"backend.api.intents_code_review",
"backend.domain.nodes_code_review",
]
check_untyped_defs = true
[tool.ruff.lint.per-file-ignores]
# backend/canvas.py's own docstring: it deliberately re-imports every
# backend.domain name "back for its own use, so every existing `from
# backend.canvas import X` consumer... keeps working unchanged" - a
# compatibility facade from the ADR-002 stage 2.2 domain-package split, not
# dead code. F401 (unused import) can't distinguish a re-export facade from
# an actual leftover, so it's ignored for exactly this one file rather than
# scattering 43 individual noqa comments across the import block.
"backend/canvas.py" = ["F401"]
# Same facade shape as backend/canvas.py above, from the Phase 4d
# api_provider.py -> provider_runtime/ split: api_provider.py re-imports
# every relocated name back into its own namespace so every existing
# `api_provider.<name>` caller and monkeypatch seam keeps working. F401
# can't tell that re-export block (or a module-top import now only used by
# relocated code) from a leftover, so it's ignored for this one file.
"api_provider.py" = ["F401"]
# T201 exemptions: these five are command-line tools, not shipped runtime
# code. printing IS their output contract - a developer runs them in a
# terminal and reads the result, so routing them through the structured JSON
# logger would make them strictly worse. Everything the app itself executes
# stays covered by the ban.
# - contracts/codegen.py .............. build-time codegen CLI (npm run check:schema shells out to it)
# - backend/evals/__main__.py ......... `python -m backend.evals` harness entry point (ADR-016 stage 16.5)
# - backend/tests/perf/measure_baselines.py ... `python -m ...` perf measurement CLI (ADR-019 stage 19.1)
# - backend/tests/perf/check_baseline.py ..... nightly regression checker, its stdout IS the CI job log (ADR-019 stage 19.3)
# - tools/build_app_icon.py ........... one-off dev icon builder
# - tools/seed_demo_graph.py .......... builds the README screenshots' fixture; its stdout confirms what it wrote
"contracts/codegen.py" = ["T201"]
"backend/evals/__main__.py" = ["T201"]
"backend/tests/perf/measure_baselines.py" = ["T201"]
"backend/tests/perf/check_baseline.py" = ["T201"]
"tools/build_app_icon.py" = ["T201"]
"tools/seed_demo_graph.py" = ["T201"]