# SPDX-License-Identifier: BUSL-1.1 # Copyright (c) 2024-2026 Ali Sadeghi Aghili """Refuse to start on an unreadable ``project_config/`` with one clear line. ``security.sql_guard`` (and through it everything the API imports) binds ``schema_data.columns.TABLE_COLUMNS`` at import time, so a missing or broken ``schema.yaml`` -- or any other file the import chain reads -- used to end ``uvicorn api.server:app`` and ``scripts/prompt_budget.py`` in a forty-line traceback whose last line is the only useful one. This module runs the real loaders for those files *before* the imports that read them and turns a failure into a single line naming the file and the error. It reads the files exactly as the application does (the same loaders, the same strict YAML parser), so it can never accept a file the application would refuse, nor refuse one it would accept. It imports only what the caller is about to import anyway -- ``knowledge.config_loader`` and ``schema_data.registry`` -- and nothing heavy (no web framework, no database driver). Public API ---------- SERVER_IMPORT_FILES The files ``import api.server`` reads. PROMPT_BUDGET_IMPORT_FILES The files ``scripts/prompt_budget.py`` reads while importing. project_config_problem(filenames) -> str | None The first problem found, as one line, or ``None``. exit_if_project_config_unusable(filenames, *, hints=(), status=2) -> None Print the problem to stderr and raise ``SystemExit``; do nothing when every file loads. """ from __future__ import annotations import sys from collections.abc import Callable, Iterable __all__ = [ "PROMPT_BUDGET_IMPORT_FILES", "SERVER_IMPORT_FILES", "exit_if_project_config_unusable", "project_config_problem", ] #: What ``import api.server`` reads: ``retrieval/*``, ``session/*``, #: ``prompt_engine/static_prefix.py`` and ``security/sql_guard.py`` bind #: these at import time. (Files read later -- the system prompt, the memory #: policy -- fail at start-up with their own message.) Pinned to a real #: import by ``tests/test_config_preflight.py``. SERVER_IMPORT_FILES: tuple[str, ...] = ( "entities.yaml", "schema.yaml", "retrieval_hints.yaml", "aliases.yaml", "business_rules.yaml", "examples.yaml", "metrics.yaml", "session_policy.yaml", ) #: What ``scripts/prompt_budget.py`` reads while importing. PROMPT_BUDGET_IMPORT_FILES: tuple[str, ...] = ( "business_rules.yaml", "examples.yaml", "metrics.yaml", "schema.yaml", ) def _loaders() -> dict[str, Callable[[], object]]: """File name -> the function the application reads that file with.""" from knowledge.config_loader import ( load_aliases, load_business_rules, load_entities, load_examples, load_metrics, load_retrieval_hints, load_session_policy, ) from schema_data.registry import load_schema return { "aliases.yaml": load_aliases, "entities.yaml": load_entities, "business_rules.yaml": load_business_rules, "examples.yaml": load_examples, "metrics.yaml": load_metrics, "schema.yaml": load_schema, "retrieval_hints.yaml": load_retrieval_hints, "session_policy.yaml": load_session_policy, } def project_config_problem(filenames: Iterable[str]) -> str | None: """Return the first problem with *filenames* under ``project_config/``. Args: filenames: Names from :func:`_loaders`, checked in the order given. Returns: ``None`` when every file loads. Otherwise the loader's own message, which names the file (``[schema.yaml] duplicate key ...`` or `` not found. ...``); a message that does not mention the file is prefixed with ``[]``. Raises: ImportError: If the loaders themselves cannot be imported (a broken install, not a configuration problem). Examples: >>> import tempfile >>> from config import override_settings >>> with tempfile.TemporaryDirectory() as empty: ... with override_settings(project_config_dir=empty): ... message = project_config_problem(["schema.yaml"]) >>> "schema.yaml" in message and "not found" in message True """ from knowledge.config_loader import ConfigNotFoundError loaders = _loaders() for name in filenames: try: loaders[name]() except (ConfigNotFoundError, ValueError, OSError) as exc: message = " ".join(str(exc).split()) return message if name in message else f"[{name}] {message}" return None def exit_if_project_config_unusable( filenames: Iterable[str], *, hints: Iterable[str] = (), status: int = 2, ) -> None: """Print ``cannot load project_config: `` and exit, or return. Nothing happens when every file in *filenames* loads, so a valid deployment starts exactly as before. Args: filenames: Files to check, in order; see :func:`project_config_problem`. hints: Extra lines printed after the first one. status: The ``SystemExit`` status; ``2`` like ``scripts/sync_schema.py``. Raises: SystemExit: With *status*, after writing to stderr, when a file cannot be loaded. """ problem = project_config_problem(filenames) if problem is None: return print(f"cannot load project_config: {problem}", file=sys.stderr) for line in hints: print(line, file=sys.stderr) raise SystemExit(status)