feat(detection-core): validate exact admission context

This commit is contained in:
Marius Ciepluch
2026-10-31 19:38:01 +01:00
parent 361c216402
commit f3adbfe333
9 changed files with 1406 additions and 108 deletions
@@ -1,110 +1,5 @@
"""Recursive closed-graph checks for reviewed admission payloads."""
from __future__ import annotations
from dataclasses import fields
from typing import Any, cast
_SCALAR_TYPES: tuple[type[object], ...] = (str, int, float, bool, type(None))
_ENUM_STATE_FIELDS = frozenset(("_value_", "_name_", "__objclass__", "_sort_order_"))
def closed_graph_problem(
value: object,
*,
dataclass_types: tuple[type[object], ...],
enum_types: tuple[type[object], ...],
path: str = "payload",
) -> str | None:
"""Return the first non-closed node in a whitelisted payload graph."""
value_type = type(value)
if value_type in dataclass_types:
return _dataclass_problem(value, value_type, dataclass_types, enum_types, path)
if value_type is tuple:
for index, item in enumerate(cast(tuple[object, ...], value)):
problem = closed_graph_problem(
item,
dataclass_types=dataclass_types,
enum_types=enum_types,
path=f"{path}[{index}]",
)
if problem is not None:
return problem
return None
if value_type in enum_types:
return _enum_problem(value, value_type, path)
if value_type in _SCALAR_TYPES:
return None
return f"{path} uses non-closed type {value_type.__name__}"
def _dataclass_problem(
value: object,
value_type: type[object],
dataclass_types: tuple[type[object], ...],
enum_types: tuple[type[object], ...],
path: str,
) -> str | None:
declared_fields = fields(cast(Any, value_type))
declared_names = {field.name for field in declared_fields}
try:
actual_names = set(vars(value))
except TypeError:
actual_names = declared_names
if actual_names != declared_names:
return f"{path} must have exactly its declared fields"
for field in declared_fields:
try:
field_value = getattr(value, field.name)
except AttributeError:
return f"{path}.{field.name} is missing"
problem = closed_graph_problem(
field_value,
dataclass_types=dataclass_types,
enum_types=enum_types,
path=f"{path}.{field.name}",
)
if problem is not None:
return problem
return None
def _enum_problem(value: object, enum_type: type[object], path: str) -> str | None:
state = vars(value)
if set(state) != _ENUM_STATE_FIELDS:
return f"{path} enum member must have exactly its canonical state fields"
expected = _canonical_enum_state(value, enum_type)
if expected is None:
return f"{path} enum type has aliases or nonunique member mappings"
expected_name, expected_value, expected_order = expected
if (
state["_name_"] is not expected_name
or state["_value_"] is not expected_value
or state["__objclass__"] is not enum_type
or type(state["_sort_order_"]) is not int
or state["_sort_order_"] != expected_order
):
return f"{path} enum member state is not canonical"
return None
def _canonical_enum_state(value: object, enum_type: type[object]) -> tuple[object, object, int] | None:
enum_class = cast(Any, enum_type)
canonical_members = tuple(enum_class)
name_entries = tuple(enum_class.__members__.items())
value_entries = tuple(enum_class._value2member_map_.items())
if len(name_entries) != len(canonical_members) or len(value_entries) != len(canonical_members):
return None
if any(sum(candidate is member for _, candidate in name_entries) != 1 for member in canonical_members):
return None
if any(sum(candidate is member for _, candidate in value_entries) != 1 for member in canonical_members):
return None
names = tuple(name for name, member in name_entries if member is value)
values = tuple(item for item, member in value_entries if member is value)
orders = tuple(index for index, member in enumerate(canonical_members) if member is value)
if len(names) != 1 or len(values) != 1 or len(orders) != 1:
return None
return names[0], values[0], orders[0]
"""Compatibility import for the neutral closed-graph validator."""
from openultrasast.core._closed_graph import closed_graph_problem as closed_graph_problem
__all__ = ["closed_graph_problem"]
+148
View File
@@ -0,0 +1,148 @@
"""Reusable recursive checks for canonical closed object graphs."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import fields
from typing import Any, cast
_SCALAR_TYPES: tuple[type[object], ...] = (str, int, float, bool, type(None))
_ENUM_STATE_FIELDS = frozenset(("_value_", "_name_", "__objclass__", "_sort_order_"))
def closed_graph_problem(
value: object,
*,
dataclass_types: tuple[type[object], ...],
enum_types: tuple[type[object], ...],
mapping_types: tuple[type[object], ...] = (),
path: str = "payload",
) -> str | None:
"""Return the first non-closed node in a whitelisted payload graph."""
value_type = type(value)
if value_type in dataclass_types:
return _dataclass_problem(value, value_type, dataclass_types, enum_types, mapping_types, path)
if value_type is tuple:
for index, item in enumerate(cast(tuple[object, ...], value)):
problem = closed_graph_problem(
item,
dataclass_types=dataclass_types,
enum_types=enum_types,
mapping_types=mapping_types,
path=f"{path}[{index}]",
)
if problem is not None:
return problem
return None
if value_type in enum_types:
return _enum_problem(value, value_type, path)
if value_type in mapping_types:
return _mapping_problem(value, dataclass_types, enum_types, mapping_types, path)
if value_type in _SCALAR_TYPES:
return None
return f"{path} uses non-closed type {value_type.__name__}"
def _dataclass_problem(
value: object,
value_type: type[object],
dataclass_types: tuple[type[object], ...],
enum_types: tuple[type[object], ...],
mapping_types: tuple[type[object], ...],
path: str,
) -> str | None:
declared_fields = fields(cast(Any, value_type))
declared_names = {field.name for field in declared_fields}
try:
actual_names = set(vars(value))
except TypeError:
actual_names = declared_names
if actual_names != declared_names:
return f"{path} must have exactly its declared fields"
for field in declared_fields:
try:
field_value = getattr(value, field.name)
except AttributeError:
return f"{path}.{field.name} is missing"
problem = closed_graph_problem(
field_value,
dataclass_types=dataclass_types,
enum_types=enum_types,
mapping_types=mapping_types,
path=f"{path}.{field.name}",
)
if problem is not None:
return problem
return None
def _mapping_problem(
value: object,
dataclass_types: tuple[type[object], ...],
enum_types: tuple[type[object], ...],
mapping_types: tuple[type[object], ...],
path: str,
) -> str | None:
for key, item in cast(Mapping[object, object], value).items():
for name, candidate in (("key", key), ("value", item)):
problem = closed_graph_problem(
candidate,
dataclass_types=dataclass_types,
enum_types=enum_types,
mapping_types=mapping_types,
path=f"{path}.{name}",
)
if problem is not None:
return problem
return None
def exact_dataclass_problem(value: object, expected_type: type[object], path: str) -> str | None:
"""Check only an aggregate root's exact class and declared instance fields."""
if type(value) is not expected_type:
return f"{path} must use exact type {expected_type.__name__}"
declared = {field.name for field in fields(cast(Any, expected_type))}
if set(vars(value)) != declared:
return f"{path} must have exactly its declared fields"
return None
def _enum_problem(value: object, enum_type: type[object], path: str) -> str | None:
state = vars(value)
if set(state) != _ENUM_STATE_FIELDS:
return f"{path} enum member must have exactly its canonical state fields"
expected = _canonical_enum_state(value, enum_type)
if expected is None:
return f"{path} enum type has aliases or nonunique member mappings"
expected_name, expected_value, expected_order = expected
if (
state["_name_"] is not expected_name
or state["_value_"] is not expected_value
or state["__objclass__"] is not enum_type
or type(state["_sort_order_"]) is not int
or state["_sort_order_"] != expected_order
):
return f"{path} enum member state is not canonical"
return None
def _canonical_enum_state(value: object, enum_type: type[object]) -> tuple[object, object, int] | None:
enum_class = cast(Any, enum_type)
canonical_members = tuple(enum_class)
name_entries = tuple(enum_class.__members__.items())
value_entries = tuple(enum_class._value2member_map_.items())
if len(name_entries) != len(canonical_members) or len(value_entries) != len(canonical_members):
return None
if any(sum(candidate is member for _, candidate in name_entries) != 1 for member in canonical_members):
return None
if any(sum(candidate is member for _, candidate in value_entries) != 1 for member in canonical_members):
return None
names = tuple(name for name, member in name_entries if member is value)
values = tuple(item for item, member in value_entries if member is value)
orders = tuple(index for index, member in enumerate(canonical_members) if member is value)
if len(names) != 1 or len(values) != 1 or len(orders) != 1:
return None
return names[0], values[0], orders[0]
__all__ = ["closed_graph_problem", "exact_dataclass_problem"]
@@ -0,0 +1,83 @@
"""Closed object-graph whitelist for evidence validation contexts."""
from __future__ import annotations
from types import MappingProxyType
from openultrasast.core._closed_graph import closed_graph_problem
from openultrasast.core.canonical import ContentRef
from openultrasast.core.custody import LineagePurpose, PopulationCustodyPolicy
from openultrasast.core.evidence import NeutralOutcome
from openultrasast.core.factpack import (
FactPack,
IndependenceAttestation,
KnowledgeEvidence,
KnowledgeEvidenceKind,
RelationEffect,
ReviewIndependencePolicy,
SemanticFact,
StructuralRequirement,
StructuralRequirementKind,
)
from openultrasast.core.family import (
DecisionRule,
FamilyContract,
FieldSpec,
ObjectSpec,
RelationOriginPolicy,
ResidualQuestion,
RoleSpec,
)
from openultrasast.core.family_catalog import FamilyCatalog, FamilyCatalogEntry
from openultrasast.core.lineage import LineageManifest, LineageUse
from openultrasast.core.model import ModelProgramDeclaration
from openultrasast.core.provenance import ProvenanceTrustPolicy
from openultrasast.core.semantics import FieldKind, RelationKind, RelationStatus
_DATACLASSES: tuple[type[object], ...] = (
FamilyCatalog,
FamilyCatalogEntry,
FamilyContract,
FieldSpec,
ObjectSpec,
RoleSpec,
DecisionRule,
ResidualQuestion,
FactPack,
IndependenceAttestation,
ReviewIndependencePolicy,
KnowledgeEvidence,
StructuralRequirement,
RelationEffect,
SemanticFact,
ContentRef,
ModelProgramDeclaration,
LineageManifest,
LineageUse,
ProvenanceTrustPolicy,
PopulationCustodyPolicy,
)
_ENUMS: tuple[type[object], ...] = (
NeutralOutcome,
RelationOriginPolicy,
FieldKind,
RelationKind,
RelationStatus,
KnowledgeEvidenceKind,
StructuralRequirementKind,
LineagePurpose,
)
_MAPPINGS: tuple[type[object], ...] = (type(MappingProxyType({})),)
def evidence_context_problem(value: object, context_type: type[object]) -> str | None:
return closed_graph_problem(
value,
dataclass_types=(context_type, *_DATACLASSES),
enum_types=_ENUMS,
mapping_types=_MAPPINGS,
path="evidence validation context",
)
__all__ = ["evidence_context_problem"]
+405
View File
@@ -0,0 +1,405 @@
"""Exact, label-blind admission context validation before feasibility."""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from openultrasast.core import canonical
from openultrasast.core._closed_graph import closed_graph_problem, exact_dataclass_problem
from openultrasast.core.admission_review import AdmissionManifestV2, validate_reviewed_admission_payloads
from openultrasast.core.custody import (
LineagePurpose,
PopulationCustodyPort,
PopulationStatus,
validate_population_custody_log,
)
from openultrasast.core.factpack import FACT_PACK_SCHEMA, ReviewIndependencePolicy
from openultrasast.core.family_catalog import FamilyCatalog, FamilyCatalogEntry, validate_catalog
from openultrasast.core.lineage import LineageExpectation, LineageManifest, validate_lineage
from openultrasast.core.measurement import (
BaselineIdentity,
DenominatorPolicy,
ScopeKey,
derive_denominator_policy_digest,
derive_identity_set_digest,
validate_denominator_policy,
)
from openultrasast.core.model import ModelProgramDeclaration
from openultrasast.core.proof import ProofTrustPolicy, derive_proof_trust_policy_digest
from openultrasast.core.readiness import (
AnalysisIdentity,
ReadinessResult,
ReadinessStage,
authorize_stage,
)
from openultrasast.core.semantics import (
OBSERVATION_SCHEMA,
SEMANTIC_RELATION_SCHEMA,
FieldKind,
ProvenanceKind,
RelationKind,
RelationStatus,
)
from openultrasast.core.validation import (
EvidenceValidationContext,
derive_fact_set_digest,
validate_evidence_validation_context,
)
ADMISSION_VALIDATION_CONTEXT_SCHEMA = "openultrasast.detection-core.admission-validation-context.v2"
OUTCOME_MAPPING_SCHEMA = "openultrasast.detection-core.outcome-mapping.v2"
SEMANTIC_VOCABULARY_SCHEMA = "openultrasast.detection-core.semantic-vocabulary.v2"
class AdmissionContextError(ValueError):
"""Admission inputs disagree before any feasibility work or label access."""
@dataclass(frozen=True)
class AdmissionValidationContext:
evidence: EvidenceValidationContext
admission_review_policy: ReviewIndependencePolicy
proof_trust_policy: ProofTrustPolicy | None
context_digest: str
def derive_fact_schema_digest() -> str:
return canonical.content_digest(FACT_PACK_SCHEMA)
def derive_semantic_vocabulary_digest() -> str:
return canonical.content_digest(
{
"schema_version": SEMANTIC_VOCABULARY_SCHEMA,
"relation_schema": SEMANTIC_RELATION_SCHEMA,
"observation_schema": OBSERVATION_SCHEMA,
"field_kinds": tuple(item.value for item in FieldKind),
"relation_kinds": tuple(item.value for item in RelationKind),
"relation_statuses": tuple(item.value for item in RelationStatus),
"provenance_kinds": tuple(item.value for item in ProvenanceKind),
}
)
def _decision_value(entry: FamilyCatalogEntry) -> canonical.JsonValue:
contract = entry.contract
return {
"contract_digest": entry.contract_digest,
"decisions": tuple(
{
"family_code": rule.family_code,
"outcome": rule.outcome.value,
"required_roles": rule.required_roles,
"allowed_statuses": tuple(status.value for status in rule.allowed_statuses),
}
for rule in contract.decisions
),
}
def derive_outcome_mapping_digest(catalog: FamilyCatalog) -> str:
validate_catalog(catalog)
return canonical.content_digest(
{"schema_version": OUTCOME_MAPPING_SCHEMA, "contracts": tuple(_decision_value(entry) for entry in catalog.entries)}
)
def derive_admission_validation_context_digest(context: AdmissionValidationContext) -> str:
proof_digest = None
if context.proof_trust_policy is not None:
proof_digest = derive_proof_trust_policy_digest(context.proof_trust_policy)
return canonical.content_digest(
{
"schema_version": ADMISSION_VALIDATION_CONTEXT_SCHEMA,
"evidence_context_digest": context.evidence.context_digest,
"admission_review_policy_digest": context.admission_review_policy.policy_digest,
"proof_trust_policy_digest": proof_digest,
}
)
def make_admission_validation_context(
*,
evidence: EvidenceValidationContext,
admission_review_policy: ReviewIndependencePolicy,
proof_trust_policy: ProofTrustPolicy | None,
) -> AdmissionValidationContext:
provisional = AdmissionValidationContext(evidence, admission_review_policy, proof_trust_policy, "0" * 64)
return AdmissionValidationContext(
evidence,
admission_review_policy,
proof_trust_policy,
derive_admission_validation_context_digest(provisional),
)
def _contract_for_scope(catalog: FamilyCatalog, scope: ScopeKey) -> FamilyCatalogEntry:
matches = tuple(
entry for entry in catalog.entries if entry.contract.family_id == scope.family_id and scope.ecosystem in entry.contract.ecosystems
)
if len(matches) != 1:
raise AdmissionContextError("each claimed scope must resolve one exact family contract")
return matches[0]
def derive_analysis_identity(
baseline: BaselineIdentity,
catalog: FamilyCatalog,
scope: ScopeKey,
model_program_digest: str | None,
) -> AnalysisIdentity:
entry = _contract_for_scope(catalog, scope)
return AnalysisIdentity(
entry.contract.schema_version,
derive_semantic_vocabulary_digest(),
baseline.analyzer_revision,
entry.contract_digest,
baseline.fact_set_digest,
baseline.acquisition_record_digest,
baseline.configuration_digest,
model_program_digest,
baseline.outcome_mapping_digest,
)
def _validate_context(context: AdmissionValidationContext) -> None:
problem = exact_dataclass_problem(context, AdmissionValidationContext, "admission validation context")
if problem is not None:
raise AdmissionContextError(f"closed admission context violation: {problem}")
validate_evidence_validation_context(context.evidence)
context.admission_review_policy.__post_init__()
if context.proof_trust_policy is not None:
problem = closed_graph_problem(
context.proof_trust_policy,
dataclass_types=(ProofTrustPolicy,),
enum_types=(),
path="proof trust policy",
)
if problem is not None:
raise AdmissionContextError(f"closed proof policy violation: {problem}")
context.proof_trust_policy.__post_init__()
expected = derive_admission_validation_context_digest(context)
if context.context_digest != expected:
raise AdmissionContextError("admission validation context digest cannot be re-derived")
def _validate_scopes_and_sets(manifest: AdmissionManifestV2, denominator: DenominatorPolicy, context: AdmissionValidationContext) -> None:
baseline = manifest.identity
evidence = context.evidence
catalog = evidence.family_catalog
validate_denominator_policy(denominator)
entries = tuple(entry.contract_digest for entry in catalog.entries)
if baseline.family_contract_digests != tuple(sorted(entries)) or baseline.family_contract_set_digest != catalog.catalog_digest:
raise AdmissionContextError("baseline family catalog set does not match canonical catalog objects")
census_scopes = tuple(row.scope for row in manifest.census.rows)
if baseline.claimed_scopes != census_scopes:
raise AdmissionContextError("claimed scopes do not match the reviewed census")
for scope in baseline.claimed_scopes:
_contract_for_scope(catalog, scope)
expected_fact_set = derive_fact_set_digest(evidence.fact_packs)
if baseline.fact_schema_digest != derive_fact_schema_digest() or baseline.fact_set_digest != expected_fact_set:
raise AdmissionContextError("baseline fact set does not match canonical fact objects")
programs = tuple(sorted(item.declaration_digest for item in evidence.model_programs))
expected_program_set = derive_identity_set_digest("model_program", programs) if programs else None
if baseline.model_program_digests != programs or baseline.model_program_set_digest != expected_program_set:
raise AdmissionContextError("baseline model program set does not match canonical model objects")
if baseline.outcome_mapping_digest != derive_outcome_mapping_digest(catalog):
raise AdmissionContextError("baseline outcome mapping does not match ordered contract decisions")
if denominator.policy_digest != derive_denominator_policy_digest(denominator):
raise AdmissionContextError("denominator policy digest cannot be re-derived")
if denominator.policy_digest != baseline.denominator_policy_digest or denominator.policy_digest != manifest.denominator_policy_digest:
raise AdmissionContextError("denominator policy does not match the reviewed manifest")
if any(rule.scopes != baseline.claimed_scopes for rule in denominator.rules):
raise AdmissionContextError("denominator scope does not exactly match claimed scopes")
def _expectations_for_program(program: ModelProgramDeclaration) -> tuple[LineageExpectation, ...]:
expected = [
LineageExpectation(LineagePurpose.PROMPT, program.prompt_digest, 1, 1),
LineageExpectation(LineagePurpose.DECISION_MAPPING, program.decision_mapping_digest, 1, 1),
]
if program.example_set_digest is not None:
expected.append(LineageExpectation(LineagePurpose.EXAMPLE, program.example_set_digest, 1, 1))
if program.fitted_component_digest is not None:
expected.append(LineageExpectation(LineagePurpose.FITTED_MODEL, program.fitted_component_digest, 1, 1))
return tuple(expected)
def _register(
expected: dict[str, tuple[str, tuple[LineageExpectation, ...]]],
digest: str,
subject: str,
expectations: tuple[LineageExpectation, ...],
) -> None:
value = (subject, expectations)
if digest in expected and expected[digest] != value:
raise AdmissionContextError("lineage manifest is ambiguously owned by canonical objects")
expected[digest] = value
def _lineage_expectations(
manifest: AdmissionManifestV2,
context: AdmissionValidationContext,
) -> dict[str, tuple[str, tuple[LineageExpectation, ...]]]:
expected: dict[str, tuple[str, tuple[LineageExpectation, ...]]] = {}
_register(
expected,
manifest.threshold_lineage_manifest_digest,
manifest.threshold_set_digest,
(LineageExpectation(LineagePurpose.THRESHOLD, manifest.threshold_set_digest, 1, 1),),
)
for program in context.evidence.model_programs:
_register(expected, program.lineage_manifest_digest, program.input_bundle_digest, _expectations_for_program(program))
for pack in context.evidence.fact_packs:
expectations = (
LineageExpectation(LineagePurpose.SEMANTIC_FACT, pack.normalized_content_digest, 1, 1),
LineageExpectation(LineagePurpose.PROMOTION, pack.normalized_content_digest, 0, 1),
)
for review in pack.review_evidence:
_register(expected, review.lineage_manifest_digest, pack.normalized_content_digest, expectations)
return expected
def _orphan_expectations(lineage: LineageManifest) -> tuple[LineageExpectation, ...]:
if not lineage.uses:
raise AdmissionContextError("unowned lineage must not be empty")
allowed = {LineagePurpose.MATCHING_BEHAVIOR, LineagePurpose.PROMOTION}
if any(use.purpose not in allowed or use.influenced_artifact_digest != lineage.subject_digest for use in lineage.uses):
raise AdmissionContextError("lineage is not owned by a canonical matcher or promotion object")
purposes = tuple(sorted({use.purpose for use in lineage.uses}, key=lambda item: item.value))
return tuple(
LineageExpectation(
purpose,
lineage.subject_digest,
sum(use.purpose is purpose for use in lineage.uses),
sum(use.purpose is purpose for use in lineage.uses),
)
for purpose in purposes
)
def _refuse_omitted_lineage_uses(
lineage: LineageManifest,
expectations: tuple[LineageExpectation, ...],
populations: set[str],
context: AdmissionValidationContext,
custody: PopulationCustodyPort,
) -> None:
expected = {(item.purpose, item.influenced_artifact_digest) for item in expectations}
declared = {use.population_use_receipt_digest for use in lineage.uses}
for population in sorted(populations):
log = custody.current_log(population)
validate_population_custody_log(log, context.evidence.custody_policy, current=custody)
for receipt in log.receipts:
request = receipt.request
key = (request.purpose, request.influenced_artifact_digest)
relevant = key in expected and (request.arm_id == lineage.arm_id or request.labels_accessed)
if relevant and receipt.receipt_digest not in declared:
label = "label-influenced " if request.labels_accessed else ""
raise AdmissionContextError(f"lineage omitted current {label}use: {receipt.receipt_digest}")
def _validate_lineages(manifest: AdmissionManifestV2, context: AdmissionValidationContext, custody: PopulationCustodyPort) -> None:
lineages = context.evidence.lineages
indexed = {item.manifest_digest: item for item in lineages}
if len(indexed) != len(lineages):
raise AdmissionContextError("duplicate lineage manifest identity")
expected = _lineage_expectations(manifest, context)
if any(digest not in indexed for digest in expected):
raise AdmissionContextError("required threshold, model, or fact lineage is absent")
populations = {manifest.identity.population_digest, *(use.source_population_digest for item in lineages for use in item.uses)}
for lineage in lineages:
binding = expected.get(lineage.manifest_digest)
subject, expectations = binding if binding is not None else (lineage.subject_digest, _orphan_expectations(lineage))
decision = validate_lineage(
lineage,
subject,
expectations,
manifest.identity.population_digest,
True,
custody,
context.evidence.custody_policy,
)
if not decision.allowed:
raise AdmissionContextError("lineage validation refused: " + ",".join(decision.reasons))
_refuse_omitted_lineage_uses(lineage, expectations, populations, context, custody)
def _model_for_scope(context: AdmissionValidationContext, contract_digest: str) -> str | None:
programs = tuple(item.declaration_digest for item in context.evidence.model_programs if item.family_contract_digest == contract_digest)
if len(programs) > 1:
raise AdmissionContextError("a readiness scope cannot select multiple model programs")
return programs[0] if programs else None
def _validate_readiness(manifest: AdmissionManifestV2, readiness: Sequence[ReadinessResult], context: AdmissionValidationContext) -> None:
if type(readiness) is not tuple:
raise AdmissionContextError("readiness container must be an exact tuple")
values = tuple(readiness)
digests = tuple(sorted(item.result_digest for item in values))
if digests != manifest.readiness_digests or len(set(digests)) != len(digests):
raise AdmissionContextError("readiness result set does not match the reviewed manifest")
consumed: set[str] = set()
for scope in manifest.identity.claimed_scopes:
receipts = tuple(
item
for item in values
if item.arm_id == manifest.identity.arm_id and item.family_id == scope.family_id and item.ecosystem == scope.ecosystem
)
entry = _contract_for_scope(context.evidence.family_catalog, scope)
model = _model_for_scope(context, entry.contract_digest)
current = derive_analysis_identity(manifest.identity, context.evidence.family_catalog, scope, model)
authorization = authorize_stage(ReadinessStage.ADMISSION, receipts, current)
if not authorization.authorized:
raise AdmissionContextError("current instrument-through-development readiness refused: " + ",".join(authorization.reasons))
consumed.update(item.result_digest for item in receipts)
if consumed != set(digests):
raise AdmissionContextError("readiness contains a scope, arm, or identity outside the reviewed manifest")
def _validate_custody(manifest: AdmissionManifestV2, context: AdmissionValidationContext, custody: PopulationCustodyPort) -> None:
log = custody.current_log(manifest.identity.population_digest)
validate_population_custody_log(log, context.evidence.custody_policy, current=custody)
if log.log_digest != manifest.custody_log_digest:
raise AdmissionContextError("manifest custody head is not the current complete custody log")
if log.status is not PopulationStatus.AVAILABLE:
raise AdmissionContextError("admission population is spent and no longer available")
def validate_admission_context(
manifest: AdmissionManifestV2,
denominator: DenominatorPolicy,
readiness: Sequence[ReadinessResult],
context: AdmissionValidationContext,
custody: PopulationCustodyPort,
resolver: canonical.ContentResolver,
) -> None:
"""Validate every pre-opening identity and lineage; feasibility is deliberately absent."""
try:
_validate_context(context)
if manifest.validation_context_digest != context.context_digest:
raise AdmissionContextError("manifest validation context has been substituted")
validate_reviewed_admission_payloads(manifest.census, manifest, context.admission_review_policy, resolver)
_validate_scopes_and_sets(manifest, denominator, context)
_validate_custody(manifest, context, custody)
_validate_lineages(manifest, context, custody)
_validate_readiness(manifest, readiness, context)
except AdmissionContextError:
raise
except (AttributeError, LookupError, TypeError, ValueError) as exc:
raise AdmissionContextError(f"admission context validation failed: {exc}") from exc
__all__ = [
"ADMISSION_VALIDATION_CONTEXT_SCHEMA",
"AdmissionContextError",
"AdmissionValidationContext",
"derive_admission_validation_context_digest",
"derive_analysis_identity",
"derive_fact_schema_digest",
"derive_outcome_mapping_digest",
"derive_semantic_vocabulary_digest",
"make_admission_validation_context",
"validate_admission_context",
]
+13
View File
@@ -9,6 +9,7 @@ from dataclasses import dataclass
from enum import Enum
from openultrasast.core import canonical
from openultrasast.core._closed_graph import closed_graph_problem
BASELINE_IDENTITY_SCHEMA = "openultrasast.detection-core.baseline-identity.v2"
DENOMINATOR_POLICY_SCHEMA = "openultrasast.detection-core.denominator-policy.v2"
@@ -300,12 +301,20 @@ class DenominatorPolicy:
_validate_policy(self)
_POLICY_TYPES: tuple[type[object], ...] = (DenominatorPolicy, MetricRule, ScopeKey)
_POLICY_ENUMS: tuple[type[object], ...] = (MetricId, NumeratorKind, DenominatorKind, IntervalMethod)
def _validate_policy(value: DenominatorPolicy) -> None:
problem = closed_graph_problem(value, dataclass_types=_POLICY_TYPES, enum_types=_POLICY_ENUMS, path="denominator policy")
if problem is not None:
raise MeasurementContractError(f"closed denominator graph violation: {problem}")
if not isinstance(value.rules, tuple) or not all(isinstance(item, MetricRule) for item in value.rules):
raise MeasurementContractError("rules must be a tuple of MetricRule values")
if tuple(rule.metric_id for rule in value.rules) != tuple(MetricId):
raise MeasurementContractError("each standard metric must occur exactly once in canonical order")
for rule in value.rules:
rule.__post_init__()
expected_numerator, expected_denominator, _ = _ALGEBRA[rule.metric_id]
if (rule.numerator, rule.denominator) != (expected_numerator, expected_denominator):
raise MeasurementContractError("metric rule violates the fixed algebra")
@@ -320,6 +329,9 @@ def _validate_policy(value: DenominatorPolicy) -> None:
raise MeasurementContractError("denominator policy digest does not match policy")
validate_denominator_policy = _validate_policy
def _policy_digest(rules: tuple[MetricRule, ...], method: IntervalMethod) -> str:
return canonical.content_digest(
{
@@ -478,4 +490,5 @@ __all__ = [
"metric_result_value",
"require_comparable_baselines",
"standard_metric_rules",
"validate_denominator_policy",
]
+8
View File
@@ -8,6 +8,7 @@ from dataclasses import dataclass
from enum import Enum
from openultrasast.core import canonical
from openultrasast.core._closed_graph import closed_graph_problem
READINESS_RESULT_SCHEMA = "openultrasast.detection-core.readiness-result.v2"
_DIGEST = re.compile(r"[0-9a-f]{64}")
@@ -197,6 +198,10 @@ class StageAuthorization:
validate_stage_authorization(self)
_RESULT_TYPES: tuple[type[object], ...] = (ReadinessResult, AnalysisIdentity, FixtureResult)
_RESULT_ENUMS: tuple[type[object], ...] = (ReadinessStage, ReadinessEvidenceUse, StageResult, FixtureRole)
def _identity_value(value: AnalysisIdentity) -> canonical.JsonValue:
return {
"core_schema_version": value.core_schema_version,
@@ -332,6 +337,9 @@ def derive_readiness_result_digest(
def validate_readiness_result(value: ReadinessResult) -> None:
problem = closed_graph_problem(value, dataclass_types=_RESULT_TYPES, enum_types=_RESULT_ENUMS, path="readiness result")
if problem is not None:
raise ReadinessValidationError(f"closed readiness graph violation: {problem}; exact tuple fields required")
if not isinstance(value, ReadinessResult):
raise ReadinessValidationError("readiness result must be a ReadinessResult")
_validate_result_fields(value)
+15
View File
@@ -6,6 +6,7 @@ from dataclasses import dataclass
from openultrasast.core import canonical
from openultrasast.core._validation_model import validate_and_replay_model_derivation
from openultrasast.core._validation_shape import evidence_context_problem
from openultrasast.core.custody import (
PopulationCustodyPolicy,
PopulationCustodyPort,
@@ -255,6 +256,9 @@ def _validate_context_members(context: EvidenceValidationContext) -> None:
def _validate_context(context: EvidenceValidationContext) -> None:
problem = evidence_context_problem(context, EvidenceValidationContext)
if problem is not None:
raise EvidenceInputValidationError(f"closed evidence context violation: {problem}")
if not isinstance(context, EvidenceValidationContext):
raise EvidenceInputValidationError("context must be an EvidenceValidationContext")
_validate_context_types(context)
@@ -266,6 +270,16 @@ def _validate_context(context: EvidenceValidationContext) -> None:
_validate_context_members(context)
def validate_evidence_validation_context(context: EvidenceValidationContext) -> None:
"""Re-derive a closed evidence context without requiring a case record."""
try:
_validate_context(context)
except EvidenceInputValidationError:
raise
except (AttributeError, LookupError, TypeError, ValueError) as exc:
raise EvidenceInputValidationError(f"evidence validation context is invalid: {exc}") from exc
def _validate_record_identity(record: EvidenceRecord) -> None:
if not isinstance(record, EvidenceRecord):
raise EvidenceInputValidationError("record must be an EvidenceRecord")
@@ -386,5 +400,6 @@ __all__ = [
"fact_set_bytes",
"make_evidence_validation_context",
"make_evidence_validation_input",
"validate_evidence_validation_context",
"validate_evidence_input",
]