Skip to content

Make the two standing quality thresholds ratchet instead of sit still - #485

Open
JE-Chen wants to merge 19 commits into
mainfrom
feat/typing-contract-and-coverage-ratchet
Open

Make the two standing quality thresholds ratchet instead of sit still#485
JE-Chen wants to merge 19 commits into
mainfrom
feat/typing-contract-and-coverage-ratchet

Conversation

@JE-Chen

@JE-Chen JE-Chen commented Aug 20, 2026

Copy link
Copy Markdown
Member

Closes the Progress.md entry "two thresholds we agreed to climb and never did".
Both were promises that lived only in a pyproject.toml comment, with nothing
anywhere that would ever move them.

Stacked on #484, but based on main rather than that branch: every workflow
triggers only on PRs targeting dev/main/stable, so a PR based on a feature
branch gets no CI at all. The diff therefore carries #484's commit as well
— already reviewed and green there. Review the second commit for this change.

Coverage: the floor was 15 points below what the tests already earn

fail_under = 35 was the first measured baseline and then never moved while the
suite grew past it. Every square of the nine-way matrix clears 50% — measured on
the run for #484, from 50.26% (ubuntu-22.04 / 3.10) to 51.69%
(windows-2022 / 3.14) — so CI would have passed a change that deleted a third of
the tests without a word.

The floor is now 50, taken from the lowest square rather than from one
machine, and the comment states the rule that was missing: this is a ratchet,
raised whenever the suite has earned it. 70 is still the destination.

mypy: the scope was 4 files, and could only ever grow by hand

The job checked je_auto_control/api and je_auto_control/utils/failure_bundle
while a comment promised the rest would join "the contract" later. A scope
written as a path list never grows on its own, and every new module lands
outside it by default.

So the scope is inverted: mypy checks the whole package, and the modules that
do not pass yet are named in test/verify/typing_contract_exempt.txt.

  • 862 of 1,017 files are inside the contract today, and a new module is
    inside it the moment it is written.
  • The list may only shrink. typing_contract_verify.py fails when a listed
    module starts passing (delete the line) as loudly as when an unlisted one
    stops. Both directions were checked by breaking them.
  • --fix re-measures the list, so shrinking it is one command.

It checks three platforms, not one

mypy resolves sys.platform branches against a single target, so the Ubuntu-only
job never looked inside the Windows, macOS or platform-gated code — three of the
four backends. That blind spot was real and is now measured: 13 modules fail
only when the target is Linux, 3 only when it is Windows.
The script runs
win32, linux and darwin and unions the results.

The part that took the work: the gate has to mean one thing

A first measurement taken on a dev checkout disagreed with a bare
pip install -e . about 38 modules — 36 Qt modules that pass only because
PySide6 is absent, and 2 that fail only because babel and pytest are.
Committing that list would have reddened CI on arrival and mis-listed the other
36. A gate that flips on pip install is not a gate.

Every third-party module outside the base dependency set is therefore forced to
Any. Two settings were needed, not one:

  • ignore_missing_imports alone is not enough — it still lets mypy read the real
    package when it is installed, which is the environment-dependence itself.
  • follow_imports = "skip" is silently ignored for .pyi files. PySide6 ships
    inline stubs, so follow_imports_for_stubs was required too — the same trap
    the numpy override in pyproject.toml had already paid for and written down.

Verified identical results across three dependency sets: dev checkout with
[gui] + [webrtc], a bare pip install -e . (CI's exact set), and bare plus
python-Xlib (the only package a Linux host adds).

Verification

  • Full headless suite: 4,867 passed, 17 skipped, no failures.
  • ruff, pylint (10.00/10), bandit and radon clean on the new script.
  • Doc-count guards pass; architecture_explore.md updated to match.
  • The typing-stable-api job id is deliberately unchanged — it is a required
    check, and renaming it would silently drop the requirement.

The remaining 155 modules cluster (utils/remote_desktop 13, gui 11,
wrapper 5); Progress.md now records clearing them one cluster at a time
instead of "no plan in between".

Their own reason said what they needed: "needs subprocess isolation (see
test_actions_menu_gui) ... skip until then". They cover real wiring — a
file received on a WebRTC worker thread reaching the GUI thread through a
queued signal rather than a thread-affine singleShot, and the admin
console's thumbnail poll deleting its QThread each tick instead of
leaking one per interval.

Skipping was right at the time. Building the WebRTC panel or the admin
console and then tearing a worker QThread down aborts the shared pytest
process under offscreen Qt, and since deleteLater is a no-op until an
event loop runs, the abort lands in some later unrelated file with no
traceback.

Run all three in one child process that writes a JSON verdict per check
and exits without teardown, the shape test_actions_menu_gui already uses.
Verdicts are ok / failed / unavailable, so a machine without the webrtc
extra reports a skip while one that has it checks the wiring.

Confirmed to have teeth rather than assumed: dropping
thread.finished.connect(thread.deleteLater) turns the third verdict into
"failed: the QThread outlived finish" and leaves the other two green.
Both were promises living only in a pyproject.toml comment, with nothing that
would ever move them.

Coverage: fail_under was the first measured baseline and never moved while the
suite grew past it, leaving ~15 points unguarded — every square of the matrix
clears 50% and CI would still have passed a change deleting a third of the
tests. The floor is now the lowest square (50.26%, ubuntu-22.04/3.10), and the
comment states the rule that was missing.

mypy: the scope was two directories, and a path list only grows when someone
remembers to grow it — a new module lands outside it by default. The scope is
now the whole package minus a shrink-only list, so 862 of 1,017 files are in
the contract and new modules join it on arrival. The verify script fails when a
listed module starts passing as loudly as when an unlisted one stops.

It also runs win32/linux/darwin rather than only the runner's platform, so the
Windows and macOS backends are checked from Ubuntu; 13 modules fail only under
a Linux target and 3 only under Windows.

The gate has to mean one thing everywhere: a dev checkout with [gui]/[webrtc]
disagreed with a bare install about 38 modules, so every non-base third-party
module is forced to Any. ignore_missing_imports alone still reads the real
package when installed, and follow_imports = "skip" is ignored for .pyi files —
PySide6 ships inline stubs, so follow_imports_for_stubs is required too, the
same trap the numpy override had already paid for.
@JE-Chen
JE-Chen changed the base branch from feat/qt-thread-marshal-isolation to main August 20, 2026 22:06
@JE-Chen JE-Chen closed this Aug 20, 2026
@JE-Chen JE-Chen reopened this Aug 20, 2026
@codacy-production

codacy-production Bot commented Aug 20, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 77 complexity · -1 duplication

Metric Results
Complexity 77
Duplication -1

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

JE-Chen added 17 commits August 21, 2026 06:14
Codacy honours a nosemgrep marker only on the exact line it reports, and the
audit rule reports the call line. Same shape as adb_client.py: argv is a list
of literals plus one value from the module-level PLATFORMS tuple, no shell.
platform_wrapper re-exports eight names and everything above it is written
against those names, but nothing said what they were. mypy bound each one to
whichever branch it read first — always the Windows one, on every target — so
the layer above the seam was checked against Win32 signatures even on Linux and
macOS, and a backend could omit a member entirely without a word.

The names are now declared before the branches bind them, three of them with
Protocols in wrapper/backend_contract.py, and each _platform_* module annotates
what it assigns, so an incomplete backend fails in its own file naming the
missing member. keyboard and mouse stay Any: their call shape really is
per-platform, and Progress.md records what a per-platform protocol would cost.

Turning it on surfaced four None-reaching-a-non-optional bugs and one backend
disagreeing with the seam it implements; both are in CHANGELOG.md. Six wrapper
modules leave the typing exemption list, and four more outside the cluster
went green on their own — they had been failing on the seam's accidental types.
Thirty-three of the forty errors were one sentence: libei reads its resolved
entry points straight off an Optional[BoundSymbols]. Every one of those reads
is guarded — connect() refuses an unavailable backend, _emit a disconnected
one — but the guarantee lived in the call graph rather than anywhere a reader
or a checker could see it, and a new call site that skipped a guard would raise
AttributeError, which is not an AutoControlException and so escapes every
containment boundary. They now go through one _api property that raises
LibeiUnavailable, which is what this module says every failure in it raises.
_teardown keeps the raw attribute on purpose: it runs from an except
BaseException handler, where raising would replace the real failure.

The other three were each one honest disagreement — environment probes typed
dict while defaulting to os.environ, a writer declared to return None while
every caller returns the tool's stdout, and Pillow's getpixel union handed to
callers that unpack three ints. No behaviour changes.
Five errors across four modules, one of them a live bug. The X11 listener's
record_queue was assigned None with no annotation, so its type *was* None:
record() could not fill it and stop_record() promised a Queue while able to
return the None it started as. Stopping a recording that was never started
reached x11_linux_record, which reads .queue off the result — an AttributeError
a frame away from the mistake. It returns an empty queue now, matching what the
public stop_record() already returns for the same case.

osx_keyboard.press_key tested `keycode in special_key_table`, which narrows the
string case into the branch but leaves int | str outside it, so an unknown name
reached Quartz as a keycode. A string only ever names a special key here.

uinput/_device's O_NONBLOCK is POSIX-only and the contract checks a Windows
target too; the flag moved into a sys.platform branch mypy can prune.
Two of these are live defects rather than annotations.

window_message did `from ...windows_window_manage import FindWindowW`, and
that module exports no such name — FindWindowW is a method on its private
user32 handle — so importing window_message raised ImportError on every
Windows machine. Nothing caught it because the only importer in the tree is a
manual test. It calls the public get_one_window_hwnd now, which is also the
one that declares HWND-width argtypes.

win32_ctype_input wrote ULONG_PTR into the standard library's own
ctypes.wintypes namespace. The name appears exactly once in the tree, on that
line, so the only thing the assignment could do was answer for another library
in the same process. Its `_fields_: tuple` annotations also redeclared a ctypes
ClassVar as an instance variable, and ctypes.POINTER and user32.SendInput were
used as types when one is a function and the other a value.

Every module under windows/ now type-checks on the win32 target. Eight stay
exempt for a reason that is not about them, measured and left in Progress.md
as a DECIDE: the gate also checks them as Linux and macOS, where typeshed does
not declare the Win32-only corner of ctypes.
The macOS 3.14 grid cell failed on `sent.count("doc.txt") == 1` seeing 2:
the test edited the file in place and then pushed its mtime forward, so
between `write_text` and `os.utime` the file briefly carried a third,
intermediate mtime. A tick landing in that window pushed the file once
for the intermediate mtime and again for the final one — correct engine
behaviour, an over-specified test. Stage the new content outside the
watch dir and swap it in with `os.replace` so one edit is one event.

The other half was the same guess in the other direction: every test
slept a fixed 0.4s hoping the baseline snapshot had been taken, but the
worker takes it asynchronously, so a slow runner could fold the test's
own edit into the baseline and push nothing at all. `wait_until_ready()`
makes that observable, and it is not test-only scaffolding — a caller
that drops files right after `start()` has the same race.
Thirteen modules, 169 errors, and only two shapes between them.

Nine were `self._x = None` with no annotation — which makes the
attribute's *type* `None`, so every later assignment is "incompatible
types" and every later read is "None has no attribute". Several already
carried the intended type in a trailing comment; those comments are now
annotations, with the classes imported under TYPE_CHECKING so the lazy
runtime imports stay lazy. Where the attribute is read back through a
closure the receiver binds to a local first, because a closure re-reads
the attribute and no narrowing survives that.

The other four were mixins reading attributes they do not own. Each
one's docstring already listed what it borrows from the host class it is
mixed into; that list is now an `if TYPE_CHECKING:` block in the class
body, which the runtime strips, so a declaration there cannot shadow
what the host binds.

Three of the fixes are behaviour rather than annotation:

* `WebRTCLoopBridge._run` read the loop back off shared state after
  `start()` set it. The loop is passed to the thread instead, and
  `start()` returns it, so submit/call_soon hold what they use.
* `_get_cursor_position` did `import sys as _sys` inside the function,
  which mypy does not recognise as a platform test — so its Win32 branch
  was checked against Linux and macOS as well.
* `totp` caught `base64.binascii.Error`, a name that exists only because
  `base64` imports binascii itself. It imports binascii by name now.
The accessibility backends, observability, the triggers, chatops, the
REST server, the MCP HTTP transport and element_repository, together,
because they kept hitting the same causes.

Thirty-four of the forty-three accessibility errors were one missing
return annotation: `_unsupported` always raises, so every method whose
last statement is a call to it looked like it could fall through. It is
`NoReturn` now.

A catch tuple that is not typed as one catches nothing as far as mypy is
concerned. `_uia_errors()` returned `Tuple[type, ...]`, which is not "a
tuple of exception classes", so all five `except UIA_ERRORS` sites were
errors — on a tuple that exists precisely because `comtypes.COMError`
inherits from `Exception` and nothing else. `rest_server` and
`chatops.router` had the same shape via `except (…, *SQLITE_ERRORS)`,
which mypy cannot follow into an `except`; both now name the set once as
an annotated module constant, as `mcp_server._protocol` already did.

`parse_content_length` declared `Mapping[str, str]` and no caller ever
passed one: all three hand it `BaseHTTPRequestHandler.headers`, an
`email.message.Message`, which is not a mapping over its keys and which
matches header names case-insensitively — the property that makes
`Content-length` work. It takes a one-method protocol now.

Four fixes are behaviour:

* `Gauge` and `Histogram` borrowed `Counter._labels_key` by assignment,
  so a gauge's label typo was validated by a method declared to take a
  counter. The rule is the same for all three and moved to the base,
  which also gives the registry's `render()` loop a base to call.
* IMAP UIDs were decoded at two of the three places that needed them.
  They are decoded once on arrival now, so the fetch, the mark-seen and
  the seen-set all speak one type.
* `ElementRepository` passed a user-editable locator to the
  accessibility API as `**kwargs`, so a field that is not a filter came
  back as a TypeError about keyword arguments from inside the backend.
* `_AtspiConnection._call` had no bus to call outside its `with`.

`_process_name` is a kernel32 round trip and now says so with a
`sys.platform` guard, instead of being type-checked against Linux.
Re-measuring the DECIDE found it was twice the size it recorded and a
different shape. Sixteen modules — not eight — pass on --platform win32
and fail on the other two targets for exactly one reason: typeshed
declares windll / WinDLL / WINFUNCTYPE / WinError / get_last_error on
Windows only. Half of them are nowhere near je_auto_control/windows/
(trash, app_idle, file_assoc, idle_keepawake, lock_session,
session_guard, usb/passthrough/key_provider, gui/main_window), which
rules out the option the entry had recommended: "measure a platform
module on its own platform" cannot be a directory rule when half the
affected modules are not in a platform directory.

The maintainer chose per-site suppression. It came to 28 lines, not the
58 the entry projected — 58 counted each source line once per non-win32
target. Every one carries its own reason; none is blanket.

Two things had to be measured. mypy honours `# type: ignore` only as
the first comment on the line, so the two lines that already carried a
`# nosec B607` take the marker first and merge the two justifications.
And nine lines could not hold the marker inside the 120-char limit, so
they are reformatted rather than shortened into meaninglessness: the
marker follows an opening paren, and two sites hoist a value into a
local first (`last_error` in the DPAPI wrapper, `kernel32` in the input
hook), which reads better than the one-liner did.

All sixteen were re-imported and exercised on Windows afterwards — a
reformat only a type checker has verified is a reformat nobody has.
Both were the same mistake told two ways — a handle whose declared type
cannot do what the code asks of it — and both hid a real defect.

`winusb_backend._load_dlls` published its three DLL handles one at a
time behind `if _setupapi is not None: return`. A failure loading
winusb.dll — what happens on a machine with no WinUSB-bound device —
left `_setupapi` set, so the guard short-circuited every later attempt
and the call sites got `AttributeError: 'NoneType' object has no
attribute 'WinUsb_Initialize'` instead of the retry the guard exists to
allow. The three now come back from one loader as a NamedTuple,
published only after all three load. With them: a device enumerated
without an interface path is skipped rather than passed to CreateFileW
as None, and a WinUsb_Initialize that reports success with a null handle
is a failure rather than an Optional[int] handed to the wrapper.

`clipboard_api()` returned `Tuple[object, object]`. `object` has no
attributes, so all twelve calls through it were errors — in the one
module whose whole docstring is about declaring these prototypes
correctly. A ctypes library resolves symbols through `__getattr__`, so
`Any` is the only honest promise and is what it says now.

Both were exercised against the real thing on Windows: a clipboard text
round trip, `clipboard_formats()` on a live clipboard, and the WinUSB
backend enumerating an actual bound device.
Thirteen of the fourteen gui modules. Most of the eighty-nine errors
were the mixin shape this branch has now fixed three times: six tab
mixins read `_tr`, `_translate` and `timer` off a host they never
declared, and each said so in its own docstring. Those docstrings are
declarations now, in a TYPE_CHECKING block the runtime strips.

Three of the fixes fail for a user, not for a checker:

* A pixel assertion with one coordinate reported the wrong problem.
  `assert_pixel(*_parse_ints(xy)[:2], _parse_ints(rgb), match=…)` binds
  the RGB list to `y` when the star-unpack yields one item, so `match`
  and `raise_on_fail` collide with positional slots and the user gets a
  TypeError about duplicate keyword arguments. The count is checked
  first now, and the message names what is missing.
* `_get_mouse_pos` unpacked a value the Windows backend really returns
  as None — `GetCursorPos` fails on a locked or secure desktop — so the
  warning box read "cannot unpack non-sequence NoneType object". It
  raises AutoControlException with a sentence instead.
* `multi_language_wrapper` typed its listeners `List[callable]`, the
  builtin function rather than a type, so every `listener(language)`
  read as calling something not callable.

`recording_edit.editor` came with them: both optional parameters were
written `end: int = None`.

webrtc_panel.py is the one that stayed. Seven of its errors come from
`_build_advanced_group(panel: TranslatableMixin, …)`, which writes five
widget attributes back onto a panel that has none of them; the honest
type is a Protocol, and the file sits exactly on its 2,555-line cap.
Progress.md now records that its exemption is blocked on the split that
file already owes — moving that builder out settles both at once.
`AC_normalize_url` and `ac_normalize_url` both forward to
`url_canon.normalize_url` and both passed `drop_fragment=` — the name
they expose to callers. The function's parameter is `strip_fragment`, so
every call raised TypeError, in the executor, in the MCP tool, and from
the Script Builder field that feeds them. The outward name stays (it is
in the action schema and the tool registry); the two call sites pass it
under the name the callee uses. Measured: the MCP tool now returns
{"url": "https://example.com/b"} where it returned an error.

The rest of the cluster was shapes this branch keeps meeting:

* ClientRequestMixin borrowed seven attributes from MCPServer and listed
  all seven in its docstring; that list is a declaration now.
* `_DISPATCH_ERRORS` and `_TOOL_INVOKE_ERRORS` — the containment
  boundary for the whole stdio loop — were not typed as tuples of
  exception classes, so all three `except` sites were errors.
* `_dispatch` fed an Optional[str] method name to `dict.get`. A request
  with no method takes the not-found branch explicitly now, with the
  same -32601 body it produced by falling through.
* The subscription callback was a default-argument lambda, which mypy
  cannot infer against a `Callable[[], None]`. `functools.partial` binds
  the same value and states the type.

Two public return annotations were wrong in the safe direction:
`set_mouse_position` and `hotkey` say `... | None` while every path
returns the tuple or raises. Narrowing them is what let the MCP handlers
stop indexing an Optional. `get_mouse_position` keeps its `| None` — the
Windows backend really does return that when GetCursorPos fails.
Thirteen modules of two to six errors each. Three recurring shapes:
`callable` used as a type (it is the builtin function, so mypy reads
every call through it as calling something not callable — six times in
plugin_loader, once in each hotkey backend); `x: SomeType = None`
defaults; and tuples that lost their length, where `tuple(r)` and
`cv2.boundingRect()` are `tuple[int, ...]` and the fields they feed
promise four ints.

Two are more than annotations:

* `_StabilityTracker` read `now - self._since` on a path where `_since`
  is non-None only because of what an earlier call did. True today,
  invisible to a reader, one refactor from a TypeError inside the poll
  loop. It binds the value and treats "no start time" as "not stable".
* `act_when_ready` passed `report.point` to a callback that requires a
  point. `point` is None whenever the target is invisible, and the guard
  above tests `report.actionable` — which implies visible, again only
  through the call graph. The point is checked where it is used.

`text_regions` needed one justified suppression, and it is worth
recording why: `cv2.MSER_create` exists at runtime in every supported
OpenCV but is absent from the .pyi opencv-python ships (measured on
4.13.0). Progress.md now notes the consequence — the mypy config lists
cv2 as a base dependency that ships no stubs, but it ships one, so the
gate reads it and its verdict can move with the OpenCV version inside
the >=4.8,<6 pin.
Eight errors, and two of them were the dispatch table disagreeing with
itself. `_android_client_cache` is keyed by the `(serial, adb_path)`
pair but declared `Dict[Optional[str], Any]`, and `backend_obj` was
assigned an Anthropic backend on one branch and an OpenAI one on the
other, so mypy read the second as an error rather than as the sibling
implementation of `AgentBackend` it is.

`_unwrap_action_list` reassigned its own parameter to the result of
`dict.get`, widening a `Union[list, dict]` to include None; it unwraps
through a local now, which is also what the `isinstance` checks below
it were already assuming.

`ensure_state`'s `StateSetter` promised `Callable[[Any], None]` while
every real setter in the tree returns a bool the function then ignores —
it re-reads instead of trusting what a setter reports, which is the
whole point of the helper. The alias says `Any` now, and the comment
says why. `RedactionPolicy.with_extra_regions` likewise: it coerces its
extras to four ints internally, so it can accept the sequences callers
actually have.
`cursor.lastrowid` is None until an INSERT has run on that cursor, and
three subsystems returned `int(cursor.lastrowid)` straight out of an
insert helper — work_queue, agent_memory and run_history. They now go
through `sqlite_support.last_row_id`, which says what it means when the
id is not there instead of handing None on as a row id.

The rest of the batch is the shapes the tail keeps repeating:

* Three `__exit__` methods annotated `-> bool` while every path returns
  False. `Literal[False]` is what they do.
* Five implicit-Optional defaults (`x: str = None`).
* `base64.binascii.Error` again, in the admin client — the same import
  side effect `totp` was catching.
* `DeterministicRun._freeze_clock` multiplied `self._freeze_time` by 1e9
  without checking it; its only caller guards on the same attribute, so
  the invariant was real but lived one frame away.
Forty-one modules in one sweep, and the list is now a single line:
`gui/remote_desktop/webrtc_panel.py`, blocked on the split its size cap
already owes it (recorded in Progress.md).

Several were annotations that were simply wrong about what the code
returns, and fixing them is what let the callers stop working around it:

* `screenshot()` said `-> List[int]` while it returns the captured BGR
  frame. Every caller that read `.shape` off it was reading through a
  lie; `screen_record` was the one mypy caught.
* `visual_match._nms` was typed for `Match` and reused from
  `rotated_match`, `edge_match` and `color_match` with their own record
  types. It takes a `TypeVar` bound to a read-only Protocol of what it
  actually reads — position, size, score — which is also why the bound
  had to be properties: the records are frozen dataclasses, and a
  Protocol attribute demands a writable one.
* `logical_frame`'s injectable grabber was typed `Callable[..., Any]`
  while its own docstring says "ImageGrab-shaped object", so `.grab()`
  on it was an error.

Four fix a `None` the code could really see: `humanize.motion` and
`watcher` unpacked `get_mouse_position()` (the Windows backend returns
None on a locked desktop), `dag.runner` opened a node's `action_file`
without checking a node had one, and `color_match` divided a `None`
accumulator when handed no channels.

`secret_store._require_unlocked` now returns the `(fernet, vault)` pair
it checks, which removed three `type: ignore`s that existed only because
the check and the use were separate statements.

Two more cv2 names — `VideoWriter_fourcc` and `ORB_create` — join
`MSER_create` as present at runtime but absent from the shipped stub.
`je_auto_control` type-checks clean on win32, linux and darwin with
nothing exempted. The list that started this branch at 155 modules now
holds a header and no entries, and the gate fails if it grows again.

The last module was webrtc_panel.py, blocked by its size rather than its
types. Seven of its errors came from `_build_advanced_group(panel:
TranslatableMixin, …)`, a free function that writes five widget
attributes back onto the panel it is handed — none of which
TranslatableMixin has. Saying that needs a Protocol, and the file was
sitting exactly on the 2,555-line cap it may only shrink from.

So the builder moved out, which is the split Progress.md already said
that file owed: `advanced_group.py` holds the shared STUN/TURN group,
the `AdvancedGroupHost` protocol naming what it reads and what it sets,
and the hardware-codec row as its own function. The panel is 2,545 lines
— under its cap for the first time — and both panels were rebuilt
offscreen to confirm the STUN default, the TURN fields and the host-only
codec picker still arrive where they did.

Ten more errors there were the pattern the whole sweep kept meeting: a
handle that is None until the session starts. `_produce_offer`,
`_trust_session_viewer`, `_answer_and_push`, `_produce_answer` and the
folder sync all reached through `_multi_host` / `_viewer` without
asking. They go through `_require_multi_host()` / `_require_viewer()`
now, raising a translated "start hosting first" / "connect to a host
first" rather than an AttributeError on None — two new keys in all four
language catalogues.

Progress.md's mypy entry collapses to what is still worth knowing: the
five recurring shapes, that a catch tuple has to be an annotated module
constant, that `# type: ignore` only counts as a line's first comment,
and that the cv2 stub omits names the library has.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant