Skip to content

chore(*): gate call correctness with a pinned type checker - #416

Merged
0xKT merged 13 commits into
mainfrom
chore/type_check_gate
Sep 16, 2026
Merged

0xKT merged 13 commits into
mainfrom
chore/type_check_gate

Conversation

@gloryfromca

@gloryfromca gloryfromca commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Nothing in this repo could see a signature change reach its callers. Ruff reads one file at a time, the import contracts read edges rather than calls, and the suite only covers lines it executes. The consequence was concrete: a required sandbox_home argument added to BoxliteExecutor reached plugins-dist/design-engine's render path with CI green, and a reviewer -- not a gate -- found it.

ty answers that question. This adds it as a pinned dev dependency, a make lint-types target and a CI step, with 110 of its 133 rules live: nine switched off with their measured backlog recorded beside each, fourteen shipped disabled upstream and untouched.

Getting there meant clearing 81 diagnostics. This PR folds what were three stacked PRs (#422 and #423, now closed); each rule's story is a commit of its own.

1. The gate, at 108 rules (chore(*): gate call correctness...)

21 diagnostics to clear, five of them real:

Where What
evolver/orchestrator/production.py design_of annotated as taking two parameters while its docstring, its only implementation and its one call site all use three
raven/cli/provider_commands.py dict[str, callable] -- the builtin function, not a type, so the annotation asserted nothing
raven/config/schema.py Config is a BaseSettings but declared env_prefix / env_nested_delimiter through pydantic's ConfigDict, which does not carry those keys
raven/spine/delivery.py StreamDelta.conversation_id is optional and was used as a dict[str, str] key; a delta without one stored a None entry the close path then read as "no open stream", losing that stream's done marker
research_flow/support/harness_text.py a predicate table typed its matchers as object, so nothing checked they are callable

The rest were unproven invariants and annotations wider than the code: three late-bound callables asserted where a comment used to carry the claim, one closure binding the callable it guarded instead of re-reading it, four hasattr probes that now check what the call actually needs, and a contextmanager yielding the value it just set.

2. unresolved-import (fix(*): resolve the imports the type gate could not)

21 diagnostics; three were stale module paths nothing had run into:

  • HarnessNode.current_core_version() read raven.__core_version__, a module that exists neither here nor in the wheel scripts/build_core_wheel.py produces. The read sat inside try/except ImportError, so the except branch was the only branch and every node store.py saved recorded core_version="unknown". It reads raven.__version__ now and returns 0.1.13.
  • raven/agent/loop/wiring.py imported SkillForgeRouterConfig from raven.config.schema; it lives in raven.config.raven. Under TYPE_CHECKING, so the annotation simply meant nothing.
  • three scripts imported raven.context_engine.base and raven.memory_engine.base, neither of which exists. Two failed at module import -- running them was not possible.

The other eighteen were the environment: the three agent products keep their plugin package beside them, so their roots are declared in [tool.ty.environment]; lxml ships a compiled etree with no type information, so lxml-stubs is a dev dependency (real types rather than five silenced lines); uno and com.sun.star.* are suppressed per line, since that worker runs inside LibreOffice's own interpreter.

3. invalid-return-type (fix(*): make the declared return types true)

39 diagnostics, and one stale annotation held eight of them: run_turn and the _run_turn behind it were declared -> LoopOutcome while all three of their returns build a TurnOutcome -- a different dataclass with no field in common. Nothing broke, because the spine worker reads what it actually gets and LoopOutcome never leaves _run_agent_loop. The annotation was the only thing wrong.

The other 31: nine annotations narrower than the code (five return tuple(<genexpr>) typed as fixed-arity tuples, an inner one() still declaring the two-tuple it returned before a third element was added, image_block's TypedDict), five declarations refusing a value the function returns (four tool execute overrides saying -> str while returning ToolResult), seven invariants that lived in a # type: ignore comment, and ten honest widenings.

4. What the gates then caught in this PR's own work

  • The qq factory guard passed locally only because an earlier test had left an event loop behind. Reproduced the CI state (asyncio.set_event_loop(None)), then made the test async so the loop is its own.
  • invalid-return-type, while still disabled, hid a regression introduced here: the qq factory returning the class instead of an instance. Found by diffing the rule histogram, which is why that rule is on now.
  • The diff-coverage gate named four lines in raven/routing/cache.py -- the asserts this work had just added, sitting inside a try whose except Exception would have swallowed the AssertionError and served the stale cache. _do_refresh returns its data now; the asserts, their noqa and the swallowing all go with it.

child_scope, QQChannel.start() and BenchmarkCache had no tests at all and have them now. Every one was mutation-checked.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • make lint-types -> All checks passed; 81 diagnostics cleared across the three rule sets

  • Gate proven live three times, once per rule: a missing argument, an import of a module that does not exist, and a function returning a str under -> int each turn it red, and removing the probe turns it green

  • uv run ruff check + ruff format --check over all six trees -> clean

  • uv run lint-imports -> 10 kept, 0 broken; deptry raven (all extras) -> no issues; uv lock --check -> resolved

  • make check-source-language -> exit 0; commitlint -> exit 0

  • make coverage locally: 22050 passed, 45 skipped, 4 failed; diff coverage 92.59%

  • All four failures are pre-existing on main, checked by a real checkout in the same environment where they fail identically: two test_the_products_tool_face_* launcher tests, test_a_ceiling_as_large_as_the_window_still_leaves_room_for_history (reserved_tools = 6044 > 6_000), and test_a_missing_key_is_said_once_rather_than_per_query (environment-dependent: it asserts SERPER_API_KEY is absent)

  • Rule counts measured with both --extra dev and --all-extras -- identical, so this gate's verdict does not depend on which optional packages are installed

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

Behaviour changes are confined to the fixes listed above, and each replaces an error path with a correct one: nodes are stamped with the real raven version instead of "unknown", two scripts that could not start now start, the stream bookkeeping no longer stores a None key, and a cache refresh returns what it fetched. Everything else is a declaration moved to match what the function already returned.

tests/ is out of the gate's scope (113 diagnostics under the same rules). Tests are executed, so a broken call there is already loud; this gate is for code CI does not run. Bringing them in is sized, separate work.

Rollback: revert the whole PR to drop the gate and its fixes together, or delete the [tool.ty] block, the lint-types target and its CI step to keep the fixes and drop only the gate.

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

Related Issues

N/A

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

I reviewed the full github/main...HEAD diff and the relevant callers and history for the CI/Makefile gate, every behavior-changing type cleanup, QQ construction, stream bookkeeping, provider resolution, config loading, detached upgrade launch, and the bundled agent/plugin paths. I found no concrete runtime, backward-compatibility, test-integrity, or architecture regression. No tests were weakened; the QQ factory change adds a focused regression test.

Nonblocking follow-up, kept here rather than as an open thread: the explanatory rule counts in [tool.ty] should be reconciled. The block explicitly disables eleven rules, not nine, and an all-rule scan with pinned ty 0.0.80 reports diagnostics in 21 of 133 rule categories, so the stated 124 of 133 baseline is not reproducible from the checked-in configuration. This does not change the gate that CI actually executes.

Verification: the exact frozen command behind lint-types passed; 934 focused tests across all touched behavior areas passed; Ruff check and format, all 10 import contracts, deptry, uv lock --check, source-language validation, and git diff --check passed. The local make lint-types wrapper itself could not be invoked because this environment has no make, so its exact uv recipe was run directly. GitHub's Python lint job is also green; the unit job was still running when this review was posted.

Coverage included AGENTS.md/the linked CLAUDE.md, CONTEXT-MAP.md and the Runtime architecture terms, the diff, callers, relevant history, backward compatibility, test weakening, and architecture boundaries.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

No new findings on this revision. The comment-only delta makes the live-rule arithmetic reproducible: 118 upstream-live rules, minus eleven local disables, plus blanket-ignore-comment, equals 108. It does not alter the configured gate or any runtime behavior. The previously recorded wording follow-up remains nonblocking and is not repeated here.

Verification on this head: the exact frozen ty command passed, all 934 focused tests across the behavior-changing areas passed, and git diff --check passed. GitHub's Python lint, pre-commit, commit-message, repository-file, Windows self-upgrade, kernel-wheel, TUI, UI RPC, page, and bridge checks are green; the unit job was still running when this review was posted.

This round covered the new delta, the full PR diff, the applicable AGENTS.md/linked CLAUDE.md and Runtime context constraints, callers and relevant history from the prior reviewed revision, backward compatibility, test integrity, and architecture boundaries.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

No new findings on this revision. The comment-only delta was checked against pinned ty 0.0.80: its diagnostic totals, mixin/optional breakdown, and cited override/import examples are reproducible, and no rule severity or runtime behavior changed. The previously recorded wording follow-up remains nonblocking and is not repeated here.

Verification on this head: the exact frozen ty gate passed, all 934 focused tests across the behavior-changing areas passed, and git diff --check passed.

This round covered the new delta, the full PR diff, applicable repository and Runtime context rules, the relevant checker output and history, backward compatibility, test integrity, and architecture boundaries. No review threads from this reviewer are open.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

No new findings on this revision. The delta only makes the QQ factory regression test run inside pytest's configured async loop; this removes dependence on ambient thread-loop state while preserving the behavior assertion, and it changes no production path. The existing nonblocking item is unchanged and is not repeated here.

Verification on this head: the exact frozen ty gate passed, all 35 tests in tests/test_channels_qq.py passed, source-language validation passed, and git diff --check passed.

This round covered the new delta, the full PR diff and record, applicable repository and Runtime context rules, the test's caller and history, backward compatibility, test integrity, and architecture boundaries. No review threads from this reviewer are open.

gloryfromca and others added 4 commits September 15, 2026 16:59
Nothing in this repo could see a signature change reach its callers. Ruff reads
one file at a time, the import contracts read edges rather than calls, and the
suite only covers lines it executes -- so a required argument added to
BoxliteExecutor reached a bundled plugin's render path with CI green, and a
reviewer, not a gate, found it.

ty answers exactly that question, and 114 of its 133 rules already hold here.
Nine are switched off with their measured backlog recorded beside them; the
rest are on, which left 21 diagnostics to clear.

Of those 21, five were real:

- evolver design_of was annotated as taking two parameters while its docstring,
  its only implementation and its one call site all use three;
- _LOGIN_HANDLERS was annotated dict[str, callable] -- the builtin function, not
  a type, so the annotation asserted nothing;
- Config is a BaseSettings but declared env_prefix and env_nested_delimiter
  through pydantic's ConfigDict, which does not carry those keys;
- StreamDelta.conversation_id is optional and was used as a dict[str, str] key:
  a delta without one stored a None entry that the close path then read as "no
  open stream", losing the stream's done marker;
- a permissive matcher table typed its predicates as object, so nothing checked
  that they are callable.

The rest were unproven invariants or annotations wider than the code. Three
late-bound callables are now asserted where a comment used to carry the claim;
one closure binds the callable it guarded instead of re-reading the attribute;
four hasattr probes became callable(getattr(...)), which is what the call
actually needs; the child_scope contextmanager yields the value it just set
rather than re-reading it through an optional getter.

One suppression is left, with the reason inline: a per-kind spec column whose
element shape is not provable without splitting the table.

tests/ is out of scope (113 diagnostics under the same rules). Tests are
executed, so a broken call there is already loud; this gate is for code that
CI does not run.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The block said 124 of 133 rules are on. That counted the fourteen ty ships
disabled -- the unsound-* family, division-by-zero, the possibly-* family --
as enabled. They are not: they are unmeasured, which is a different claim from
satisfied. Live is 108, and the comment now accounts for all 133.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
The counts were right and two of the characterisations were not. The
unresolved-attribute note said "mostly third-party without type information";
it is 432 agent-loop mixin attribute reads and 180 optional dereferences, with
almost no third-party in it. The invalid-argument-type note asserted a cause
that was never sampled.

Each entry now says what a reader would find if they opened it, and which ones
are worth clearing: invalid-method-override is tool subclasses renaming a base
parameter that a model-facing schema passes by keyword, and unresolved-import
holds eight stale module paths.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…state

botpy.Client.__init__ calls asyncio.get_event_loop(). Outside a running loop
that answers from thread-global state: on this machine it warns and creates
one, on CI it raised, because whatever ran before had left no current loop.
The test passed locally for a reason that was never the test's own.

Async instead, so asyncio_mode = "auto" runs the body inside a loop and the
call is answered by that loop. Reproduced the CI state first
(asyncio.set_event_loop(None), then build the bot -- same RuntimeError), and
the async form survives it. Still red when the factory hands back the class.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
gloryfromca and others added 4 commits September 15, 2026 17:15
The diff gate reported 73% on the changed lines, and it was right: three of the
four spots this change touched had no test behind them.

child_scope had none at all. It is how the subagent probe hangs its spans under
one it does not own, so the contract worth pinning is that the caller's context
comes back exactly as it was -- a loan, not an assignment -- and that an entry
point with no ambient context still gets a usable one instead of a reparented
None. Both go red if the reset is dropped.

QQChannel.start() had no test either. The factory has one; this covers the seam
where its return becomes self._client and then the object the connect loop
awaits, which is the line a wrong return type lands on.

The gateway's two broker-wiring sites are inside a 258-line nested function in
a 620-line command, and no test reaches serve at all. Rather than leave them
uncovered, they now read `callable(getattr(tool := ..., "set_broker", None))`
and call `tool.set_broker(...)`, which satisfies the checker the same way while
naming the object being wired at the call. main.py's site takes the same shape,
so the idiom is written once.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…le on

Three of the twenty-one unresolved imports were stale module paths that nothing
had ever run into:

- evolver's HarnessNode read `raven.__core_version__`, a module that exists
  neither here nor in the wheel scripts/build_core_wheel.py produces. The read
  sat inside try/except ImportError, so the except branch was the only branch
  and every node store.py saved recorded core_version "unknown". The docstring
  already said where the value comes from -- `raven` version -- so it now reads
  that, and returns 0.1.13 instead.
- raven/agent/loop/wiring.py imported SkillForgeRouterConfig from
  raven.config.schema; it lives in raven.config.raven. Under TYPE_CHECKING, so
  nothing raised -- the annotation just meant nothing.
- three scripts imported raven.context_engine.base and raven.memory_engine.base,
  neither of which exists. Two of them failed at module import: running them was
  not possible. AssemblyContext is in raven.contracts.context and TokenBudget in
  raven.contracts.assembled.

The other eighteen were the environment, not the code. The three agent products
keep their plugin package beside them rather than under plugins-dist, so their
roots are declared in [tool.ty.environment] the way plugins-dist is declared in
ruff's src. lxml ships a compiled module with no type information, so lxml-stubs
is a dev dependency now -- real types rather than five silenced lines, though it
does surface one stub imprecision (itertext is typed as yielding str or bytes
because one element API serves byte-mode trees), suppressed by rule name where
it reads. The UNO bridge imports are suppressed per line: that worker runs
inside LibreOffice's own interpreter and is never imported by the host.

unresolved-import comes off the ignore list, so the gate is 109 of ty's 133
rules. Proven live: an import of a module that does not exist turns
`make lint-types` red and removing it turns it green.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Both notes were written from one sample each and both were wrong about the
shape of the backlog.

invalid-method-override was called the highest-harm family. Enumerated, 53 of
its 66 are Tool subclasses naming their parameters against a base that takes
only `**kwargs`, which is how a tool publishes its schema; 9 are wrappers that
forward through `**kwargs`; 3 rename a parameter together with the schema key
it maps to. One is real, and it is a kernel contract question rather than a
call site: LLMProvider.chat_stream is declared as a coroutine returning an
async iterator while every implementation is an async generator. Turning the
rule on would mean suppressing 65 lines to keep that one.

invalid-return-type is worth clearing and the note now says what clearing it
costs: about half are annotations narrower than the code, half are signatures
that refuse a value the function can return.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
invalid-return-type was off with 39 diagnostics. One stale annotation held
eight of them: AgentLoop.run_turn and the _run_turn behind it were declared
`-> LoopOutcome` while all three of their returns build a TurnOutcome, which is
a different dataclass with no field in common. Nothing broke, because the spine
worker reads `.usage` and `.explicit_reply` off what it actually gets;
LoopOutcome never leaves _run_agent_loop, whose own consumer reads `.status`
and `.checkpoint_id`. The annotation was the only thing that was wrong.

Three more were annotations narrower than the code:

- five `return tuple(<genexpr>)` typed as fixed-arity tuples. `tuple(...)` over
  a generator is `tuple[T, ...]`, so each now unpacks to the arity it declares.
  A length that does not match raises where it used to build a wrong-shaped
  tuple, and in `_as_size` that lands inside the ValueError the caller already
  catches.
- an inner `one()` in the deck reviewer still declared the two-tuple it
  returned before a third element was added.
- image_block returns the ImagePart TypedDict, which is not assignable to a
  bare dict; the two callers that wrap it say ImagePart now, so the key names
  are checked at their call sites too. The note in raven/utils/images.py saying
  CI runs no type checker is no longer true, and no longer says it.

Five were declarations refusing a value the function returns: four tool
`execute` overrides said `-> str` while returning ToolResult on their error
paths (the base is `str | ToolResult`), and a sentinel helper said `MenuReply`
while returning None on two paths its only caller already types as optional.

Seven were invariants that lived in a trailing `# type: ignore` comment -- the
chain is never empty, _do_refresh sets it or raises, the constructor refuses an
empty key. Those are asserts now, stated where both a reader and the checker
meet them, each with the noqa that says why an assert is the right shape.

The rest are honest widenings: a shaping dict that really does carry a None, a
delivered path that really can be absent, an api-key source that really is
`str | None`, and an onboarding prompt that really can return the untyped
`_BACK` sentinel -- the same `Any` its four siblings already declare. Giving
that sentinel a type of its own is a separate change across twelve sites.

The gate is now 110 of ty's 133 rules. Proven live: a function returning a str
under `-> int` turns `make lint-types` red, and removing it turns it green.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

No new findings on this revision. The rebase onto the sharded-test main is clean, the added QQ and tracing tests cover the new behavior, and restoring the broker call expressions to their pre-existing form leaves only the stricter callable guards as the production delta. The existing nonblocking item is unchanged and is not repeated here.

Verification on this head: the exact frozen ty gate passed; 189 targeted QQ, tracing, deep-research, and gateway tests passed; Ruff on the touched delta, source-language validation, and git diff --check passed. GitHub's four unit shards, combined coverage gate, Python lint, pre-commit, commit checks, Windows upgrade, kernel wheel, TUI, UI RPC, page, and bridge checks are all green.

This round covered the rebase range-diff, new commit and full PR diff, applicable repository and Runtime context rules, callers and history, backward compatibility, test integrity, coverage accounting, and architecture boundaries. No review threads from this reviewer are open.

…h routes

The two spots the diff gate named were the asserts this change had just added,
and they were the wrong shape twice over. _do_refresh sets self._data and
returns None, so the caller had to read the attribute back and claim it was
not None -- and the claim sat inside the try, where an AssertionError would
have been swallowed by the `except Exception` below it and served the stale
copy instead of failing.

_do_refresh returns the data it fetched now, so the caller returns that. Two
asserts, two noqa and the swallowing all go with it.

BenchmarkCache had no test. Its two refresh-success routes decide whether a
router sees today's numbers or yesterday's, so they are covered now, with the
failed-refresh route beside them as the contrast that says why the disk copy
is a fallback and not the answer. The stale-route test goes red if the refresh
is skipped.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the new d5f0588f82eb...f64bff7be1db delta and rechecked the full github/main...HEAD change against AGENTS.md/CLAUDE.md and the CONTEXT architecture contracts, including callers and history, backward compatibility, and whether tests were weakened. The two newly enabled rules (unresolved-import and invalid-return-type) are cleared with corrections that preserve the runtime contracts; in particular, _do_refresh() now returns its fetched value directly to both foreground callers while its background caller safely ignores that value. The gate accounting now matches the configuration at 110 live rules and nine locally disabled rules. The delta only adds tests rather than weakening existing coverage.

Verification: the exact pinned ty gate passed locally; 519 focused routing, evolver, loop, CLI, QQ, and tracing tests passed; and Ruff passed on the newly changed implementation/tests. GitHub's four unit shards, coverage gates, Python lint/type checks, kernel-wheel smoke, repository checks, TUI/page/RPC checks, and Windows upgrade check all pass. The red Bridge job is unrelated to this diff: its TypeScript build passed, then npm's retiring audit endpoint returned 400 Invalid package tree; bridge sources and lockfile are unchanged. A local bridge build could not start because this checkout has no bridge dependencies installed (tsc was absent), so the successful CI compile is the meaningful build result. No review threads are open.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

This revision only merges the current target branch. I checked the refreshed github/main...HEAD diff, the merge against both parents, commit history/range-diff, and the stable patch ID: all nine PR commits and the complete PR patch are unchanged, with no conflict-resolution delta. That also preserves the prior conclusions on callers, backward compatibility, test strength, AGENTS.md/CLAUDE.md rules, and the CONTEXT architecture contracts.

On the merged tree, the exact pinned ty gate passed and the focused routing, evolver, loop, CLI, QQ, and tracing run passed all 519 tests. CI's lint, repository, commit, bridge, kernel-wheel, page, TUI, RPC, Windows, and three unit-shard checks pass. The remaining unit shard ran all 5,429 tests successfully but failed the new idle ceiling because an upstream tests/test_rpc_model.py case recorded 3.44 seconds idle against a 3-second limit. This PR changes neither that test nor the idle-ceiling machinery, the test passes locally with --idle-ceiling-strict, and the target commit itself already has a failing unit shard; this is not a regression introduced by this revision. No review threads are open.

@gloryfromca
gloryfromca requested a review from 0xKT September 16, 2026 14:01
0xKT
0xKT previously approved these changes Sep 16, 2026

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; suggestions only, and they are marked inline.

This revision is another merge of main. I checked both parents, the refreshed github/main...HEAD diff, history/range-diff, and stable patch IDs: all nine PR commits and the full PR patch remain identical, with no conflict-resolution change. The prior review of repository rules, callers, compatibility, architecture contracts, and test strength therefore still applies; the new work here was checking interaction with the updated target.

Named nonblocking follow-up (summary-only because the affected line is untouched target code): the pinned gate now reports one unresolved-import in raven/rpc/knowledge_preview.py:34. KnowledgeDocumentRecord is imported from raven.knowledge._types, but it is defined in _records and exported from raven.knowledge. The line came from target commit 7beffb9d0f, not this PR, so this is a pre-existing issue newly exposed by the gate and does not meet the late-round blocker requirement that this change introduce the defect. Correcting that import on main or carrying the same correction here will clear the currently red Python-lint job.

Verification: the exact ty command fails only on that import; the focused merged-tree run passed all 519 tests; all four CI unit shards, coverage, and every other reported job pass. No tests were weakened and no review threads are open.

`knowledge_preview` imported KnowledgeDocumentRecord from
`raven.knowledge._types`, which does not define it; it lives in
`raven.knowledge._records`, and that is what `manager.get_document()` hands
back. The import sits under TYPE_CHECKING, so nothing raised -- the five
annotations it feeds, on `resolve`, `named`, `is_renderable`, `pdf_for` and
`_alias_for`, simply resolved to nothing.

Arrived from the target branch and caught by this PR's own gate, which is the
same shape it already cleared in `agent/loop/wiring.py`: a TYPE_CHECKING import
naming the wrong module is invisible until something reads the annotation.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

The new one-line delta resolves the target-branch interaction noted on the previous revision: KnowledgeDocumentRecord is defined in raven.knowledge._records, and that is the class returned by KnowledgeManager.get_document(). Because the import is under TYPE_CHECKING, the correction changes annotation resolution only and preserves runtime/backward compatibility. I checked the refreshed full diff, the symbol's definition and caller, commit history, AGENTS.md/CLAUDE.md and CONTEXT constraints, and test changes; no tests were weakened.

Verification: the exact pinned ty gate now passes, 49 knowledge-preview/routing/evolver tests pass locally, and the revision's Python lint/type-check CI job plus all completed non-unit jobs are green. The remaining unit shards were still running when reviewed. No review threads are open.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

This revision is another merge of current main into the branch. The range-diff preserves all 10 PR commits one-for-one, and the stable patch ID is unchanged from the clean 891316dd7c58 revision. I checked the repository rules, the full PR diff and new-main overlap, affected callers and history, backward compatibility, test-strength changes, and the existing architecture constraints; the merge introduces no new PR-side behavior or weakened tests.

Verification:

  • uv run --frozen --python 3.12 --all-extras ty check raven evolver agents plugins-dist scripts - passed
  • uv run pytest tests/test_routing_benchmark_cache.py tests/test_evolver_node_core_version.py tests/test_agent_loop_run_emit.py tests/test_cli_session_commands.py tests/test_cli_onboard_commands.py tests/test_channels_qq.py tests/test_tracing_api.py -q - 529 passed
  • git diff --check github/main...HEAD - passed

All completed GitHub checks were green when inspected; the four unit shards were still pending.

@0xKT
0xKT merged commit a018351 into main Sep 16, 2026
16 of 17 checks passed
@0xKT
0xKT deleted the chore/type_check_gate branch September 16, 2026 14:33
0xKT pushed a commit that referenced this pull request Sep 17, 2026
…ads (#463)

## Summary

`main` is red on three jobs (`python lint`, `unit 2/4`, `unit 3/4`), and
has been
since #451 merged. All of it is fallout from one arrival: `image_search`
came with a
second switch beside its credential.

`tools.web.search.images` decides whether the tool is registered at all
(`raven/agent/loop/wiring.py`), so a lane that never places a picture
keeps the tool
face it had. The capability table was not told. `is_configured` answers
on the vendor
key alone, so with a Serper key and the switch left off the table ticked
a capability
the agent does not hold, and the two tests that drive a real `AgentLoop`
caught exactly
the disagreement that module exists to prevent.

The switch belongs on `is_disabled`, not `is_configured`. That
function's own docstring
already names the reason: a switched-off tool usually has its credential
set, and
calling it unconfigured sends the deployer to set a key that is already
there.
`tools.disabledTools` and this flag are the same kind of answer, so
`is_offered` needs
no change and the doctor row reads "off" rather than "missing a key".

The test configs that mean "everything supplied" now turn the switch on,
the way
`raven/core/runtime.py` passes it from the config, and a new case pins
the combination
that was wrong: key present, pictures off, nothing offered, table saying
off.

Two more pieces of the same arrival:

- `ImageSearchTool` copied `WebSearchTool`'s key resolution without its
two attribute
annotations, so the untyped lambda `wiring.py` hands it left the
`api_key` property
returning `~AlwaysFalsy | str` against a declared `str`. The type
checker pinned in
  #416 reports it; the annotations are the fix.
- `WebSearchConfig` gained the `images` field, so the writer's validated
subtree now
carries it and two `test_config_update_tools` expectations that pin the
written
  section by value went stale.

Not changed here: `get_web_search` still reports `{provider, api_key,
max_results}` and
does not surface `images`, though `set_web_search` can write it. That is
a gap in the
read side of the config API, not a CI failure, and it wants its own
change.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run on Windows 11 in a clean worktree cut from `main` (`5a29ea22`):

- `uv run pytest tests/test_tool_capabilities.py
tests/test_config_update_tools.py -q`:
71 passed. All four tests failing on `main` now pass, plus the new case.
- `uv run --frozen --python 3.12 --all-extras ty check raven evolver
agents plugins-dist scripts`:
the `web.py:516` diagnostic is gone. The diagnostics that remain locally
are
unresolved imports for optional packages absent from this machine's
environment
(`boxlite`, `nio`); CI reported exactly one diagnostic before this
change, the one
  fixed here.
- `uv run --frozen --python 3.12 --extra dev ruff check raven tests`:
all checks passed.
- `ruff format --check` on the four changed files: already formatted.
- `uv run pytest tests/test_cli_doctor_commands.py
tests/test_rpc_console.py -q`, which
consume `is_disabled`: the failing-test set is byte-identical before and
after this
change (8 pre-existing Windows-only failures around file modes and an
absent browser
  package, none of them in the set CI reports).

## Risk

`is_disabled` gains a second reason to answer true, so a deployment with
a search key
and `tools.web.search.images` off now reads as "switched off" in `raven
doctor` and the
console capability report, where it previously read as offered. That is
the state the
loop was already in; only the description changes. Nothing about
registration, gating,
or the tool's runtime behaviour moves.

The other two changes are an annotation and two test expectations, with
no runtime
effect.

Rollback is reverting this commit; it touches nothing that other work
builds on.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
## Summary

Nothing in this repo could see a signature change reach its callers.
Ruff reads one file at a time, the import contracts read edges rather
than calls, and the suite only covers lines it executes. The consequence
was concrete: a required `sandbox_home` argument added to
`BoxliteExecutor` reached `plugins-dist/design-engine`'s render path
with CI green, and a reviewer -- not a gate -- found it.

`ty` answers that question. This adds it as a pinned dev dependency, a
`make lint-types` target and a CI step, with **110 of its 133 rules
live**: nine switched off with their measured backlog recorded beside
each, fourteen shipped disabled upstream and untouched.

Getting there meant clearing 81 diagnostics. This PR folds what were
three stacked PRs (#422 and #423, now closed); each rule's story is a
commit of its own.

### 1. The gate, at 108 rules (`chore(*): gate call correctness...`)

21 diagnostics to clear, five of them real:

| Where | What |
|---|---|
| `evolver/orchestrator/production.py` | `design_of` annotated as taking
two parameters while its docstring, its only implementation and its one
call site all use three |
| `raven/cli/provider_commands.py` | `dict[str, callable]` -- the
builtin function, not a type, so the annotation asserted nothing |
| `raven/config/schema.py` | `Config` is a `BaseSettings` but declared
`env_prefix` / `env_nested_delimiter` through pydantic's `ConfigDict`,
which does not carry those keys |
| `raven/spine/delivery.py` | `StreamDelta.conversation_id` is optional
and was used as a `dict[str, str]` key; a delta without one stored a
`None` entry the close path then read as "no open stream", losing that
stream's done marker |
| `research_flow/support/harness_text.py` | a predicate table typed its
matchers as `object`, so nothing checked they are callable |

The rest were unproven invariants and annotations wider than the code:
three late-bound callables asserted where a comment used to carry the
claim, one closure binding the callable it guarded instead of re-reading
it, four `hasattr` probes that now check what the call actually needs,
and a contextmanager yielding the value it just set.

### 2. `unresolved-import` (`fix(*): resolve the imports the type gate
could not`)

21 diagnostics; three were stale module paths nothing had run into:

- `HarnessNode.current_core_version()` read `raven.__core_version__`, a
module that exists neither here nor in the wheel
`scripts/build_core_wheel.py` produces. The read sat inside `try/except
ImportError`, so the except branch was the only branch and **every node
`store.py` saved recorded `core_version="unknown"`**. It reads
`raven.__version__` now and returns `0.1.13`.
- `raven/agent/loop/wiring.py` imported `SkillForgeRouterConfig` from
`raven.config.schema`; it lives in `raven.config.raven`. Under
`TYPE_CHECKING`, so the annotation simply meant nothing.
- three scripts imported `raven.context_engine.base` and
`raven.memory_engine.base`, neither of which exists. **Two failed at
module import** -- running them was not possible.

The other eighteen were the environment: the three agent products keep
their plugin package beside them, so their roots are declared in
`[tool.ty.environment]`; `lxml` ships a compiled `etree` with no type
information, so `lxml-stubs` is a dev dependency (real types rather than
five silenced lines); `uno` and `com.sun.star.*` are suppressed per
line, since that worker runs inside LibreOffice's own interpreter.

### 3. `invalid-return-type` (`fix(*): make the declared return types
true`)

39 diagnostics, and **one stale annotation held eight of them**:
`run_turn` and the `_run_turn` behind it were declared `-> LoopOutcome`
while all three of their returns build a `TurnOutcome` -- a different
dataclass with no field in common. Nothing broke, because the spine
worker reads what it actually gets and `LoopOutcome` never leaves
`_run_agent_loop`. The annotation was the only thing wrong.

The other 31: nine annotations narrower than the code (five `return
tuple(<genexpr>)` typed as fixed-arity tuples, an inner `one()` still
declaring the two-tuple it returned before a third element was added,
`image_block`'s TypedDict), five declarations refusing a value the
function returns (four tool `execute` overrides saying `-> str` while
returning `ToolResult`), seven invariants that lived in a `# type:
ignore` comment, and ten honest widenings.

### 4. What the gates then caught in this PR's own work

- The qq factory guard passed locally only because an earlier test had
left an event loop behind. Reproduced the CI state
(`asyncio.set_event_loop(None)`), then made the test async so the loop
is its own.
- `invalid-return-type`, while still disabled, hid a regression
introduced here: the qq factory returning the class instead of an
instance. Found by diffing the rule histogram, which is why that rule is
on now.
- The diff-coverage gate named four lines in `raven/routing/cache.py` --
the asserts this work had just added, sitting inside a `try` whose
`except Exception` would have swallowed the `AssertionError` and served
the stale cache. `_do_refresh` returns its data now; the asserts, their
noqa and the swallowing all go with it.

`child_scope`, `QQChannel.start()` and `BenchmarkCache` had no tests at
all and have them now. Every one was mutation-checked.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [x] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- `make lint-types` -> All checks passed; 81 diagnostics cleared across
the three rule sets
- Gate proven live three times, once per rule: a missing argument, an
import of a module that does not exist, and a function returning a str
under `-> int` each turn it red, and removing the probe turns it green
- `uv run ruff check` + `ruff format --check` over all six trees ->
clean
- `uv run lint-imports` -> 10 kept, 0 broken; `deptry raven` (all
extras) -> no issues; `uv lock --check` -> resolved
- `make check-source-language` -> exit 0; `commitlint` -> exit 0
- `make coverage` locally: **22050 passed, 45 skipped, 4 failed**; diff
coverage 92.59%
- All four failures are **pre-existing on `main`**, checked by a real
checkout in the same environment where they fail identically: two
`test_the_products_tool_face_*` launcher tests,
`test_a_ceiling_as_large_as_the_window_still_leaves_room_for_history`
(`reserved_tools = 6044 > 6_000`), and
`test_a_missing_key_is_said_once_rather_than_per_query`
(environment-dependent: it asserts `SERPER_API_KEY` is absent)
- Rule counts measured with both `--extra dev` and `--all-extras` --
identical, so this gate's verdict does not depend on which optional
packages are installed

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

Behaviour changes are confined to the fixes listed above, and each
replaces an error path with a correct one: nodes are stamped with the
real raven version instead of `"unknown"`, two scripts that could not
start now start, the stream bookkeeping no longer stores a `None` key,
and a cache refresh returns what it fetched. Everything else is a
declaration moved to match what the function already returned.

`tests/` is out of the gate's scope (113 diagnostics under the same
rules). Tests are executed, so a broken call there is already loud; this
gate is for code CI does not run. Bringing them in is sized, separate
work.

Rollback: revert the whole PR to drop the gate and its fixes together,
or delete the `[tool.ty]` block, the `lint-types` target and its CI step
to keep the fixes and drop only the gate.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
…ads (#463)

## Summary

`main` is red on three jobs (`python lint`, `unit 2/4`, `unit 3/4`), and
has been
since #451 merged. All of it is fallout from one arrival: `image_search`
came with a
second switch beside its credential.

`tools.web.search.images` decides whether the tool is registered at all
(`raven/agent/loop/wiring.py`), so a lane that never places a picture
keeps the tool
face it had. The capability table was not told. `is_configured` answers
on the vendor
key alone, so with a Serper key and the switch left off the table ticked
a capability
the agent does not hold, and the two tests that drive a real `AgentLoop`
caught exactly
the disagreement that module exists to prevent.

The switch belongs on `is_disabled`, not `is_configured`. That
function's own docstring
already names the reason: a switched-off tool usually has its credential
set, and
calling it unconfigured sends the deployer to set a key that is already
there.
`tools.disabledTools` and this flag are the same kind of answer, so
`is_offered` needs
no change and the doctor row reads "off" rather than "missing a key".

The test configs that mean "everything supplied" now turn the switch on,
the way
`raven/core/runtime.py` passes it from the config, and a new case pins
the combination
that was wrong: key present, pictures off, nothing offered, table saying
off.

Two more pieces of the same arrival:

- `ImageSearchTool` copied `WebSearchTool`'s key resolution without its
two attribute
annotations, so the untyped lambda `wiring.py` hands it left the
`api_key` property
returning `~AlwaysFalsy | str` against a declared `str`. The type
checker pinned in
  #416 reports it; the annotations are the fix.
- `WebSearchConfig` gained the `images` field, so the writer's validated
subtree now
carries it and two `test_config_update_tools` expectations that pin the
written
  section by value went stale.

Not changed here: `get_web_search` still reports `{provider, api_key,
max_results}` and
does not surface `images`, though `set_web_search` can write it. That is
a gap in the
read side of the config API, not a CI failure, and it wants its own
change.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run on Windows 11 in a clean worktree cut from `main` (`5a29ea22`):

- `uv run pytest tests/test_tool_capabilities.py
tests/test_config_update_tools.py -q`:
71 passed. All four tests failing on `main` now pass, plus the new case.
- `uv run --frozen --python 3.12 --all-extras ty check raven evolver
agents plugins-dist scripts`:
the `web.py:516` diagnostic is gone. The diagnostics that remain locally
are
unresolved imports for optional packages absent from this machine's
environment
(`boxlite`, `nio`); CI reported exactly one diagnostic before this
change, the one
  fixed here.
- `uv run --frozen --python 3.12 --extra dev ruff check raven tests`:
all checks passed.
- `ruff format --check` on the four changed files: already formatted.
- `uv run pytest tests/test_cli_doctor_commands.py
tests/test_rpc_console.py -q`, which
consume `is_disabled`: the failing-test set is byte-identical before and
after this
change (8 pre-existing Windows-only failures around file modes and an
absent browser
  package, none of them in the set CI reports).

## Risk

`is_disabled` gains a second reason to answer true, so a deployment with
a search key
and `tools.web.search.images` off now reads as "switched off" in `raven
doctor` and the
console capability report, where it previously read as offered. That is
the state the
loop was already in; only the description changes. Nothing about
registration, gating,
or the tool's runtime behaviour moves.

The other two changes are an annotation and two test expectations, with
no runtime
effect.

Rollback is reverting this commit; it touches nothing that other work
builds on.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
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.

2 participants