From bd86d4c7365594e58e91b234af063b6969c94097 Mon Sep 17 00:00:00 2001 From: dovvnloading <157447210+dovvnloading@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:06:51 -0400 Subject: [PATCH] Put the last 14 files under the type gate, and fix what they were hiding A coverage check of [tool.mypy].files against the tree found three directories with Python in them that nothing was checking: plugins/ (8 files), mutation_tests/ (5) and tests_e2e/ (1). The gate said "339 source files" and meant it, but "all of them" was never verified - so this checks. mutation_tests/ and tests_e2e/ were already clean. plugins/ was not. plugins/system_prompt/plugin.py had six errors, of two kinds: root = document.get_branch_root(parent_node_id) ... edge.target == root.id get_branch_root returns SceneNode | None - None for an id it does not know. It cannot be None here, because the guard fifteen lines up already returned early for a parent_node_id that is not in document.nodes. Rather than assert that with an ignore, the None branch now returns _create_pending() - the answer this plugin already has for "no usable parent", and the difference between an AttributeError and a sensible fallback if that guard ever changes. The other four were the familiar unnarrowed node state, in a generator that read `document.nodes[edge.source]` three separate times. A checker cannot tie a kind check on one subscript expression to a field read on another, and neither can a reader. It is a plain loop over one bound candidate now, with the same result: first match in edge order, or None. Two mypy settings come with plugins/: namespace_packages and explicit_package_bases. plugins/ is eight sibling directories each holding a plugin.py with no __init__.py anywhere - the layout the loader discovers by path - and without those two mypy maps all eight onto one module name and refuses to check any of them. Verified they change nothing for the existing files: the run is clean before and after. The gate now covers 353 source files and the coverage check reports no uncovered directory and no uncovered root module. Verified it still bites by injecting a deliberate type error into a plugin, a mutation test and api_provider - caught in all three. Test plan: full suite 3,285 passed / 20 skipped; the System Prompt plugin's own 23 tests pass. ruff clean. mypy clean across 353 files. Co-Authored-By: Claude Opus 5 --- plugins/system_prompt/plugin.py | 35 ++++++++++++++++++++++----------- pyproject.toml | 14 +++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/plugins/system_prompt/plugin.py b/plugins/system_prompt/plugin.py index 58a682a..2b8c046 100644 --- a/plugins/system_prompt/plugin.py +++ b/plugins/system_prompt/plugin.py @@ -30,6 +30,8 @@ from __future__ import annotations from backend.canvas import SceneDocument +from backend.domain.node_access import is_node_of +from backend.domain.node_states import NoteState from backend.plugin_sdk import HostContext, PluginRunContext @@ -46,17 +48,26 @@ def _execute( # the note -> root edge _resolve_branch_system_prompt looks for. return _create_pending(document, run_ctx) root = document.get_branch_root(parent_node_id) - existing = next( - ( - document.nodes[edge.source] - for edge in document.edges.values() - if edge.target == root.id - and edge.source in document.nodes - and document.nodes[edge.source].kind == "note" - and document.nodes[edge.source].state.is_system_prompt - ), - None, - ) + if root is None: + # get_branch_root returns None only for an id it does not know, and + # the guard above already established that this one is in + # document.nodes - so this cannot fire today. It is here because the + # alternative is an AttributeError on the next line if that ever + # stops being true, and because the plugin already has a sensible + # answer for "no usable parent": make the prompt unattached. + return _create_pending(document, run_ctx) + # A loop rather than the generator this used to be: the kind check and + # the field read have to happen on the SAME bound node for either a + # reader or a checker to see that the second follows from the first. + # Identical result - the first match in edge order, or None. + existing = None + for edge in document.edges.values(): + if edge.target != root.id or edge.source not in document.nodes: + continue + candidate = document.nodes[edge.source] + if is_node_of(candidate, "note", NoteState) and candidate.state.is_system_prompt: + existing = candidate + break if existing is not None: return existing.id @@ -85,7 +96,7 @@ def _create_pending( ( node for node in document.nodes.values() - if node.kind == "note" + if is_node_of(node, "note", NoteState) and node.state.is_system_prompt and not any(edge.source == node.id for edge in document.edges.values()) ), diff --git a/pyproject.toml b/pyproject.toml index ae1e480..f788c27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -389,7 +389,21 @@ files = [ "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