Skip to content

U9 — Contracts & verticals (victor-contracts/, verticals/*)

331 modules / 109,763 LOC / 673 churn commits (12m). Reviewer: parallel unit review, 2026-09-03.

Overview

The contracts↔verticals split is architecturally real (bidirectional AST guards, optional victor-ai deps, lazy PEP 562 bridges) but under-enforced: make check-vertical-boundaries passes while 15+ production lazy imports of host internals (victor.config/tools/storage/observability) evade the auditor's prefix list. PluginContext is accreting (20 methods, 4 deprecated) with capability negotiation done by hasattr probes, and the register(context) consolidation seam is bypassed by 19 sidecar entry-point groups. The 4 small verticals are ~73% template-copied scaffolding whose dedup abstraction (victor/contrib) sits on the wrong side of the boundary. victor-coding is well-subpackaged but carries 3-4k-line tool monoliths.

Findings

ID Sev Location Finding Why it matters Fix sketch Effort
F1 P1 victor/core/verticals/contract_audit.py:16-28; verticals/victor-coding/victor_coding/tools/code_search_tool.py:692,1108,2674,3157; graph_tool.py:1451; victor-rag/victor_rag/document_store.py:643 Boundary audit forbids only victor.framework/core/security/agent/workflows/providers — omits victor.config, victor.tools, victor.storage, victor.observability. Vertical production code lazily imports all four; audit PASSES all 5 verticals. numpy declared in verticals "Required by victor.storage.embeddings" (victor-coding/pyproject.toml:39) The "law" is under-enforced; verticals already depend on unaudited host internals, so independent release of victor-ai can break them at runtime Extend FORBIDDEN_RUNTIME_IMPORT_PREFIXES; add contracts bridges (config/settings/search) for each legit need; migrate imports M
F2 P1 victor-coding/victor_coding/codebase_analyzer.py:3239,2903 Vertical imports host private symbol _get_or_build_index from victor.tools.code_search_tool — a deprecated placeholder stub in the host (victor/tools/code_search_tool.py:13) Import is dead or fails-then-fallbacks silently; private-symbol coupling across the seam Replace with capability lookup or move index access into the vertical S
F3 P2 victor-contracts/victor_contracts/discovery.py:553-611,638 _CollectingPluginContext implements only 13 of 20 PluginContext methods; collect_verticals_from_candidate calls plugin.register(context) at discovery — a plugin calling register_safety_rule/register_ccg_builder/register_mcp_server without hasattr probing raises AttributeError and the vertical is silently dropped SDK's own discovery context violates its own protocol; third-party plugins break in a way first-party plugins only survive by probing (plugins.py:206 admits "plugins should probe with hasattr()") Make _CollectingPluginContext a total no-op implementation of every method; ideally a Protocol the SDK typechecks against S
F4 P2 victor-contracts/victor_contracts/core/plugins.py:65-135,200-206 PluginContext grab-bag: 20 register/get methods, 4 deprecated; consolidation methods added so plugins self-register via register(context) — yet all 5 verticals still register via sidecar groups (19 distinct entry-point groups; victor.team_spec_providers at victor-coding/pyproject.toml:114 vs victor.framework.teams.providers at victor-rag/pyproject.toml:69 — two groups, same concept) Interface drift: the consolidation design exists but is unused; two team groups must both be consumed forever (victor/framework/providers/protocol.py:39-40) Finish S4 consolidation: fold sidecar groups into register(context); delete one team group after a deprecation cycle M
F5 P2 victor/contrib/ (38 modules, 4,923 LOC); scripts/migrate_vertical_to_contrib.py Dedup base classes (BaseSafetyExtension, BaseModeConfigProvider, BaseConversationManager) live in the host's victor/contrib — importable by verticals only via boundary violation (4 host files consume them). The 4 small verticals each carry template-copied safety/mode_config/conversation/handlers modules — devops is 4,853/6,654 LOC (73%) in 13 copied module names; plugin.py files byte-identical modulo name The abstraction that would delete ~12k LOC of duplication is unreachable by the packages that need it; migrate_vertical_to_contrib.py can never apply to extracted verticals Promote the contrib bases into victor_contracts.verticals (definition-layer safe), then migrate verticals onto them M/L
F6 P2 tests/unit/core/verticals/test_external_vertical_import_boundaries.py:43-68,175 Three guards define three different boundaries: this test allows victor.framework.*, victor.tools.*, victor.security.* and only checks 3 verticals (devops/rag/dataanalysis unguarded); test_contracts_import_boundaries.py bans 10 prefixes but only for assistant.py/plugin.py; contract_audit.py bans 6 prefixes for all files No single source of truth; a violation only needs to miss one guard Derive all three from one shared prefix table (e.g. victor_contracts.constants); include all 5 in-repo verticals in the test S
F7 P2 victor-coding/victor_coding/tools/graph_tool.py:1005-1388 (4,087 LOC, fan_in=0); code_search_tool.py (3,929); codebase_analyzer.py (3,504) victor-coding tools re-implement graph machinery (own GraphAnalyzer with pagerank/centrality/shortest_path) plus index management, side by side with victor-codegraph and host graph services 12k LOC of highest-churn code in three monoliths; two graph implementations drift (per-repo backend marker fixes #886/#908 touched exactly this seam) Split tool modules into schema/validation/analyzer layers; fold duplicate graph algorithms onto victor-codegraph L
F8 P3 victor-contracts/victor_contracts/core/types.py:1089,1052-1140 VerticalDefinition.skills: List[Any] untyped in the frozen manifest; mutable dataclass with heavy __post_init__ normalization; definition_version supported range exactly "1.0"–"1.0" (types.py:24-25) — forward-compat path never exercised; no pydantic, serialization hand-rolled Untyped fields accrete into the "stable" contract silently Type skills, split normalization into a constructor fn, add a 1.1-manifest round-trip test M
F9 P3 victor-rag/victor_rag/demo_sec_filings.py (998 LOC, fan_in=0), demo_docs.py; victor-dataanalysis/handlers.py (971, fan_in=0) Demo/exemplar code shipped inside the installed vertical packages Wheel size, audit surface, reader confusion Move demos to verticals/*/examples (already exists) S
F10 P3 victor-contracts/discovery.py:638 + victor/core/bootstrap.py:781 Every plugin's register() executes at least twice per process — once with the no-op discovery context, once with the real HostPluginContext; register_tool during discovery silently discards (discovery.py:559) Duplicated side effects at import time; silent no-op registration can mask plugin bugs Split register (declarative) from activate (effectful), or pass a mode flag M
F11 P3 victor/framework/entry_point_registry.py vs 23 entry_points() scan sites in 14 host modules A cached facade exists but isn't universally used; boot triggers multiple full metadata scans Cold-start amortization; groups added in one place may be missed by other scanners Route all scans through entry_point_registry with lru_cache M
F12 P3 victor-coding/victor_coding/compat/settings.py:26-50 vs code_search_tool.py:692, query_enhancer (victor-rag) :191, tools/query.py:345 The correct contract-first fallback pattern exists (compat/settings.py resolves contracts → host → minimal) but sibling modules bypass it and import victor.config.settings directly Internal drift within one vertical; good pattern 3 users, bad one 7 Make compat/settings the single accessor; lint-ban direct victor.config imports in verticals S
F13 P3 victor/core/plugins/context.py (802 lines) HostPluginContext has broad except Exception per register method (105-297) — errors logged and swallowed Failure isolation works but silently-degraded registration is hard to debug Structured registration_errors report surfaced via health_check/CLI S

Co-design opportunities

  1. Single boundary manifest: publish the forbidden/allowed prefix table from victor_contracts.constants; contract_audit.py + both guard tests import it — closes F1/F6 in one move and gives external vertical authors the same linter locally (victor-contracts check already exists).
  2. Contracts-side settings/config bridge: victor_contracts.config_runtime exposing get_project_paths/load_settings lazily, generalizing victor-coding's compat/settings.py chain; removes the largest class of victor.config leaks.
  3. Version-declared capability negotiation: replace hasattr probing with the existing CapabilityContract.min_sdk_version + CURRENT_API_VERSION=2 machinery — the seam exists, it's just not what the hot path uses.
  4. Promote contrib bases to contracts (F5): the single highest-leverage dedup; scaffold_vertical.py should emit contract-based implementations, not copies.
  5. Bridge drift CI: a test asserting every _LAZY_IMPORTS target resolves against the current host (15 bridges × ~10 symbols) — today a host rename breaks verticals only at runtime AttributeError.
  6. Team-spec group unification: deprecate one of the two competing entry-point groups per the CONTRACT_STABILITY policy.

Keep intact

  • Lazy PEP 562 bridges (_LAZY_IMPORTS + module __getattr__): module import stays host-free (measured 0.125s import, 70 modules) — the right pattern for the host-required tier.
  • victor_sdk meta-path alias (victor_sdk/init.py:44-66): aliases the same module objects, preserving isinstance identity across old/new names; deprecation warnings verified firing.
  • Bidirectional guards with stale-baseline enforcement (test_core_vertical_import_boundary.py:229, dynamic-import detection :85) — core↔vertical decoupling genuinely held at zero violations.
  • victor-ai as optional runtime extra in all 5 verticals + audit error on required victor-ai dep — keeps verticals publishable standalone.
  • graph_tool.py start-time bridge validation (:39-55) — good degradation behavior for host-version skew.

Quick metrics

  • contracts: 109 modules / 15,898 LOC (+82 victor_sdk shim); import victor_contracts = 0.125s / 70 modules; 15 stable + 6 deprecated bridges; 76 dataclasses, 0 pydantic.
  • verticals: coding 132 modules / 63,472 LOC (+20,491 test); rag 37/12,528; devops 18/6,654; research 17/5,682; dataanalysis 18/6,244.
  • devops: 73% of LOC in 13 template-copied module names; 4 of 5 plugin.py byte-identical modulo name.
  • 19 distinct entry-point groups across verticals; 2 competing team-spec groups; entry_points() scanned at 23 sites in host.
  • Boundary audit: all 5 in-repo verticals PASS while 15+ lazy host-internal imports evade the forbidden list.
  • victor/contrib: 38 modules / 4,923 LOC, consumed by only 4 host files, 0 verticals.
  • Largest victor-coding modules: graph_tool 4,087; code_search_tool 3,929; languages/plugins/additional 3,510; codebase_analyzer 3,504; codebase/indexer 3,427.