Version. 0.1
Date. 2026-08-06
Author. Claude Code (inspection session). Operator: Marvin Percival.
Target. /Users/dunin7/loomworks-engine, branch main, HEAD 8992179a946a052c65ec2a58b1ca92301f7787cc, clean tree throughout.
Charter. standing-notes/dunin7-standing-authorization-charter-v0_1. R-5 inspection run.
Status. Read-only. No fix, no change request, no design decision. This document reports what each checker currently finds and where; it does not say what a gate should check.
Two commands were actually run against the repo (ruff check . and mypy, both read-only static analysis — no database, no server, no write to any tracked file). Everything below is measured from those two runs plus git blame, not estimated or carried from the build list.
Both are configured in pyproject.toml and shipped in the dev optional-dependency group; neither is invoked anywhere in .github/workflows/tests.yml, which runs pytest -q only. The workflow file's own header comment confirms this explicitly: "It does NOT run ruff or mypy (B-48's decision, not yet gated)."
pyproject.toml:``` [tool.ruff] line-length = 100 target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM"]
```
Eight rule families selected: pycodestyle errors/warnings (E/W), Pyflakes (F), isort (I), pep8-naming (N), pyupgrade (UP), flake8-bugbear (B), flake8-simplify (SIM). No per-rule ignores, no per-path excludes, no extend-immutable-calls (relevant below, §5).
uv run ruff check . (whole repo, from the repo root) — ruff 0.15.10. --statistics for the category rollup, --output-format=json for the file/line-level data everything else here is derived from.pyproject.toml:
```
[tool.mypy]
python_version = "3.12"
strict = true
```
strict = true turns on the full strict flag set (disallow-untyped-defs, no-implicit-optional, warn-return-any, etc.). No files, packages, or exclude key — the config does not scope what gets checked, only how strictly.
uv run mypy src — the distributable package ([tool.hatch.build.targets.wheel] packages = ["src/loomworks"]). Used as the primary figure below.uv run mypy . — the whole checked-out tree, which also pulls in tests/, migrations/, and scripts/. Reported as secondary context in §2.mypy 1.20.1. No documented invocation elsewhere in the repo either..py files (94%).
2,277 auto-fixable outright (ruff check . --fix), a further 67 reachable only with --unsafe-fixes — 2,344 of 3,618 (64.8%) mechanically fixable without a human judgment call on each one.
Full category breakdown (rule code, count, [*] = safely auto-fixable, [ ] = manual, [-] = fix exists but disabled by default):
| Count | Code | Rule | Fixable | |---:|---|---|---| | 825 | UP017 | datetime-timezone-utc | ✓ | | 800 | B008 | function-call-in-default-argument | — | | 672 | I001 | unsorted-imports | ✓ | | 303 | F401 | unused-import | ✓ | | 261 | UP045 | non-pep604-annotation-optional | ✓ | | 227 | E501 | line-too-long | — | | 131 | UP037 | quoted-annotation | ✓ | | 92 | B904 | raise-without-from-inside-except | — | | 38 | F841 | unused-variable | disabled | | 36 | N818 | error-suffix-on-exception-name | — | | 32 | E402 | module-import-not-at-top-of-file | — | | 25 | UP035 | deprecated-import | ✓ | | 24 | F821 | undefined-name | — | | 24 | SIM117 | multiple-with-statements | disabled | | 17 | SIM105 | suppressible-exception | — | | 17 | UP041 | timeout-error-alias | ✓ | | 15 | N802 | invalid-function-name | — | | 11 | B017 | assert-raises-exception | — | | 9 | F811 | redefined-while-unused | disabled | | ≤7 each | N806, N815, SIM102, SIM300, B905, N814, E702, F541, SIM103, SIM108, N812, UP012, UP047, B007, N801, N811, SIM110, SIM222, UP007, UP042 | (20 further codes, 36 findings total) | mixed |
Category breakdown (src-only):
| Count | Category |
|---:|---|
| 147 | type-arg — generic type missing its type argument (e.g. bare dict, Callable) |
| 114 | no-untyped-def — function missing a parameter or return type annotation |
| 80 | arg-type — argument's type doesn't match the parameter's declared type |
| 70 | import-untyped — importing a module with no type stubs / py.typed marker |
| 52 | call-arg — call site missing a required argument or passing an unexpected one |
| 31 | no-any-return | returning Any from a function with a non-Any declared return type |
| 21 | no-untyped-call — calling an untyped function from typed code |
| 20 | union-attr — attribute access on a value that could be None (or another union member lacking it) |
| 18 | attr-defined | 13 unused-ignore | 13 name-defined | 12 assignment | ≤3 each: var-annotated, return-value, no-redef, prop-decorator, override, operator, list-item, index, call-overload |
Secondary context — whole-repo scope (mypy ., includes tests/, migrations/, scripts/): 6,482 errors across 605 of 957 checked files. The tests/ tree is not part of the distributed package and strict-mode annotation requirements are a different question there than in src/; this inspection does not take a position on which scope a gate should use, only reports both numbers so the choice is informed.
Mypy has no [*]-style auto-fix — none of its findings are mechanically resolvable the way most of ruff's are.
Ruff. Top 10 files by finding count carry 583 of 3,618 (16.1%). 780 of 829 tracked Python files (94%) have at least one finding. This is broad scatter, not a small hot zone.
| Findings | % of total | File | Dominant codes |
|---:|---:|---|---|
| 100 | 2.8% | src/loomworks/sales_tools/prompts.py | E501 ×100 (100%) |
| 86 | 2.4% | src/loomworks/orchestration/router.py | UP045 ×51, UP037 ×20, I001 ×6 |
| 73 | 2.0% | src/loomworks/api/routers/engagements.py | B008 ×63 (86%), B904 ×9 |
| 64 | 1.8% | src/loomworks/api/routers/renders.py | B008 ×63 (98%) |
| 55 | 1.5% | src/loomworks/api/routers/assertions.py | B008 ×48 (87%), B904 ×7 |
| 53 | 1.5% | src/loomworks/api/deps.py | B008 ×27, I001 ×10, UP017 ×8 |
| 46 | 1.3% | src/loomworks/api/routers/considerations.py | B008 ×46 (100%) |
| 38 | 1.1% | src/loomworks/api/routers/memberships.py | B008 ×32 (84%), B904 ×5 |
| 35 | 1.0% | src/loomworks/notifications/service.py | UP045 ×23, UP017 ×8 |
| 33 | 0.9% | src/loomworks/api/routers/shape_events.py | B008 ×32 (97%) |
Six of these ten files' apparent concentration is almost entirely one rule — B008 — firing on ordinary FastAPI route-parameter declarations (Depends(...), Path(...), etc.) in every router file that has more than a handful of endpoints. That is a distribution artifact of the framework, not a defect hot spot; see §5.
Mypy (src-only). Top 10 files carry 202 of 604 (33.4%) — more concentrated than ruff's spread, but still short of a small isolated cluster. 171 of 374 source files (46%) have at least one error.
| Errors | File |
|---:|---|
| 34 | src/loomworks/api/deps.py |
| 26 | src/loomworks/api/routers/sales_tools.py |
| 25 | src/loomworks/engagement/boundary.py |
| 23 | src/loomworks/api/routers/seed_conversation.py |
| 19 | src/loomworks/orchestration/router.py |
| 18 | src/loomworks/orchestration/routers/converse.py |
| 17 | src/loomworks/orchestration/prompt.py |
| 15 | src/loomworks/uploads/executor.py |
| 13 | src/loomworks/credit/seam.py |
| 12 | src/loomworks/engagement/composition_orchestrator.py |
git blame on every flagged lineBlamed every flagged line (all 3,618 ruff findings; all 636 mypy src-only finding lines — some lines carry more than one mypy error) against its introducing commit's author date. Repo history spans 2026-04-13 (earliest commit) to 2026-08-06 (HEAD, this inspection's baseline).
Ruff, by month the flagged line was introduced (all 3,618 findings matched):
| Month | Findings | % | |---|---:|---:| | 2026-04 | 1,388 | 38.4% | | 2026-05 | 1,283 | 35.5% | | 2026-06 | 505 | 14.0% | | 2026-07 | 271 | 7.5% | | 2026-08 | 171 | 4.7% |
Mypy src-only, by month (all 636 finding-lines matched):
| Month | Findings | % | |---|---:|---:| | 2026-04 | 193 | 30.3% | | 2026-05 | 255 | 40.1% | | 2026-06 | 111 | 17.5% | | 2026-07 | 57 | 9.0% | | 2026-08 | 20 | 3.1% |
Reading this. For both tools, the first two months of the project (April–May) account for roughly two-thirds to three-quarters of everything currently flagged (73.9% ruff, 70.4% mypy) — the backlog is mostly old. But it is not only old: June–August combined still accounts for 26.2% of ruff findings and 29.6% of mypy findings, including 171 ruff findings and 20 mypy errors on lines introduced in August alone, the same month as this inspection. Recent work is still adding to both backlogs, not just failing to shrink an inherited one.
datetime.timezone.utc → datetime.UTC. Purely a Python-3.11+ spelling modernization; zero behavioral difference; auto-fixable. Examples:
migrations/versions/0034_phase_15_loomworks_engagement_induction.py:72migrations/versions/0035_phase_15_founding_memory.py:163src/loomworks/api/deps.py (8 instances)
All 800 instances, checked without sampling, are FastAPI's own dependency-injection idiom — Depends(...) (540), Path(...) (228, including 4 under a local alias PathParam), Query(...) (18), Body(...) (7), File(...) (3), and 2 instances of a Pydantic request-model default (body: AdvanceStageRequest = AdvanceStageRequest()). Examples:
src/loomworks/api/deps.py:59 — store: CredentialStore = Depends(get_credential_store)src/loomworks/api/routers/considerations.py:410 — body: AdvanceStageRequest = AdvanceStageRequest()src/loomworks/api/routers/contributions.py:113 — engagement_id: UUID = PathParam(...)
Calling a function in an argument default is exactly what FastAPI's dependency-injection system requires at every route signature; this is the pattern the rule exists to catch in ordinary code, firing on the one framework where it's the documented, required way to write a function signature. Ruff's own documentation names fastapi.Depends/fastapi.Query et al. as the standard case for the extend-immutable-calls setting, which this repo's [tool.ruff.lint] block does not set.
Import-block ordering/formatting (isort-equivalent). Auto-fixable. 95 of the 672 (14%) are in migrations/versions/*.py — one-off, never-re-run migration scripts rather than living application code. Examples:
migrations/env.py:18migrations/versions/0001_phase_1_substrate_events.py:7type-arg (147, the largest mypy category)
A generic type used without its type parameter — dict instead of dict[str, Any], Callable instead of Callable[..., None]. Examples:
src/loomworks/uploads/skills/base.py:108 — "Missing type arguments for generic type dict"src/loomworks/engagement/boundary_predicates_custom.py:25 — "Missing type arguments for generic type Callable"src/loomworks/storage/object_store.py:86 — "Missing type arguments for generic type dict"no-untyped-def (114, second-largest)
A function with no parameter or return annotation at all — the base case strict = true exists to close. Examples:
src/loomworks/files/conversion.py:58 — "Function is missing a return type annotation"src/loomworks/memory/projector.py:993 — "Function is missing a type annotation"src/loomworks/db.py:14 — "Function is missing a type annotation for one or more parameters"import-untyped (70, fourth-largest)
All 70 instances are the same underlying fact, checked without sampling: importing stele.registry or stele.webauthn — the sibling DUNIN7 package this repo depends on via [tool.uv.sources] stele = { path = "../stele", editable = true } — which ships no py.typed marker and no separate stub package. Every call site importing from stele in this repo trips it identically. This is a gap in a different DUNIN7 repository, not a defect any change in loomworks-engine can close on its own. Examples:
src/loomworks/api/app.py:214src/loomworks/api/deps.py:14src/loomworks/api/routers/admin_grants.py:43
Not everything mypy reports is a framework or annotation-completeness artifact. src/loomworks/orchestration/router.py alone carries several that read as real type mismatches, not missing annotations:
router.py:3512 — "Incompatible types in assignment (expression has type Assertion, variable has type dict[str, Any] | None)"router.py:3523 — "Item dict[str, Any] of dict[str, Any] | None has no attribute display_number"router.py:2679 — "Argument llm_client to _call_seed_commit_from_brief has incompatible type LLMClient | None; expected LLMClient"
Surface repo (loomworks, TypeScript/Next.js) — gated, and gated on framework defaults. eslint.config.mjs uses eslint-config-next's core-web-vitals and typescript presets verbatim — the framework's own recommended rule set, not a hand-curated allowlist — with exactly one override: @typescript-eslint/no-unused-vars relaxed to recognize the codebase's existing leading-underscore-means-deliberately-unused convention (dated to CR-2026-166 D-4, with the override's reasoning recorded in the config file itself). Wired into CI (eslint, tsc, build, vitest all run on every push per CR-2026-167). The one override exists to match an established codebase convention, not to suppress a category of findings wholesale.
Marketing repo (loomworks-marketing, Astro) — no precedent. No eslint config file, no lint script in package.json, no CI workflow beyond what ships inside node_modules. Nothing to compare against.
Sibling Python repos — no precedent, same gap. Checked stele and stele-agentic-id, the only other Python codebases in DUNIN7. Neither has a ruff or mypy section in its pyproject.toml, and stele's own CI workflow (.github/workflows/test.yml) runs pytest -q only — the identical pattern this engine repo has before B-48. There is no DUNIN7 Python precedent for what a ruff/mypy gate should check; the surface repo's TypeScript precedent (gate on framework defaults, override narrowly and only to match an established convention) is the only existing example in the org, and it is a different toolchain.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — B-48 quality tooling Step 0 inspection findings — v0.1 — 2026-08-06