Skip to content

Commit f81e378

Browse files
dovvnloadingclaude
andauthored
Put the last 14 files under the type gate, and fix what they were hiding (#418)
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 <noreply@anthropic.com>
1 parent 81ea12f commit f81e378

2 files changed

Lines changed: 37 additions & 12 deletions

File tree

‎plugins/system_prompt/plugin.py‎

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
from __future__ import annotations
3131

3232
from backend.canvas import SceneDocument
33+
from backend.domain.node_access import is_node_of
34+
from backend.domain.node_states import NoteState
3335
from backend.plugin_sdk import HostContext, PluginRunContext
3436

3537

@@ -46,17 +48,26 @@ def _execute(
4648
# the note -> root edge _resolve_branch_system_prompt looks for.
4749
return _create_pending(document, run_ctx)
4850
root = document.get_branch_root(parent_node_id)
49-
existing = next(
50-
(
51-
document.nodes[edge.source]
52-
for edge in document.edges.values()
53-
if edge.target == root.id
54-
and edge.source in document.nodes
55-
and document.nodes[edge.source].kind == "note"
56-
and document.nodes[edge.source].state.is_system_prompt
57-
),
58-
None,
59-
)
51+
if root is None:
52+
# get_branch_root returns None only for an id it does not know, and
53+
# the guard above already established that this one is in
54+
# document.nodes - so this cannot fire today. It is here because the
55+
# alternative is an AttributeError on the next line if that ever
56+
# stops being true, and because the plugin already has a sensible
57+
# answer for "no usable parent": make the prompt unattached.
58+
return _create_pending(document, run_ctx)
59+
# A loop rather than the generator this used to be: the kind check and
60+
# the field read have to happen on the SAME bound node for either a
61+
# reader or a checker to see that the second follows from the first.
62+
# Identical result - the first match in edge order, or None.
63+
existing = None
64+
for edge in document.edges.values():
65+
if edge.target != root.id or edge.source not in document.nodes:
66+
continue
67+
candidate = document.nodes[edge.source]
68+
if is_node_of(candidate, "note", NoteState) and candidate.state.is_system_prompt:
69+
existing = candidate
70+
break
6071
if existing is not None:
6172
return existing.id
6273

@@ -85,7 +96,7 @@ def _create_pending(
8596
(
8697
node
8798
for node in document.nodes.values()
88-
if node.kind == "note"
99+
if is_node_of(node, "note", NoteState)
89100
and node.state.is_system_prompt
90101
and not any(edge.source == node.id for edge in document.edges.values())
91102
),

‎pyproject.toml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,21 @@ files = [
389389
"graphlink_token_estimator.py",
390390
"graphlink_version.py",
391391
"graphlink_wire_schema.py",
392+
# 2026-09-05: the three directories a coverage check found sitting outside
393+
# the gate - 14 files nobody was checking. plugins/ held six real errors
394+
# (an unguarded Optional and two unnarrowed node-state reads in the System
395+
# Prompt plugin); mutation_tests/ and tests_e2e/ were already clean.
396+
"mutation_tests",
397+
"plugins",
398+
"tests_e2e",
392399
]
400+
# plugins/ is eight sibling directories each containing a `plugin.py`, with no
401+
# __init__.py anywhere - the layout the plugin loader discovers by path. Without
402+
# these two, mypy maps all eight onto one module name and refuses to check any
403+
# of them ("Duplicate module named 'plugin'"). With them, the module name comes
404+
# from the path relative to the repo root, so they stay distinct.
405+
namespace_packages = true
406+
explicit_package_bases = true
393407
ignore_missing_imports = true
394408
# Check the files in `files` fully; use everything they import for type
395409
# information without reporting errors inside it. Without this, listing

0 commit comments

Comments
 (0)