Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/registry/plugin_catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"schema_version": "strategy_plugin_catalog.v1",
"inventory_only": true,
"source_policy": "Metadata inventory only; do not claim evidence or live authority.",
"entries": [
{"plugin_id":"crisis_response_shadow","lineage":"market_regime/crisis","owner_strategy":["tqqq_growth_income"],"status":"DEFERRED","position_mutation_allowed":false,"broker_order_allowed":false},
{"plugin_id":"macro_risk_governor","lineage":"market_regime/macro","owner_strategy":["tqqq_growth_income"],"status":"DEFERRED","position_mutation_allowed":false,"broker_order_allowed":false},
{"plugin_id":"market_regime_control","lineage":"market_regime/unified","owner_strategy":["global_etf_rotation","mega_cap_leader_rotation_top50_balanced","russell_1000_multi_factor_defensive","soxl_soxx_trend_income","tqqq_growth_income"],"status":"DEFERRED","position_mutation_allowed":false,"broker_order_allowed":false},
{"plugin_id":"panic_reversal_shadow","lineage":"event_context/panic_reversal","owner_strategy":["tqqq_growth_income"],"status":"DEFERRED","position_mutation_allowed":false,"broker_order_allowed":false},
{"plugin_id":"taco_rebound_shadow","lineage":"event_context/taco_rebound","owner_strategy":["tqqq_growth_income"],"status":"DEFERRED","position_mutation_allowed":false,"broker_order_allowed":false}
]
}
65 changes: 65 additions & 0 deletions src/quant_strategy_plugins/plugin_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Deterministic inventory for strategy-plugin sidecars.

This is an inventory/matrix boundary, not a promotion gate. It deliberately
does not invent evidence digests: those are populated by a concrete run.
"""

from __future__ import annotations

from typing import Any

from .plugin_lineage import PLUGIN_LINEAGE_REGISTRY
from .plugin_policies import (
PLUGIN_COMPATIBLE_STRATEGIES,
PLUGIN_LIFECYCLE_POLICY_REGISTRY,
PLUGIN_CONSUMPTION_POLICY_REGISTRY,
)

PLUGIN_CATALOG_SCHEMA_VERSION = "strategy_plugin_catalog.v1"


def build_plugin_catalog() -> dict[str, Any]:
"""Return the sidecar inventory consumed by lifecycle matrix tooling.

``owner_strategy`` is a tuple because a sidecar may be mounted by more
than one strategy. A missing run-specific digest/expiry remains ``None``
and therefore cannot be mistaken for valid evidence.
"""
entries: list[dict[str, Any]] = []
for plugin_id in sorted(PLUGIN_LIFECYCLE_POLICY_REGISTRY):
lifecycle = PLUGIN_LIFECYCLE_POLICY_REGISTRY[plugin_id]
lineage = PLUGIN_LINEAGE_REGISTRY[plugin_id]
policies = [
policy
for (policy_plugin, _), policy in PLUGIN_CONSUMPTION_POLICY_REGISTRY.items()
if policy_plugin == plugin_id
]
position_allowed = any(policy.position_control_allowed for policy in policies)
entries.append(
{
"plugin_id": plugin_id,
"lineage": lineage["lineage"],
"owner_strategy": list(PLUGIN_COMPATIBLE_STRATEGIES.get(plugin_id, ())),
"input_digest": None,
"evidence_package_id": None,
"evidence_valid_until": None,
"bounded_budget": None,
"position_mutation_allowed": False,
"broker_order_allowed": False,
"policy_position_control_allowed": position_allowed,
"lifecycle_stage": lifecycle.lifecycle_stage,
"status": "DEFERRED",
"new_mount_allowed": lifecycle.new_mount_allowed,
"replay_only": lifecycle.replay_only,
"successor": lifecycle.successor,
}
)
return {
"schema_version": PLUGIN_CATALOG_SCHEMA_VERSION,
"inventory_only": True,
"source_policy": "Metadata inventory only; do not claim evidence or live authority.",
"entries": entries,
}


__all__ = ["PLUGIN_CATALOG_SCHEMA_VERSION", "build_plugin_catalog"]
28 changes: 28 additions & 0 deletions tests/test_plugin_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from quant_strategy_plugins.plugin_catalog import PLUGIN_CATALOG_SCHEMA_VERSION, build_plugin_catalog


def test_catalog_is_complete_inventory_and_never_grants_capital_authority() -> None:
catalog = build_plugin_catalog()

assert catalog["schema_version"] == PLUGIN_CATALOG_SCHEMA_VERSION
assert catalog["inventory_only"] is True
assert "do not claim evidence" in catalog["source_policy"]
assert len(catalog["entries"]) == 5
for entry in catalog["entries"]:
assert entry["plugin_id"]
assert entry["lineage"]
assert entry["owner_strategy"]
assert entry["status"] == "DEFERRED"
assert entry["input_digest"] is None
assert entry["evidence_package_id"] is None
assert entry["position_mutation_allowed"] is False
assert entry["broker_order_allowed"] is False


def test_catalog_keeps_approved_policy_distinct_from_runtime_authority() -> None:
entries = {entry["plugin_id"]: entry for entry in build_plugin_catalog()["entries"]}

# A policy may permit a future strategy-side use, but the inventory itself
# never promotes a plugin or supplies a run-specific evidence package.
assert entries["market_regime_control"]["policy_position_control_allowed"] is True
assert entries["market_regime_control"]["status"] == "DEFERRED"