Plugin Dependencies
A plugin can declare ordered dependencies on other plugins. The loader resolves them topologically before falling back to the existing priority integer for tie-breaks within each layer.
See also: Plugin Distribution, Backend Hooks.
Why
Hook order is observable: an onContextReady handler that reads branch_overrides only sees a useful value if branch_switching has already populated it. Before this field, ordering was coupled to a single global priority: int shared by every plugin — collisions were silent and forced magic numbers.
Declaring dependencies: ["branch_switching"] makes the constraint explicit, self-documenting, and verified at load time.
Where to Declare
Two equivalent surfaces:
1. Distributable Plugin (plugin.yaml)
name: my_plugin
version: 1.0.0
has_backend: true
entry_module: my_plugin.plugin
entry_class: MyPlugin
priority: 50
dependencies:
- branch_switching
- permission_bridge
The dependencies field is part of PluginPackageManifest (src/codumentor/plugins/package.py). Default is [] — existing manifests need no change.
2. Built-in or Inline (meta)
class MyPlugin:
meta = {
"name": "my_plugin",
"version": "1.0.0",
"priority": 50,
"dependencies": ["branch_switching"],
}
The loader reads meta["dependencies"] for in-tree plugins. Same semantics as plugin.yaml.
3. Per-deployment Override (codumentor.yaml)
plugins:
- module: codumentor.plugins.my_plugin
class: MyPlugin
dependencies: ["branch_switching"]
A dependencies entry in codumentor.yaml overrides whatever the plugin's meta (or external manifest) declared. Use this sparingly — it lets a deployment fix a load-order bug without rebuilding the plugin, but it also drifts from upstream.
Resolution Rules
- Topological sort first. Each plugin appears after every plugin it depends on, transitively.
- Priority within a layer. Plugins whose dependencies are all satisfied at the same point sort by
priorityinteger (lower number runs first —priority: 4registers beforepriority: 6), preserving the pre-existing behaviour for plugins that don't declaredependencies. - Names match
meta["name"]. A dependency string"branch_switching"must match another loaded plugin'smeta["name"](or external manifestname).
Failure Modes
The two failures are treated differently, and the difference matters when you
decide whether to declare an edge at all:
| Situation | Severity | What happens |
|---|---|---|
| Declared dependency is not loaded | Soft — startup continues | The edge is dropped and the loader logs Plugin 'my_plugin' declares dependency 'foo', which is not among the loaded plugins; ignoring (plugin will load at default ordering for its layer). Your plugin still loads, ordered only by priority within its layer. |
| Dependency graph has a cycle | Fatal — startup aborts | ValueError: Plugin dependency cycle detected: a -> b -> a. Break the cycle by removing one edge (often one direction was unintended). |
A missing dependency is not fatal. Declaring an edge on a plugin a given
deployment happens not to run costs you a warning line, not a failed startup —
so declaring the edge is the right call whenever the ordering genuinely matters
to you. What you do not get is a guarantee: the dependency being absent is
exactly the case where your handler runs without the state it wanted.
So an edge buys you ordering, not presence. Pair it with a runtime check for
anything you actually read — peek at ctx["scratchpad"] for the key the other
plugin populates, or ask a collect point,
and degrade gracefully when the answer is empty.
When Not to Use
- Pure event listeners with no shared state. If your hook does its work entirely from
payload, ordering doesn't matter — leavedependenciesempty and letprioritydecide. - Presence requirements.
dependenciescannot express "X must be installed" — a missing entry is only a warning. If your plugin is unusable without X, check for it at first use and fail loudly there; the loader will not do it for you.
Optional integrations are not on this list. If your plugin works fine without
X but must run after X when X is present, declaring the edge is correct: you get
the ordering where X exists and a warning where it doesn't.
Example: file_links → branch_switching (deferred)
file_links will eventually merge with the permalink rewrite pipeline so locally-modified or generated files get correct download links. That merger consumes branch_overrides from branch_switching, so the merged plugin will declare:
meta = {
"name": "file_links",
"dependencies": ["branch_switching"],
}
Today file_links runs with dependencies: [] simply because the consumer code
isn't wired up yet: there is nothing to order against. The edge can be added the
moment the consumer lands — deployments that run without branch_switching
(selfdev configs commonly do) will log one warning and carry on, so nothing has
to wait for universal availability.