feat(WP-0003c): context adapters, first ActivityDefinition, full test suite

T51: ContextResolver ABC + CONTEXT_RESOLVER_REGISTRY; resolve_context activity
updated to dispatch via registry (warns + binds {} on failure, never aborts run).
T52: RepoScopingContextResolver with 5-min in-process cache.
T53: StateHubContextResolver (no cache) for domain_summary and repo_sbom_status.
T54: activity-definitions/weekly-sbom-staleness.md (Monday 09:00 Berlin, cron
trigger, flag-stale-sbom rule at >30 days) + tasks/sbom-rescan.md template.
T55: 51 parametrized evaluator tests — all whitelisted operators, unsafe
expression rejection, empty condition, missing attribute, nested context access.
T56: 15 executor safety tests — UntrustedFieldError, object-type rejection,
injection fixture, LLM retry on bad JSON, review_required field.
T57: 6 integration tests — parses real definition, evaluates rule per-repo
(stale/fresh boundary), emits via NullSink, verifies spawn log entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 23:24:48 +02:00
parent fd8d0827d7
commit 827ef9c1a0
12 changed files with 839 additions and 27 deletions

View File

@@ -89,34 +89,52 @@ async def load_activity_definition(activity_id: str) -> dict:
@activity.defn
async def resolve_context(context_sources: list[dict]) -> dict:
async def resolve_context(
context_sources: list[dict],
event_envelope_json: str | None = None,
) -> dict:
"""Resolve each context source and merge into a snapshot dict.
Returns: {source.name: resolved_value, ...}
Returns: {bind_key: resolved_value, ...}
Supported source types:
static — returns config["value"] directly
http_get — not yet implemented
db_query — not yet implemented
Source types are dispatched via CONTEXT_RESOLVER_REGISTRY.
A resolver that raises logs a warning and binds {} — it does not abort the run.
The 'static' type is handled inline without a registry entry.
"""
import activity_core.context_resolvers # noqa: F401 — registers all adapters
from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY
snapshot: dict = {}
for source in context_sources:
name = source["name"]
source_type = source["type"]
config = source.get("config", {})
source_type = source.get("type", "")
query = source.get("query", "")
params = source.get("params") or {}
raw_bind = source.get("bind_to") or source.get("name") or source_type
# Strip the 'context.' namespace prefix so evaluator can find the key.
bind_key = raw_bind.removeprefix("context.") if raw_bind.startswith("context.") else raw_bind
if source_type == "static":
snapshot[name] = config.get("value")
elif source_type in ("http_get", "db_query"):
raise ApplicationError(
f"Context source type {source_type!r} is not yet implemented",
non_retryable=True,
snapshot[bind_key] = source.get("config", {}).get("value")
continue
resolver_cls = CONTEXT_RESOLVER_REGISTRY.get(source_type)
if resolver_cls is None:
activity.logger.warning(
"Unknown context source type %r — binding {}",
source_type,
)
else:
raise ApplicationError(
f"Unknown context source type {source_type!r}",
non_retryable=True,
snapshot[bind_key] = {}
continue
try:
snapshot[bind_key] = resolver_cls().resolve(query, None, params)
except Exception as exc:
activity.logger.warning(
"Context resolver %r failed — %s; binding {}",
source_type,
exc,
)
snapshot[bind_key] = {}
return snapshot