* feat(analyzer): add the Ollama Analyzer backend (#15) Add compiler/analyzer/backend.py per MASTER_SPEC §12.3, §12.10. - AnalyzerBackend Protocol keeps the LLM backend swappable with no downstream coupling; the module imports no downstream compiler stage. - OllamaBackend talks to a local Ollama /api/generate endpoint using only the standard library (urllib). The HTTP call is an injectable transport, so the backend is fully unit-testable without a live Ollama. - Sends the configured model, temperature (default 0.0), and the user prompt unmodified, applying the configured timeout. BackendResult exposes the response text, model name, and request duration for logging. - Failure modes map to codes: connection failure -> BackendUnavailableError (SC0012, Fatal); timeout -> BackendTimeoutError (SC0013); HTTP error / unexpected response -> BackendResponseError (SC0018, new extension). Adds SC0018 (Analyzer Unexpected Response) to the message-code registry. Tests (no live Ollama): interface conformance, from_config, success with telemetry + unmodified prompt, and timeout/connection/URLError/HTTP/malformed response mapping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(analyzer): use pytest.approx and single throwing call in backend tests Avoid float-equality on temperature (SonarCloud reliability) and construct the backend outside pytest.raises so each block has a single throwing invocation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: kunail0804 <kunail0804@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
"""Central registry of compiler message codes (MASTER_SPEC §21.2, Appendix B).
|
|
|
|
Every diagnostic :class:`Message` uses a stable code from here, giving one source
|
|
of truth for a code's canonical severity and title. Codes SC0001-SC0014 come from
|
|
Appendix B; SC0015+ are extensions added as the compiler is implemented and are
|
|
intended to be folded back into Appendix B and the wiki.
|
|
|
|
Use :func:`message` to build a message so the severity and title always match the
|
|
code.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from compiler.common.result import Message, Severity
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CodeSpec:
|
|
"""The canonical severity and title for a message code."""
|
|
|
|
severity: Severity
|
|
title: str
|
|
|
|
|
|
# Appendix B (SC0001-SC0014).
|
|
_APPENDIX_B: dict[str, CodeSpec] = {
|
|
"SC0001": CodeSpec(Severity.WARNING, "Unknown Concept"),
|
|
"SC0002": CodeSpec(Severity.ERROR, "Invalid Scene JSON"),
|
|
"SC0003": CodeSpec(Severity.ERROR, "Circular Expansion"),
|
|
"SC0004": CodeSpec(Severity.FATAL, "Knowledge Base Load Failure"),
|
|
"SC0005": CodeSpec(Severity.ERROR, "Duplicate Canonical ID"),
|
|
"SC0006": CodeSpec(Severity.WARNING, "Deprecated Concept"),
|
|
"SC0007": CodeSpec(Severity.WARNING, "Duplicate Tag Removed"),
|
|
"SC0008": CodeSpec(Severity.ERROR, "Invalid Category"),
|
|
"SC0009": CodeSpec(Severity.ERROR, "Missing Required Field"),
|
|
"SC0010": CodeSpec(Severity.ERROR, "Schema Version Mismatch"),
|
|
"SC0011": CodeSpec(Severity.ERROR, "Analyzer Schema Validation Failure"),
|
|
"SC0012": CodeSpec(Severity.FATAL, "Analyzer Unavailable"),
|
|
"SC0013": CodeSpec(Severity.ERROR, "Analyzer Timeout"),
|
|
"SC0014": CodeSpec(Severity.FATAL, "Invalid Configuration"),
|
|
}
|
|
|
|
# Extensions beyond Appendix B (to be documented in the spec/wiki).
|
|
_EXTENSIONS: dict[str, CodeSpec] = {
|
|
"SC0015": CodeSpec(Severity.WARNING, "Unexpected Field Removed"),
|
|
"SC0016": CodeSpec(Severity.WARNING, "Empty Concept Removed"),
|
|
"SC0017": CodeSpec(Severity.WARNING, "Interaction Dropped"),
|
|
"SC0018": CodeSpec(Severity.ERROR, "Analyzer Unexpected Response"),
|
|
}
|
|
|
|
CODES: dict[str, CodeSpec] = {**_APPENDIX_B, **_EXTENSIONS}
|
|
|
|
|
|
def message(code: str, description: str, **context: Any) -> Message:
|
|
"""Build a :class:`Message` for a registered code.
|
|
|
|
The severity and title are taken from the registry so they always match the
|
|
code; only the description and optional context vary per call.
|
|
|
|
Raises:
|
|
KeyError: If ``code`` is not registered.
|
|
"""
|
|
spec = CODES[code]
|
|
return Message(
|
|
code=code,
|
|
severity=spec.severity,
|
|
title=spec.title,
|
|
description=description,
|
|
context=context,
|
|
)
|