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¶
- 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 checkalready exists). - Contracts-side settings/config bridge:
victor_contracts.config_runtimeexposing get_project_paths/load_settings lazily, generalizing victor-coding's compat/settings.py chain; removes the largest class ofvictor.configleaks. - Version-declared capability negotiation: replace hasattr probing with the existing
CapabilityContract.min_sdk_version+CURRENT_API_VERSION=2machinery — the seam exists, it's just not what the hot path uses. - Promote contrib bases to contracts (F5): the single highest-leverage dedup; scaffold_vertical.py should emit contract-based implementations, not copies.
- Bridge drift CI: a test asserting every
_LAZY_IMPORTStarget resolves against the current host (15 bridges × ~10 symbols) — today a host rename breaks verticals only at runtime AttributeError. - 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
runtimeextra 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.