context loading, path resolution, form state, dynamic rules, and provider-neutral assessment runner/cache boundary

This commit is contained in:
tegwick 2026-05-04 13:52:29 +02:00
parent eccf1874fb
commit 8361f9ea45
29 changed files with 2809 additions and 65 deletions

View file

@ -27,6 +27,7 @@ from markitect_tool.diagnostics import (
has_error,
valid_severity,
)
from markitect_tool.runtime import RuntimeContext, evaluate_form_state, load_runtime_context_file
@dataclass(frozen=True)
@ -55,6 +56,7 @@ class ContractCheckResult:
document_path: str | None = None
contract_path: str | None = None
metrics: dict[str, Any] = field(default_factory=dict)
runtime: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
data = {
@ -63,6 +65,7 @@ class ContractCheckResult:
"document_path": self.document_path,
"contract_path": self.contract_path,
"metrics": self.metrics or None,
"runtime": self.runtime or None,
}
return {key: value for key, value in data.items() if value is not None}
@ -131,26 +134,48 @@ def validate_contract(contract: DocumentContract) -> ContractValidationResult:
def check_markdown_file(
markdown_path: str | Path, contract_path: str | Path
markdown_path: str | Path,
contract_path: str | Path,
*,
context_path: str | Path | None = None,
runtime_context: RuntimeContext | None = None,
) -> ContractCheckResult:
"""Parse and check a Markdown file against a contract file."""
document = parse_markdown_file(markdown_path)
contract = load_contract_file(contract_path)
return check_document_contract(document, contract)
context = runtime_context
if context_path is not None:
context = load_runtime_context_file(context_path)
return check_document_contract(document, contract, runtime_context=context)
def check_document_contract(
document: Document, contract: DocumentContract
document: Document,
contract: DocumentContract,
*,
runtime_context: RuntimeContext | None = None,
) -> ContractCheckResult:
"""Check a parsed Markdown document against a document contract."""
contract_validation = validate_contract(contract)
document_metrics = collect_metrics(document)
diagnostics = list(contract_validation.diagnostics)
runtime: dict[str, Any] = {}
if contract_validation.valid:
diagnostics.extend(_check_document_type(document, contract))
diagnostics.extend(_check_fields(document, contract))
if runtime_context is None and not contract.rules:
diagnostics.extend(_check_fields(document, contract))
else:
form_state = evaluate_form_state(
document,
contract,
runtime_context or RuntimeContext.empty(),
)
diagnostics.extend(form_state.diagnostics)
context = runtime_context or RuntimeContext.empty()
runtime["context"] = context.to_dict()
runtime["form_state"] = form_state.to_dict()
diagnostics.extend(_check_document_metrics(document, contract, document_metrics))
diagnostics.extend(_check_assertions(document.body, contract.assertions, document, contract))
diagnostics.extend(_check_sections(document, contract, document_metrics))
@ -161,6 +186,7 @@ def check_document_contract(
document_path=document.source_path,
contract_path=contract.source_path,
metrics=document_metrics.to_dict(),
runtime=runtime,
)

View file

@ -120,6 +120,7 @@ def _looks_like_contract(data: dict[str, Any]) -> bool:
"metric_bands",
"assertions",
"forms",
"rules",
"rubrics",
}
)

View file

@ -223,6 +223,7 @@ class DocumentContract:
assertions: list[AssertionSpec] = field(default_factory=list)
forms: list[dict[str, Any]] = field(default_factory=list)
context: dict[str, Any] = field(default_factory=dict)
rules: list[dict[str, Any]] = field(default_factory=list)
rubrics: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
raw: dict[str, Any] = field(default_factory=dict)
@ -264,6 +265,7 @@ class DocumentContract:
assertions=assertions_from_value(raw.get("assertions")),
forms=raw.get("forms") if isinstance(raw.get("forms"), list) else [],
context=raw.get("context") if isinstance(raw.get("context"), dict) else {},
rules=raw.get("rules") if isinstance(raw.get("rules"), list) else [],
rubrics=raw.get("rubrics") if isinstance(raw.get("rubrics"), list) else [],
metadata=metadata,
raw=raw,
@ -284,6 +286,7 @@ class DocumentContract:
"assertions": [assertion.raw for assertion in self.assertions],
"forms": self.forms,
"context": self.context,
"rules": self.rules,
"rubrics": self.rubrics,
"source_path": self.source_path,
}