def validate_dependencies(
config: NormalizedConfig,
*,
bindings: ConfigBindings,
context: ValidationContext,
) -> None:
"""Collect all native-surface violations in rule-id order, without effects."""
prompt = _family(config, "cap.prompt", PromptContextConfig)
memory = _family(config, "cap.memory", MemoryConfig)
tools = _family(config, "cap.tools", ToolingConfig)
human = _family(config, "cap.human", HumanInteractionConfig)
safety = _family(config, "cap.safety", SafetyConfig)
consolidation = _family(config, "svc.consolidation", ConsolidationServiceConfig)
checkpoint = _family(config, "svc.checkpoint", CheckpointServiceConfig)
session = _family(config, "svc.session", SessionServiceConfig)
errors: list[tuple[str, str]] = []
advisories: list[tuple[str, str]] = []
# XV-05
if prompt:
if prompt.layering == "stratigraphic" and prompt.hms_pipeline is None:
errors.append(("XV-05", "stratigraphic layering requires hms_pipeline"))
if prompt.caching.manifest_position == "volatile" and (
prompt.layering != "stratigraphic" or prompt.strategy == "jit"
):
errors.append(("XV-05", "volatile manifest requires stratigraphic non-JIT"))
if prompt.jit and prompt.jit.manifest_token_budget > 0 and prompt.strategy != "jit":
errors.append(("XV-05", "jit manifest_token_budget requires JIT strategy"))
if (
tools
and tools.intent_routing
and not tools.intent_routing.always_include
and (not prompt or prompt.strategy != "jit")
):
errors.append(("XV-05", "empty always_include requires JIT strategy"))
# XV-06 / XV-07
spreading = bool(memory and memory.hydration and memory.hydration.spreading)
if prompt and prompt.activation_log != "never" and not spreading:
errors.append(("XV-06", "activation_log requires hydration.spreading"))
cadence = (
prompt.hms_pipeline.extraction_directive_cadence if prompt and prompt.hms_pipeline else 0
)
if cadence > 0 and (memory is None or memory.extraction is None):
errors.append(("XV-07", "extraction directive requires cap.memory.extraction"))
# XV-08
if (
prompt
and "haiku" in config.runtime.model.model_name.lower()
and "1h" in {prompt.caching.system_prefix_ttl, prompt.caching.tools_ttl}
):
advisories.append(("XV-08", "1h cache tier may be ignored on Haiku"))
# XV-10 / XV-11
if (
safety
and safety.precondition_gate is not None
and (memory is None or MemoryLayer.PROCEDURAL not in memory.layers)
):
errors.append(("XV-10", "precondition_gate requires procedural memory"))
if (
prompt
and prompt.procedural_preflight is not None
and (memory is None or MemoryLayer.PROCEDURAL not in memory.layers)
):
errors.append(("XV-10", "procedural_preflight requires procedural memory"))
if safety and safety.fabrication.mode == "revise" and safety.metacognition is None:
errors.append(("XV-11", "fabrication revise requires metacognition"))
# XV-13 / XV-14
intent_consumers = bool(
tools and tools.intent_routing and tools.intent_routing.mode != "off"
) + bool(
memory
and memory.hydration
and memory.hydration.intent_filter
and memory.hydration.intent_filter.mode != "off"
)
if intent_consumers == 2 and bindings.intent_classifier is None:
errors.append(("XV-13", "intent consumers require one classifier binding"))
callable_bindings = {
"scheduler": bindings.scheduler,
"metacognition_gate": bindings.metacognition_gate,
"role_model_resolver": bindings.role_model_resolver,
"hms_budget_callback": bindings.hms_budget_callback,
}
for name, binding in callable_bindings.items():
if binding is not None and not callable(binding) and name != "scheduler":
errors.append(("XV-14", f"{name} binding does not satisfy its protocol"))
selective = bool(safety and safety.metacognition and safety.metacognition.selective_gate)
if bindings.metacognition_gate is not None and not selective:
errors.append(("XV-14", "metacognition_gate has no selective_gate config"))
# XV-15 / XV-16 / XV-17
telemetry = config.integrations.telemetry
if telemetry and "opentelemetry" not in context.available_extras:
errors.append(("XV-15", "telemetry selection requires opentelemetry extra"))
if consolidation and consolidation.entity_linking:
extractor = consolidation.entity_linking.extractor
if extractor == "spacy" and "spacy" not in context.available_extras:
errors.append(("XV-15", "spacy entity linker requires spacy extra"))
paths = []
if memory and memory.extraction and memory.extraction.template_path:
paths.append(memory.extraction.template_path)
if memory and memory.telemetry and memory.telemetry.sink != "logger":
paths.append(memory.telemetry.sink)
if checkpoint and checkpoint.sqlite_path:
paths.append(checkpoint.sqlite_path)
if paths and "filesystem" not in context.explicit_effects:
errors.append(("XV-16", "path-bearing configuration requires filesystem effect"))
if memory and memory.scope_blend.mode != "off" and not context.scope_has_parent:
errors.append(("XV-17", "scope blending requires a parent scope"))
# XV-18 / XV-19 / XV-21
if (
human
and (human.elicitation or human.interrupts)
and (checkpoint is None or "svc.background" not in config.services)
):
errors.append(("XV-18", "human pause/resume requires checkpoint and background"))
if session and session.transcript and checkpoint is None:
errors.append(("XV-18", "transcript persistence requires checkpoint"))
if session and session.transcript and checkpoint and checkpoint.backend == "memory":
advisories.append(("XV-18", "persisted transcripts use an ephemeral checkpoint"))
if consolidation and consolidation.nightly_nap and bindings.scheduler is None:
advisories.append(("XV-19", "nightly_nap has no scheduler binding"))
if consolidation and memory is None:
errors.append(("XV-21", "consolidation requires cap.memory"))
if errors:
ordered = sorted(errors, key=lambda item: (item[0], item[1]))
detail = "\n".join(f"{rule}: {message}" for rule, message in ordered)
raise ConfigurationError(f"Configuration validation failed:\n{detail}")
for rule, message in sorted(advisories):
warnings.warn(f"{rule}: {message}", UserWarning, stacklevel=3)