mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 17:58:18 +00:00
feat(detection-core): complete single-writer v2 cutover
This commit is contained in:
@@ -398,7 +398,7 @@
|
|||||||
- _Requirements: 14.3, 15.3_
|
- _Requirements: 14.3, 15.3_
|
||||||
- _Boundary: Migration Tests and Operational Tooling_
|
- _Boundary: Migration Tests and Operational Tooling_
|
||||||
- _Depends: 10.4, 10.5_
|
- _Depends: 10.4, 10.5_
|
||||||
- [ ] 10.7 Delete v1 write capability and publish the single v2 surface atomically
|
- [x] 10.7 Delete v1 write capability and publish the single v2 surface atomically
|
||||||
- Require every inventory entry to be terminal and the migration-time production-consumer boundary to be zero before deletion begins.
|
- Require every inventory entry to be terminal and the migration-time production-consumer boundary to be zero before deletion begins.
|
||||||
- Delete the compatibility namespace, v1 writer facade, and temporary shims while publishing the canonical v2 modules and top-level exports in the same integration change.
|
- Delete the compatibility namespace, v1 writer facade, and temporary shims while publishing the canonical v2 modules and top-level exports in the same integration change.
|
||||||
- Prove old top-level and submodule imports and writes fail and that no v1 serializer or converter remains.
|
- Prove old top-level and submodule imports and writes fail and that no v1 serializer or converter remains.
|
||||||
|
|||||||
@@ -1,87 +1,5 @@
|
|||||||
"""Detection core: the shared security-trace contract package.
|
"""Family-neutral detection-core v2 public surface."""
|
||||||
|
|
||||||
Owns contracts only -- no analysis logic. Arms implement the trace contract;
|
from openultrasast.core import admission, consumer, verdict
|
||||||
runners and consumers adopt coverage, budget, provenance, and admission contracts.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from openultrasast.core.legacy_v1 import (
|
__all__ = ["admission", "consumer", "verdict"]
|
||||||
REPORT_SCHEMA_KEYS,
|
|
||||||
AdmissionManifest,
|
|
||||||
AdmissionManifestError,
|
|
||||||
BoundedSilence,
|
|
||||||
Budget,
|
|
||||||
BudgetGateFailure,
|
|
||||||
Complexity,
|
|
||||||
Coverage,
|
|
||||||
FactPack,
|
|
||||||
FactPackError,
|
|
||||||
FittedComponentBlocked,
|
|
||||||
Guard,
|
|
||||||
Hop,
|
|
||||||
InstrumentFailure,
|
|
||||||
Judgment,
|
|
||||||
ProbeResult,
|
|
||||||
ProvenanceMismatch,
|
|
||||||
ProvenClaim,
|
|
||||||
PushRequest,
|
|
||||||
PushVerdict,
|
|
||||||
ResolutionOutcome,
|
|
||||||
ShortcutDetected,
|
|
||||||
TraceRecord,
|
|
||||||
Witness,
|
|
||||||
admit_fact_pack,
|
|
||||||
as_clean,
|
|
||||||
assert_no_shortcut,
|
|
||||||
check_budget,
|
|
||||||
check_reads,
|
|
||||||
consume_verdict,
|
|
||||||
derive_budget,
|
|
||||||
require_admission_evidence,
|
|
||||||
reversed_pair_probe,
|
|
||||||
stamp,
|
|
||||||
validate_admission_manifest,
|
|
||||||
validate_report_schema,
|
|
||||||
verify_fact_pack,
|
|
||||||
verify_stamp,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"REPORT_SCHEMA_KEYS",
|
|
||||||
"AdmissionManifest",
|
|
||||||
"AdmissionManifestError",
|
|
||||||
"BoundedSilence",
|
|
||||||
"Budget",
|
|
||||||
"BudgetGateFailure",
|
|
||||||
"Complexity",
|
|
||||||
"Coverage",
|
|
||||||
"FactPack",
|
|
||||||
"FactPackError",
|
|
||||||
"FittedComponentBlocked",
|
|
||||||
"Guard",
|
|
||||||
"Hop",
|
|
||||||
"InstrumentFailure",
|
|
||||||
"Judgment",
|
|
||||||
"ProbeResult",
|
|
||||||
"ProvenClaim",
|
|
||||||
"ProvenanceMismatch",
|
|
||||||
"PushRequest",
|
|
||||||
"PushVerdict",
|
|
||||||
"ResolutionOutcome",
|
|
||||||
"ShortcutDetected",
|
|
||||||
"TraceRecord",
|
|
||||||
"Witness",
|
|
||||||
"admit_fact_pack",
|
|
||||||
"as_clean",
|
|
||||||
"assert_no_shortcut",
|
|
||||||
"check_budget",
|
|
||||||
"check_reads",
|
|
||||||
"consume_verdict",
|
|
||||||
"derive_budget",
|
|
||||||
"require_admission_evidence",
|
|
||||||
"reversed_pair_probe",
|
|
||||||
"stamp",
|
|
||||||
"validate_admission_manifest",
|
|
||||||
"validate_report_schema",
|
|
||||||
"verify_fact_pack",
|
|
||||||
"verify_stamp",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ from openultrasast.core.semantics import (
|
|||||||
SourceLocation,
|
SourceLocation,
|
||||||
)
|
)
|
||||||
from openultrasast.core.validation import EvidenceValidationInput
|
from openultrasast.core.validation import EvidenceValidationInput
|
||||||
from openultrasast.core.verdict_v2 import BoundedSatisfaction, BoundedSilence, DefectClaim
|
from openultrasast.core.verdict import BoundedSatisfaction, BoundedSilence, DefectClaim
|
||||||
|
|
||||||
_DATACLASSES: tuple[type[object], ...] = (
|
_DATACLASSES: tuple[type[object], ...] = (
|
||||||
ScopeKey,
|
ScopeKey,
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
"""Temporary explicit import shim for the pre-v2 fact-pack contract."""
|
|
||||||
|
|
||||||
from openultrasast.core.legacy_v1.contracts import (
|
|
||||||
FactPackError,
|
|
||||||
LegacyFactPack,
|
|
||||||
admit_fact_pack,
|
|
||||||
admit_pack_file,
|
|
||||||
load_pack_file,
|
|
||||||
verify_fact_pack,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"FactPackError",
|
|
||||||
"LegacyFactPack",
|
|
||||||
"admit_fact_pack",
|
|
||||||
"admit_pack_file",
|
|
||||||
"load_pack_file",
|
|
||||||
"verify_fact_pack",
|
|
||||||
]
|
|
||||||
@@ -16,7 +16,7 @@ from openultrasast.core.proof import (
|
|||||||
ProofVerificationReceipt,
|
ProofVerificationReceipt,
|
||||||
derive_producer_identity_digest,
|
derive_producer_identity_digest,
|
||||||
)
|
)
|
||||||
from openultrasast.core.verdict_v2 import DefectClaim
|
from openultrasast.core.verdict import DefectClaim
|
||||||
|
|
||||||
|
|
||||||
def _validate_claim_binding(claim: DefectClaim, attachment: ProofAttachment) -> None:
|
def _validate_claim_binding(claim: DefectClaim, attachment: ProofAttachment) -> None:
|
||||||
|
|||||||
@@ -1,17 +1,165 @@
|
|||||||
"""Temporary explicit import shim for the pre-v2 admission contract."""
|
"""Canonical v2 admission facade."""
|
||||||
|
|
||||||
from openultrasast.core.legacy_v1.contracts import (
|
from openultrasast.core.admission_case import (
|
||||||
REPORT_SCHEMA_KEYS,
|
ADMISSION_CASE_RELEASE_PAYLOAD_SCHEMA,
|
||||||
AdmissionManifest,
|
ADMISSION_CASE_RELEASE_SCHEMA,
|
||||||
AdmissionManifestError,
|
RELEASED_CASE_BINDING_SCHEMA,
|
||||||
validate_admission_manifest,
|
AdmissionCaseRelease,
|
||||||
validate_report_schema,
|
AdmissionCaseReleaseError,
|
||||||
|
AdmissionCaseResult,
|
||||||
|
AdmissionCaseRole,
|
||||||
|
ReleasedCaseBinding,
|
||||||
|
create_admission_case_result,
|
||||||
|
derive_admission_case_release_digest,
|
||||||
|
derive_admission_case_release_payload_digest,
|
||||||
|
derive_admission_case_result_digest,
|
||||||
|
derive_case_coverage_digest,
|
||||||
|
derive_released_case_binding_digest,
|
||||||
|
validate_admission_case_release,
|
||||||
|
validate_admission_case_shape,
|
||||||
|
)
|
||||||
|
from openultrasast.core.admission_context import (
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
from openultrasast.core.admission_feasibility import (
|
||||||
|
AdmissionFeasibilityError,
|
||||||
|
FeasibleCountAssignment,
|
||||||
|
ThresholdFeasibility,
|
||||||
|
evaluate_threshold_feasibility,
|
||||||
|
)
|
||||||
|
from openultrasast.core.admission_fitted import (
|
||||||
|
REPOSITORY_POPULATION_MANIFEST_SCHEMA,
|
||||||
|
FittedComponentAuthorizationError,
|
||||||
|
FittedComponentEvidence,
|
||||||
|
authorize_fitted_component,
|
||||||
|
)
|
||||||
|
from openultrasast.core.admission_metrics import (
|
||||||
|
AdmissionMetricEvidence,
|
||||||
|
AdmissionMetricsError,
|
||||||
|
derive_admission_metrics,
|
||||||
|
derive_scoped_pair_key,
|
||||||
|
)
|
||||||
|
from openultrasast.core.admission_report import (
|
||||||
|
ADMISSION_REPLAY_SCHEMA,
|
||||||
|
ADMISSION_REPORT_SCHEMA,
|
||||||
|
AdmissionDecision,
|
||||||
|
AdmissionReplay,
|
||||||
|
AdmissionReportError,
|
||||||
|
AdmissionReportV2,
|
||||||
|
CaseBudgetEvaluation,
|
||||||
|
GateKind,
|
||||||
|
GateResult,
|
||||||
|
ThresholdEvaluation,
|
||||||
|
admission_replay_value,
|
||||||
|
admission_report_value,
|
||||||
|
score_admission,
|
||||||
|
validate_admission_replay,
|
||||||
|
)
|
||||||
|
from openultrasast.core.admission_review import (
|
||||||
|
AdmissionManifestV2,
|
||||||
|
AdmissionReviewError,
|
||||||
|
AdmissionThreshold,
|
||||||
|
PopulationCensus,
|
||||||
|
ScopeCensus,
|
||||||
|
derive_census_digest,
|
||||||
|
derive_census_payload_digest,
|
||||||
|
derive_manifest_digest,
|
||||||
|
derive_manifest_payload_digest,
|
||||||
|
derive_threshold_set_digest,
|
||||||
|
validate_reviewed_admission_payloads,
|
||||||
|
)
|
||||||
|
from openultrasast.core.admission_spend import (
|
||||||
|
ADMISSION_AUTHORIZATION_SCHEMA,
|
||||||
|
ADMISSION_PREPARATION_SCHEMA,
|
||||||
|
AdmissionAuthorization,
|
||||||
|
AdmissionPreparation,
|
||||||
|
AdmissionSpendError,
|
||||||
|
authorize_admission,
|
||||||
|
derive_admission_authorization_digest,
|
||||||
|
derive_admission_preparation_digest,
|
||||||
|
validate_pre_label_spend,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"REPORT_SCHEMA_KEYS",
|
"ADMISSION_AUTHORIZATION_SCHEMA",
|
||||||
"AdmissionManifest",
|
"ADMISSION_CASE_RELEASE_PAYLOAD_SCHEMA",
|
||||||
"AdmissionManifestError",
|
"ADMISSION_CASE_RELEASE_SCHEMA",
|
||||||
"validate_admission_manifest",
|
"ADMISSION_PREPARATION_SCHEMA",
|
||||||
"validate_report_schema",
|
"ADMISSION_REPLAY_SCHEMA",
|
||||||
|
"ADMISSION_REPORT_SCHEMA",
|
||||||
|
"ADMISSION_VALIDATION_CONTEXT_SCHEMA",
|
||||||
|
"RELEASED_CASE_BINDING_SCHEMA",
|
||||||
|
"REPOSITORY_POPULATION_MANIFEST_SCHEMA",
|
||||||
|
"AdmissionAuthorization",
|
||||||
|
"AdmissionCaseRelease",
|
||||||
|
"AdmissionCaseReleaseError",
|
||||||
|
"AdmissionCaseResult",
|
||||||
|
"AdmissionCaseRole",
|
||||||
|
"AdmissionContextError",
|
||||||
|
"AdmissionDecision",
|
||||||
|
"AdmissionFeasibilityError",
|
||||||
|
"AdmissionManifestV2",
|
||||||
|
"AdmissionMetricEvidence",
|
||||||
|
"AdmissionMetricsError",
|
||||||
|
"AdmissionPreparation",
|
||||||
|
"AdmissionReplay",
|
||||||
|
"AdmissionReportError",
|
||||||
|
"AdmissionReportV2",
|
||||||
|
"AdmissionReviewError",
|
||||||
|
"AdmissionSpendError",
|
||||||
|
"AdmissionThreshold",
|
||||||
|
"AdmissionValidationContext",
|
||||||
|
"CaseBudgetEvaluation",
|
||||||
|
"FeasibleCountAssignment",
|
||||||
|
"FittedComponentAuthorizationError",
|
||||||
|
"FittedComponentEvidence",
|
||||||
|
"GateKind",
|
||||||
|
"GateResult",
|
||||||
|
"PopulationCensus",
|
||||||
|
"ReleasedCaseBinding",
|
||||||
|
"ScopeCensus",
|
||||||
|
"ThresholdEvaluation",
|
||||||
|
"ThresholdFeasibility",
|
||||||
|
"admission_replay_value",
|
||||||
|
"admission_report_value",
|
||||||
|
"authorize_admission",
|
||||||
|
"authorize_fitted_component",
|
||||||
|
"create_admission_case_result",
|
||||||
|
"derive_admission_authorization_digest",
|
||||||
|
"derive_admission_case_release_digest",
|
||||||
|
"derive_admission_case_release_payload_digest",
|
||||||
|
"derive_admission_case_result_digest",
|
||||||
|
"derive_admission_preparation_digest",
|
||||||
|
"derive_admission_metrics",
|
||||||
|
"derive_admission_validation_context_digest",
|
||||||
|
"derive_analysis_identity",
|
||||||
|
"derive_case_coverage_digest",
|
||||||
|
"derive_census_digest",
|
||||||
|
"derive_census_payload_digest",
|
||||||
|
"derive_fact_schema_digest",
|
||||||
|
"derive_manifest_digest",
|
||||||
|
"derive_manifest_payload_digest",
|
||||||
|
"derive_outcome_mapping_digest",
|
||||||
|
"derive_released_case_binding_digest",
|
||||||
|
"derive_scoped_pair_key",
|
||||||
|
"derive_semantic_vocabulary_digest",
|
||||||
|
"derive_threshold_set_digest",
|
||||||
|
"evaluate_threshold_feasibility",
|
||||||
|
"make_admission_validation_context",
|
||||||
|
"score_admission",
|
||||||
|
"validate_admission_case_release",
|
||||||
|
"validate_admission_case_shape",
|
||||||
|
"validate_admission_context",
|
||||||
|
"validate_admission_replay",
|
||||||
|
"validate_pre_label_spend",
|
||||||
|
"validate_reviewed_admission_payloads",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from openultrasast.core.measurement import DenominatorPolicy, ScopeKey
|
|||||||
from openultrasast.core.proof import ProofAttachment
|
from openultrasast.core.proof import ProofAttachment
|
||||||
from openultrasast.core.readiness import ReadinessResult
|
from openultrasast.core.readiness import ReadinessResult
|
||||||
from openultrasast.core.validation import EvidenceValidationInput
|
from openultrasast.core.validation import EvidenceValidationInput
|
||||||
from openultrasast.core.verdict_v2 import BoundedSatisfaction, BoundedSilence, DefectClaim
|
from openultrasast.core.verdict import BoundedSatisfaction, BoundedSilence, DefectClaim
|
||||||
|
|
||||||
ADMISSION_CASE_RELEASE_SCHEMA = "openultrasast.detection-core.admission-case-release.v2"
|
ADMISSION_CASE_RELEASE_SCHEMA = "openultrasast.detection-core.admission-case-release.v2"
|
||||||
ADMISSION_CASE_RELEASE_PAYLOAD_SCHEMA = "openultrasast.detection-core.admission-case-release-payload.v2"
|
ADMISSION_CASE_RELEASE_PAYLOAD_SCHEMA = "openultrasast.detection-core.admission-case-release-payload.v2"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from openultrasast.core.model import ModelStatus
|
|||||||
from openultrasast.core.proof import ProofAttachment, attach_differential_proof
|
from openultrasast.core.proof import ProofAttachment, attach_differential_proof
|
||||||
from openultrasast.core.readiness import ReadinessResult
|
from openultrasast.core.readiness import ReadinessResult
|
||||||
from openultrasast.core.validation import validate_evidence_input
|
from openultrasast.core.validation import validate_evidence_input
|
||||||
from openultrasast.core.verdict_v2 import DefectClaim, derive_verdict_outputs
|
from openultrasast.core.verdict import DefectClaim, derive_verdict_outputs
|
||||||
|
|
||||||
|
|
||||||
class AdmissionMetricsError(ValueError):
|
class AdmissionMetricsError(ValueError):
|
||||||
|
|||||||
@@ -1,5 +1,340 @@
|
|||||||
"""Temporary explicit import shim for the pre-v2 consumer contract."""
|
"""Closed v2 push-consumer edge for validated bounded verdicts."""
|
||||||
|
|
||||||
from openultrasast.core.legacy_v1.contracts import PushRequest, PushVerdict, consume_verdict
|
from __future__ import annotations
|
||||||
|
|
||||||
__all__ = ["PushRequest", "PushVerdict", "consume_verdict"]
|
import time
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass, replace
|
||||||
|
|
||||||
|
from openultrasast.core import canonical, verdict
|
||||||
|
from openultrasast.core.budget import BudgetDerivation, BudgetGateFailure, ChangeComplexity, check_budget
|
||||||
|
from openultrasast.core.evidence import AnalyzedScope, EvidenceRecord, SourceRegion, derive_packet_digest, validate_scope
|
||||||
|
from openultrasast.core.family import FamilyContract, family_contract_digest
|
||||||
|
from openultrasast.core.family_catalog import resolve_contract
|
||||||
|
from openultrasast.core.proof import ProofAttachment, ProofTrustPolicy, attach_differential_proof
|
||||||
|
from openultrasast.core.provenance import PopulationCustodyPort
|
||||||
|
from openultrasast.core.semantics import SourceLocation
|
||||||
|
from openultrasast.core.validation import EvidenceValidationContext, EvidenceValidationInput, validate_evidence_input
|
||||||
|
from openultrasast.core.verdict import (
|
||||||
|
BoundedSatisfaction,
|
||||||
|
DefectClaim,
|
||||||
|
VerdictOutputs,
|
||||||
|
derive_verdict_outputs,
|
||||||
|
)
|
||||||
|
|
||||||
|
PUSH_REQUEST_SCHEMA = "openultrasast.detection-core.push-request.v2"
|
||||||
|
CONSUMER_COVERAGE_SCHEMA = "openultrasast.detection-core.consumer-coverage.v2"
|
||||||
|
_monotonic_ns = time.monotonic_ns
|
||||||
|
|
||||||
|
|
||||||
|
class ConsumerContractError(ValueError):
|
||||||
|
"""A raw push request cannot produce a bounded v2 verdict."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PushRequest:
|
||||||
|
request_id: str
|
||||||
|
scope: AnalyzedScope
|
||||||
|
workload_class: str
|
||||||
|
complexity: ChangeComplexity
|
||||||
|
budget: BudgetDerivation
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PushVerdict:
|
||||||
|
request_digest: str
|
||||||
|
defects: tuple[DefectClaim, ...]
|
||||||
|
satisfactions: tuple[BoundedSatisfaction, ...]
|
||||||
|
silence: tuple[verdict.BoundedSilence, ...]
|
||||||
|
input_record_digests: tuple[str, ...]
|
||||||
|
complexity: ChangeComplexity
|
||||||
|
total_reply_ms: int
|
||||||
|
budget_derivation_digest: str
|
||||||
|
|
||||||
|
|
||||||
|
def _content_ref_value(value: canonical.ContentRef) -> canonical.JsonValue:
|
||||||
|
return {
|
||||||
|
"artifact_id": value.artifact_id,
|
||||||
|
"content_digest": value.content_digest,
|
||||||
|
"media_type": value.media_type,
|
||||||
|
"byte_length": value.byte_length,
|
||||||
|
"retention_policy_digest": value.retention_policy_digest,
|
||||||
|
"storage_receipt_digest": value.storage_receipt_digest,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _scope_value(value: AnalyzedScope) -> canonical.JsonValue:
|
||||||
|
return {
|
||||||
|
"base_revision": value.base_revision,
|
||||||
|
"head_revision": value.head_revision,
|
||||||
|
"change_digest": value.change_digest,
|
||||||
|
"regions": tuple({"path": region.path, "start_line": region.start_line, "end_line": region.end_line} for region in value.regions),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _request_value(request: PushRequest) -> canonical.JsonValue:
|
||||||
|
complexity = request.complexity
|
||||||
|
return {
|
||||||
|
"schema_version": PUSH_REQUEST_SCHEMA,
|
||||||
|
"scope": _scope_value(request.scope),
|
||||||
|
"workload_class": request.workload_class,
|
||||||
|
"complexity": {
|
||||||
|
"change_id": complexity.change_id,
|
||||||
|
"workload_class": complexity.workload_class,
|
||||||
|
"changed_regions": complexity.changed_regions,
|
||||||
|
"transition_count": complexity.transition_count,
|
||||||
|
"changed_bytes": complexity.changed_bytes,
|
||||||
|
"measurement": _content_ref_value(complexity.measurement),
|
||||||
|
},
|
||||||
|
"budget_derivation_digest": request.budget.derivation_digest,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def derive_push_request_id(request: PushRequest) -> str:
|
||||||
|
if not isinstance(request, PushRequest):
|
||||||
|
raise ConsumerContractError("request must be a PushRequest")
|
||||||
|
return canonical.content_digest(_request_value(request))
|
||||||
|
|
||||||
|
|
||||||
|
def make_push_request(
|
||||||
|
*,
|
||||||
|
scope: AnalyzedScope,
|
||||||
|
workload_class: str,
|
||||||
|
complexity: ChangeComplexity,
|
||||||
|
budget: BudgetDerivation,
|
||||||
|
) -> PushRequest:
|
||||||
|
provisional = PushRequest("0" * 64, scope, workload_class, complexity, budget)
|
||||||
|
return PushRequest(derive_push_request_id(provisional), scope, workload_class, complexity, budget)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_request(request: PushRequest) -> None:
|
||||||
|
if not isinstance(request, PushRequest):
|
||||||
|
raise ConsumerContractError("request must be a PushRequest")
|
||||||
|
try:
|
||||||
|
request.scope.__post_init__()
|
||||||
|
request.complexity.__post_init__()
|
||||||
|
request.budget.__post_init__()
|
||||||
|
except (AttributeError, TypeError, ValueError) as exc:
|
||||||
|
raise ConsumerContractError(f"push request is invalid: {exc}") from exc
|
||||||
|
if request.request_id != derive_push_request_id(request):
|
||||||
|
raise ConsumerContractError("push request identity cannot be re-derived")
|
||||||
|
if request.workload_class != request.complexity.workload_class or request.workload_class != request.budget.workload_class:
|
||||||
|
raise ConsumerContractError("push workload class does not match complexity and budget")
|
||||||
|
if request.complexity.change_id != request.scope.change_digest:
|
||||||
|
raise ConsumerContractError("complexity change identity does not match request scope")
|
||||||
|
if request.complexity.changed_regions != len(request.scope.regions):
|
||||||
|
raise ConsumerContractError("complexity changed-region count does not match request scope")
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_records(
|
||||||
|
values: Sequence[EvidenceValidationInput],
|
||||||
|
context: EvidenceValidationContext,
|
||||||
|
custody: PopulationCustodyPort,
|
||||||
|
resolver: canonical.ContentResolver,
|
||||||
|
) -> tuple[EvidenceRecord, ...]:
|
||||||
|
selected = tuple(values)
|
||||||
|
if not selected:
|
||||||
|
raise ConsumerContractError("push inputs must be nonempty")
|
||||||
|
if not all(isinstance(value, EvidenceValidationInput) for value in selected):
|
||||||
|
raise ConsumerContractError("every push input must be an EvidenceValidationInput")
|
||||||
|
input_ids = tuple(value.input_digest for value in selected)
|
||||||
|
if len(set(input_ids)) != len(input_ids):
|
||||||
|
raise ConsumerContractError("duplicate validation input identity")
|
||||||
|
try:
|
||||||
|
return tuple(validate_evidence_input(value, context, custody, resolver) for value in selected)
|
||||||
|
except (AttributeError, LookupError, TypeError, ValueError) as exc:
|
||||||
|
raise ConsumerContractError(f"evidence validation failed: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _contract(context: EvidenceValidationContext, record: EvidenceRecord) -> FamilyContract:
|
||||||
|
packet = record.packet
|
||||||
|
try:
|
||||||
|
return resolve_contract(context.family_catalog, packet.family_id, packet.family_contract_digest, packet.ecosystem)
|
||||||
|
except (LookupError, TypeError, ValueError) as exc:
|
||||||
|
raise ConsumerContractError(f"record family cannot be resolved: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _coverage_digest(request: PushRequest, contract: FamilyContract, records: tuple[EvidenceRecord, ...]) -> str:
|
||||||
|
return canonical.content_digest(
|
||||||
|
{
|
||||||
|
"schema_version": CONSUMER_COVERAGE_SCHEMA,
|
||||||
|
"request_id": request.request_id,
|
||||||
|
"family_contract_digest": family_contract_digest(contract),
|
||||||
|
"record_ids": tuple(sorted(record.record_id for record in records)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _derive_outputs(
|
||||||
|
request: PushRequest,
|
||||||
|
records: tuple[EvidenceRecord, ...],
|
||||||
|
context: EvidenceValidationContext,
|
||||||
|
) -> VerdictOutputs:
|
||||||
|
grouped: dict[str, tuple[FamilyContract, list[EvidenceRecord]]] = {}
|
||||||
|
for record in records:
|
||||||
|
try:
|
||||||
|
validate_scope(record.packet, request.scope)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ConsumerContractError(f"record scope is outside the push request: {exc}") from exc
|
||||||
|
contract = _contract(context, record)
|
||||||
|
digest = record.packet.family_contract_digest
|
||||||
|
if digest not in grouped:
|
||||||
|
grouped[digest] = (contract, [])
|
||||||
|
grouped[digest][1].append(record)
|
||||||
|
defects: list[DefectClaim] = []
|
||||||
|
satisfactions: list[BoundedSatisfaction] = []
|
||||||
|
silence: list[verdict.BoundedSilence] = []
|
||||||
|
try:
|
||||||
|
for digest in sorted(grouped):
|
||||||
|
contract, members = grouped[digest]
|
||||||
|
selected = tuple(members)
|
||||||
|
outputs = derive_verdict_outputs(selected, contract, _coverage_digest(request, contract, selected))
|
||||||
|
defects.extend(outputs.defects)
|
||||||
|
satisfactions.extend(outputs.satisfactions)
|
||||||
|
silence.extend(outputs.silence)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ConsumerContractError(f"bounded verdict derivation failed: {exc}") from exc
|
||||||
|
return VerdictOutputs(tuple(defects), tuple(satisfactions), tuple(silence))
|
||||||
|
|
||||||
|
|
||||||
|
def _region_within(allowed: AnalyzedScope, candidate: SourceRegion) -> bool:
|
||||||
|
return any(
|
||||||
|
candidate.path == region.path and candidate.start_line >= region.start_line and candidate.end_line <= region.end_line
|
||||||
|
for region in allowed.regions
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _proof_target_index(claims: tuple[DefectClaim, ...]) -> dict[tuple[str, str, SourceLocation], int]:
|
||||||
|
keys: dict[tuple[str, str, SourceLocation], int] = {}
|
||||||
|
for index, claim in enumerate(claims):
|
||||||
|
key = (claim.record.packet.family_id, derive_packet_digest(claim.record.packet), claim.location)
|
||||||
|
if key in keys:
|
||||||
|
raise ConsumerContractError("proof target is ambiguous across defect claims")
|
||||||
|
keys[key] = index
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_attachment(
|
||||||
|
request: PushRequest,
|
||||||
|
result: list[DefectClaim],
|
||||||
|
keys: dict[tuple[str, str, SourceLocation], int],
|
||||||
|
consumed: set[int],
|
||||||
|
attachment: ProofAttachment,
|
||||||
|
trust: ProofTrustPolicy,
|
||||||
|
resolver: canonical.ContentResolver,
|
||||||
|
) -> None:
|
||||||
|
if not isinstance(attachment, ProofAttachment):
|
||||||
|
raise ConsumerContractError("every proof attachment must use the v2 proof contract")
|
||||||
|
key = (attachment.family_id, attachment.packet_digest, attachment.location)
|
||||||
|
if key not in keys:
|
||||||
|
raise ConsumerContractError("proof attachment does not match one exact defect claim")
|
||||||
|
index = keys[key]
|
||||||
|
if index in consumed:
|
||||||
|
raise ConsumerContractError("multiple proof attachments target one defect claim")
|
||||||
|
if not _region_within(request.scope, result[index].change_anchor):
|
||||||
|
raise ConsumerContractError("proof claim anchor is outside the push request scope")
|
||||||
|
result[index] = attach_differential_proof(result[index], attachment, trust, resolver)
|
||||||
|
consumed.add(index)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_proofs(
|
||||||
|
request: PushRequest,
|
||||||
|
claims: tuple[DefectClaim, ...],
|
||||||
|
attachments: Sequence[ProofAttachment],
|
||||||
|
trust: ProofTrustPolicy | None,
|
||||||
|
resolver: canonical.ContentResolver,
|
||||||
|
) -> tuple[DefectClaim, ...]:
|
||||||
|
selected = tuple(attachments)
|
||||||
|
if not selected:
|
||||||
|
return claims
|
||||||
|
if not isinstance(trust, ProofTrustPolicy):
|
||||||
|
raise ConsumerContractError("nonempty proof attachments require a proof trust policy")
|
||||||
|
keys = _proof_target_index(claims)
|
||||||
|
result = list(claims)
|
||||||
|
consumed: set[int] = set()
|
||||||
|
try:
|
||||||
|
for attachment in selected:
|
||||||
|
_apply_attachment(request, result, keys, consumed, attachment, trust, resolver)
|
||||||
|
except ConsumerContractError:
|
||||||
|
raise
|
||||||
|
except (AttributeError, LookupError, TypeError, ValueError) as exc:
|
||||||
|
raise ConsumerContractError(f"proof validation failed: {exc}") from exc
|
||||||
|
return tuple(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _total_reply_ms(elapsed_before_entry_ms: int, started_ns: int) -> int:
|
||||||
|
if isinstance(elapsed_before_entry_ms, bool) or not isinstance(elapsed_before_entry_ms, int) or elapsed_before_entry_ms < 0:
|
||||||
|
raise ConsumerContractError("elapsed-before-entry measurement must be a non-negative integer")
|
||||||
|
finished_ns = _monotonic_ns()
|
||||||
|
ticks = (started_ns, finished_ns)
|
||||||
|
if any(isinstance(tick, bool) or not isinstance(tick, int) or tick < 0 for tick in ticks):
|
||||||
|
raise ConsumerContractError("monotonic core timing ticks must be non-negative integers")
|
||||||
|
if finished_ns < started_ns:
|
||||||
|
raise ConsumerContractError("monotonic core timing measurement moved backwards")
|
||||||
|
core_elapsed_ms = (finished_ns - started_ns + 999_999) // 1_000_000
|
||||||
|
total_reply_ms = elapsed_before_entry_ms + core_elapsed_ms
|
||||||
|
if total_reply_ms <= 0:
|
||||||
|
raise ConsumerContractError("combined elapsed measurement must be a strictly positive integer")
|
||||||
|
return total_reply_ms
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_budget_artifacts(request: PushRequest, resolver: canonical.ContentResolver) -> None:
|
||||||
|
try:
|
||||||
|
check_budget(request.complexity, 1, request.budget, resolver)
|
||||||
|
except (BudgetGateFailure, AttributeError, LookupError, TypeError, ValueError) as exc:
|
||||||
|
raise ConsumerContractError(f"measured budget validation failed: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _enforce_validated_deadline(request: PushRequest, total_reply_ms: int) -> None:
|
||||||
|
if total_reply_ms > request.budget.budget_ms:
|
||||||
|
raise ConsumerContractError(
|
||||||
|
f"measured budget gate failed: budget:{request.complexity.change_id}:"
|
||||||
|
f"{total_reply_ms}ms>{request.budget.budget_ms}ms "
|
||||||
|
f"class={request.budget.workload_class} derivation={request.budget.derivation_digest}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def consume_records(
|
||||||
|
request: PushRequest,
|
||||||
|
inputs: Sequence[EvidenceValidationInput],
|
||||||
|
validation_context: EvidenceValidationContext,
|
||||||
|
custody: PopulationCustodyPort,
|
||||||
|
elapsed_before_entry_ms: int,
|
||||||
|
resolver: canonical.ContentResolver,
|
||||||
|
attachments: Sequence[ProofAttachment] = (),
|
||||||
|
proof_trust: ProofTrustPolicy | None = None,
|
||||||
|
) -> PushVerdict:
|
||||||
|
started_ns = _monotonic_ns()
|
||||||
|
_validate_request(request)
|
||||||
|
records = _validated_records(inputs, validation_context, custody, resolver)
|
||||||
|
transition_count = sum(len(record.packet.transitions) for record in records)
|
||||||
|
if request.complexity.transition_count != transition_count:
|
||||||
|
raise ConsumerContractError("complexity transition count does not match validated records")
|
||||||
|
outputs = _derive_outputs(request, records, validation_context)
|
||||||
|
defects = _apply_proofs(request, outputs.defects, attachments, proof_trust, resolver)
|
||||||
|
if not defects and not outputs.satisfactions and not outputs.silence:
|
||||||
|
raise ConsumerContractError("push verdict cannot be an unbounded clean result")
|
||||||
|
_validate_budget_artifacts(request, resolver)
|
||||||
|
provisional = PushVerdict(
|
||||||
|
request.request_id,
|
||||||
|
defects,
|
||||||
|
outputs.satisfactions,
|
||||||
|
outputs.silence,
|
||||||
|
tuple(sorted(record.record_id for record in records)),
|
||||||
|
request.complexity,
|
||||||
|
0,
|
||||||
|
request.budget.derivation_digest,
|
||||||
|
)
|
||||||
|
total_reply_ms = _total_reply_ms(elapsed_before_entry_ms, started_ns)
|
||||||
|
_enforce_validated_deadline(request, total_reply_ms)
|
||||||
|
return replace(provisional, total_reply_ms=total_reply_ms)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ConsumerContractError",
|
||||||
|
"PushRequest",
|
||||||
|
"PushVerdict",
|
||||||
|
"consume_records",
|
||||||
|
"derive_push_request_id",
|
||||||
|
"make_push_request",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,341 +0,0 @@
|
|||||||
"""Closed v2 push-consumer edge for validated bounded verdicts."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import time
|
|
||||||
from collections.abc import Sequence
|
|
||||||
from dataclasses import dataclass, replace
|
|
||||||
|
|
||||||
from openultrasast.core import canonical
|
|
||||||
from openultrasast.core.budget import BudgetDerivation, BudgetGateFailure, ChangeComplexity, check_budget
|
|
||||||
from openultrasast.core.evidence import AnalyzedScope, EvidenceRecord, SourceRegion, derive_packet_digest, validate_scope
|
|
||||||
from openultrasast.core.family import FamilyContract, family_contract_digest
|
|
||||||
from openultrasast.core.family_catalog import resolve_contract
|
|
||||||
from openultrasast.core.proof import ProofAttachment, ProofTrustPolicy, attach_differential_proof
|
|
||||||
from openultrasast.core.provenance import PopulationCustodyPort
|
|
||||||
from openultrasast.core.semantics import SourceLocation
|
|
||||||
from openultrasast.core.validation import EvidenceValidationContext, EvidenceValidationInput, validate_evidence_input
|
|
||||||
from openultrasast.core.verdict_v2 import (
|
|
||||||
BoundedSatisfaction,
|
|
||||||
BoundedSilence,
|
|
||||||
DefectClaim,
|
|
||||||
VerdictOutputs,
|
|
||||||
derive_verdict_outputs,
|
|
||||||
)
|
|
||||||
|
|
||||||
PUSH_REQUEST_SCHEMA = "openultrasast.detection-core.push-request.v2"
|
|
||||||
CONSUMER_COVERAGE_SCHEMA = "openultrasast.detection-core.consumer-coverage.v2"
|
|
||||||
_monotonic_ns = time.monotonic_ns
|
|
||||||
|
|
||||||
|
|
||||||
class ConsumerContractError(ValueError):
|
|
||||||
"""A raw push request cannot produce a bounded v2 verdict."""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PushRequest:
|
|
||||||
request_id: str
|
|
||||||
scope: AnalyzedScope
|
|
||||||
workload_class: str
|
|
||||||
complexity: ChangeComplexity
|
|
||||||
budget: BudgetDerivation
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PushVerdict:
|
|
||||||
request_digest: str
|
|
||||||
defects: tuple[DefectClaim, ...]
|
|
||||||
satisfactions: tuple[BoundedSatisfaction, ...]
|
|
||||||
silence: tuple[BoundedSilence, ...]
|
|
||||||
input_record_digests: tuple[str, ...]
|
|
||||||
complexity: ChangeComplexity
|
|
||||||
total_reply_ms: int
|
|
||||||
budget_derivation_digest: str
|
|
||||||
|
|
||||||
|
|
||||||
def _content_ref_value(value: canonical.ContentRef) -> canonical.JsonValue:
|
|
||||||
return {
|
|
||||||
"artifact_id": value.artifact_id,
|
|
||||||
"content_digest": value.content_digest,
|
|
||||||
"media_type": value.media_type,
|
|
||||||
"byte_length": value.byte_length,
|
|
||||||
"retention_policy_digest": value.retention_policy_digest,
|
|
||||||
"storage_receipt_digest": value.storage_receipt_digest,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _scope_value(value: AnalyzedScope) -> canonical.JsonValue:
|
|
||||||
return {
|
|
||||||
"base_revision": value.base_revision,
|
|
||||||
"head_revision": value.head_revision,
|
|
||||||
"change_digest": value.change_digest,
|
|
||||||
"regions": tuple({"path": region.path, "start_line": region.start_line, "end_line": region.end_line} for region in value.regions),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _request_value(request: PushRequest) -> canonical.JsonValue:
|
|
||||||
complexity = request.complexity
|
|
||||||
return {
|
|
||||||
"schema_version": PUSH_REQUEST_SCHEMA,
|
|
||||||
"scope": _scope_value(request.scope),
|
|
||||||
"workload_class": request.workload_class,
|
|
||||||
"complexity": {
|
|
||||||
"change_id": complexity.change_id,
|
|
||||||
"workload_class": complexity.workload_class,
|
|
||||||
"changed_regions": complexity.changed_regions,
|
|
||||||
"transition_count": complexity.transition_count,
|
|
||||||
"changed_bytes": complexity.changed_bytes,
|
|
||||||
"measurement": _content_ref_value(complexity.measurement),
|
|
||||||
},
|
|
||||||
"budget_derivation_digest": request.budget.derivation_digest,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def derive_push_request_id(request: PushRequest) -> str:
|
|
||||||
if not isinstance(request, PushRequest):
|
|
||||||
raise ConsumerContractError("request must be a PushRequest")
|
|
||||||
return canonical.content_digest(_request_value(request))
|
|
||||||
|
|
||||||
|
|
||||||
def make_push_request(
|
|
||||||
*,
|
|
||||||
scope: AnalyzedScope,
|
|
||||||
workload_class: str,
|
|
||||||
complexity: ChangeComplexity,
|
|
||||||
budget: BudgetDerivation,
|
|
||||||
) -> PushRequest:
|
|
||||||
provisional = PushRequest("0" * 64, scope, workload_class, complexity, budget)
|
|
||||||
return PushRequest(derive_push_request_id(provisional), scope, workload_class, complexity, budget)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_request(request: PushRequest) -> None:
|
|
||||||
if not isinstance(request, PushRequest):
|
|
||||||
raise ConsumerContractError("request must be a PushRequest")
|
|
||||||
try:
|
|
||||||
request.scope.__post_init__()
|
|
||||||
request.complexity.__post_init__()
|
|
||||||
request.budget.__post_init__()
|
|
||||||
except (AttributeError, TypeError, ValueError) as exc:
|
|
||||||
raise ConsumerContractError(f"push request is invalid: {exc}") from exc
|
|
||||||
if request.request_id != derive_push_request_id(request):
|
|
||||||
raise ConsumerContractError("push request identity cannot be re-derived")
|
|
||||||
if request.workload_class != request.complexity.workload_class or request.workload_class != request.budget.workload_class:
|
|
||||||
raise ConsumerContractError("push workload class does not match complexity and budget")
|
|
||||||
if request.complexity.change_id != request.scope.change_digest:
|
|
||||||
raise ConsumerContractError("complexity change identity does not match request scope")
|
|
||||||
if request.complexity.changed_regions != len(request.scope.regions):
|
|
||||||
raise ConsumerContractError("complexity changed-region count does not match request scope")
|
|
||||||
|
|
||||||
|
|
||||||
def _validated_records(
|
|
||||||
values: Sequence[EvidenceValidationInput],
|
|
||||||
context: EvidenceValidationContext,
|
|
||||||
custody: PopulationCustodyPort,
|
|
||||||
resolver: canonical.ContentResolver,
|
|
||||||
) -> tuple[EvidenceRecord, ...]:
|
|
||||||
selected = tuple(values)
|
|
||||||
if not selected:
|
|
||||||
raise ConsumerContractError("push inputs must be nonempty")
|
|
||||||
if not all(isinstance(value, EvidenceValidationInput) for value in selected):
|
|
||||||
raise ConsumerContractError("every push input must be an EvidenceValidationInput")
|
|
||||||
input_ids = tuple(value.input_digest for value in selected)
|
|
||||||
if len(set(input_ids)) != len(input_ids):
|
|
||||||
raise ConsumerContractError("duplicate validation input identity")
|
|
||||||
try:
|
|
||||||
return tuple(validate_evidence_input(value, context, custody, resolver) for value in selected)
|
|
||||||
except (AttributeError, LookupError, TypeError, ValueError) as exc:
|
|
||||||
raise ConsumerContractError(f"evidence validation failed: {exc}") from exc
|
|
||||||
|
|
||||||
|
|
||||||
def _contract(context: EvidenceValidationContext, record: EvidenceRecord) -> FamilyContract:
|
|
||||||
packet = record.packet
|
|
||||||
try:
|
|
||||||
return resolve_contract(context.family_catalog, packet.family_id, packet.family_contract_digest, packet.ecosystem)
|
|
||||||
except (LookupError, TypeError, ValueError) as exc:
|
|
||||||
raise ConsumerContractError(f"record family cannot be resolved: {exc}") from exc
|
|
||||||
|
|
||||||
|
|
||||||
def _coverage_digest(request: PushRequest, contract: FamilyContract, records: tuple[EvidenceRecord, ...]) -> str:
|
|
||||||
return canonical.content_digest(
|
|
||||||
{
|
|
||||||
"schema_version": CONSUMER_COVERAGE_SCHEMA,
|
|
||||||
"request_id": request.request_id,
|
|
||||||
"family_contract_digest": family_contract_digest(contract),
|
|
||||||
"record_ids": tuple(sorted(record.record_id for record in records)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _derive_outputs(
|
|
||||||
request: PushRequest,
|
|
||||||
records: tuple[EvidenceRecord, ...],
|
|
||||||
context: EvidenceValidationContext,
|
|
||||||
) -> VerdictOutputs:
|
|
||||||
grouped: dict[str, tuple[FamilyContract, list[EvidenceRecord]]] = {}
|
|
||||||
for record in records:
|
|
||||||
try:
|
|
||||||
validate_scope(record.packet, request.scope)
|
|
||||||
except (TypeError, ValueError) as exc:
|
|
||||||
raise ConsumerContractError(f"record scope is outside the push request: {exc}") from exc
|
|
||||||
contract = _contract(context, record)
|
|
||||||
digest = record.packet.family_contract_digest
|
|
||||||
if digest not in grouped:
|
|
||||||
grouped[digest] = (contract, [])
|
|
||||||
grouped[digest][1].append(record)
|
|
||||||
defects: list[DefectClaim] = []
|
|
||||||
satisfactions: list[BoundedSatisfaction] = []
|
|
||||||
silence: list[BoundedSilence] = []
|
|
||||||
try:
|
|
||||||
for digest in sorted(grouped):
|
|
||||||
contract, members = grouped[digest]
|
|
||||||
selected = tuple(members)
|
|
||||||
outputs = derive_verdict_outputs(selected, contract, _coverage_digest(request, contract, selected))
|
|
||||||
defects.extend(outputs.defects)
|
|
||||||
satisfactions.extend(outputs.satisfactions)
|
|
||||||
silence.extend(outputs.silence)
|
|
||||||
except (TypeError, ValueError) as exc:
|
|
||||||
raise ConsumerContractError(f"bounded verdict derivation failed: {exc}") from exc
|
|
||||||
return VerdictOutputs(tuple(defects), tuple(satisfactions), tuple(silence))
|
|
||||||
|
|
||||||
|
|
||||||
def _region_within(allowed: AnalyzedScope, candidate: SourceRegion) -> bool:
|
|
||||||
return any(
|
|
||||||
candidate.path == region.path and candidate.start_line >= region.start_line and candidate.end_line <= region.end_line
|
|
||||||
for region in allowed.regions
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _proof_target_index(claims: tuple[DefectClaim, ...]) -> dict[tuple[str, str, SourceLocation], int]:
|
|
||||||
keys: dict[tuple[str, str, SourceLocation], int] = {}
|
|
||||||
for index, claim in enumerate(claims):
|
|
||||||
key = (claim.record.packet.family_id, derive_packet_digest(claim.record.packet), claim.location)
|
|
||||||
if key in keys:
|
|
||||||
raise ConsumerContractError("proof target is ambiguous across defect claims")
|
|
||||||
keys[key] = index
|
|
||||||
return keys
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_attachment(
|
|
||||||
request: PushRequest,
|
|
||||||
result: list[DefectClaim],
|
|
||||||
keys: dict[tuple[str, str, SourceLocation], int],
|
|
||||||
consumed: set[int],
|
|
||||||
attachment: ProofAttachment,
|
|
||||||
trust: ProofTrustPolicy,
|
|
||||||
resolver: canonical.ContentResolver,
|
|
||||||
) -> None:
|
|
||||||
if not isinstance(attachment, ProofAttachment):
|
|
||||||
raise ConsumerContractError("every proof attachment must use the v2 proof contract")
|
|
||||||
key = (attachment.family_id, attachment.packet_digest, attachment.location)
|
|
||||||
if key not in keys:
|
|
||||||
raise ConsumerContractError("proof attachment does not match one exact defect claim")
|
|
||||||
index = keys[key]
|
|
||||||
if index in consumed:
|
|
||||||
raise ConsumerContractError("multiple proof attachments target one defect claim")
|
|
||||||
if not _region_within(request.scope, result[index].change_anchor):
|
|
||||||
raise ConsumerContractError("proof claim anchor is outside the push request scope")
|
|
||||||
result[index] = attach_differential_proof(result[index], attachment, trust, resolver)
|
|
||||||
consumed.add(index)
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_proofs(
|
|
||||||
request: PushRequest,
|
|
||||||
claims: tuple[DefectClaim, ...],
|
|
||||||
attachments: Sequence[ProofAttachment],
|
|
||||||
trust: ProofTrustPolicy | None,
|
|
||||||
resolver: canonical.ContentResolver,
|
|
||||||
) -> tuple[DefectClaim, ...]:
|
|
||||||
selected = tuple(attachments)
|
|
||||||
if not selected:
|
|
||||||
return claims
|
|
||||||
if not isinstance(trust, ProofTrustPolicy):
|
|
||||||
raise ConsumerContractError("nonempty proof attachments require a proof trust policy")
|
|
||||||
keys = _proof_target_index(claims)
|
|
||||||
result = list(claims)
|
|
||||||
consumed: set[int] = set()
|
|
||||||
try:
|
|
||||||
for attachment in selected:
|
|
||||||
_apply_attachment(request, result, keys, consumed, attachment, trust, resolver)
|
|
||||||
except ConsumerContractError:
|
|
||||||
raise
|
|
||||||
except (AttributeError, LookupError, TypeError, ValueError) as exc:
|
|
||||||
raise ConsumerContractError(f"proof validation failed: {exc}") from exc
|
|
||||||
return tuple(result)
|
|
||||||
|
|
||||||
|
|
||||||
def _total_reply_ms(elapsed_before_entry_ms: int, started_ns: int) -> int:
|
|
||||||
if isinstance(elapsed_before_entry_ms, bool) or not isinstance(elapsed_before_entry_ms, int) or elapsed_before_entry_ms < 0:
|
|
||||||
raise ConsumerContractError("elapsed-before-entry measurement must be a non-negative integer")
|
|
||||||
finished_ns = _monotonic_ns()
|
|
||||||
ticks = (started_ns, finished_ns)
|
|
||||||
if any(isinstance(tick, bool) or not isinstance(tick, int) or tick < 0 for tick in ticks):
|
|
||||||
raise ConsumerContractError("monotonic core timing ticks must be non-negative integers")
|
|
||||||
if finished_ns < started_ns:
|
|
||||||
raise ConsumerContractError("monotonic core timing measurement moved backwards")
|
|
||||||
core_elapsed_ms = (finished_ns - started_ns + 999_999) // 1_000_000
|
|
||||||
total_reply_ms = elapsed_before_entry_ms + core_elapsed_ms
|
|
||||||
if total_reply_ms <= 0:
|
|
||||||
raise ConsumerContractError("combined elapsed measurement must be a strictly positive integer")
|
|
||||||
return total_reply_ms
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_budget_artifacts(request: PushRequest, resolver: canonical.ContentResolver) -> None:
|
|
||||||
try:
|
|
||||||
check_budget(request.complexity, 1, request.budget, resolver)
|
|
||||||
except (BudgetGateFailure, AttributeError, LookupError, TypeError, ValueError) as exc:
|
|
||||||
raise ConsumerContractError(f"measured budget validation failed: {exc}") from exc
|
|
||||||
|
|
||||||
|
|
||||||
def _enforce_validated_deadline(request: PushRequest, total_reply_ms: int) -> None:
|
|
||||||
if total_reply_ms > request.budget.budget_ms:
|
|
||||||
raise ConsumerContractError(
|
|
||||||
f"measured budget gate failed: budget:{request.complexity.change_id}:"
|
|
||||||
f"{total_reply_ms}ms>{request.budget.budget_ms}ms "
|
|
||||||
f"class={request.budget.workload_class} derivation={request.budget.derivation_digest}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def consume_records(
|
|
||||||
request: PushRequest,
|
|
||||||
inputs: Sequence[EvidenceValidationInput],
|
|
||||||
validation_context: EvidenceValidationContext,
|
|
||||||
custody: PopulationCustodyPort,
|
|
||||||
elapsed_before_entry_ms: int,
|
|
||||||
resolver: canonical.ContentResolver,
|
|
||||||
attachments: Sequence[ProofAttachment] = (),
|
|
||||||
proof_trust: ProofTrustPolicy | None = None,
|
|
||||||
) -> PushVerdict:
|
|
||||||
started_ns = _monotonic_ns()
|
|
||||||
_validate_request(request)
|
|
||||||
records = _validated_records(inputs, validation_context, custody, resolver)
|
|
||||||
transition_count = sum(len(record.packet.transitions) for record in records)
|
|
||||||
if request.complexity.transition_count != transition_count:
|
|
||||||
raise ConsumerContractError("complexity transition count does not match validated records")
|
|
||||||
outputs = _derive_outputs(request, records, validation_context)
|
|
||||||
defects = _apply_proofs(request, outputs.defects, attachments, proof_trust, resolver)
|
|
||||||
if not defects and not outputs.satisfactions and not outputs.silence:
|
|
||||||
raise ConsumerContractError("push verdict cannot be an unbounded clean result")
|
|
||||||
_validate_budget_artifacts(request, resolver)
|
|
||||||
provisional = PushVerdict(
|
|
||||||
request.request_id,
|
|
||||||
defects,
|
|
||||||
outputs.satisfactions,
|
|
||||||
outputs.silence,
|
|
||||||
tuple(sorted(record.record_id for record in records)),
|
|
||||||
request.complexity,
|
|
||||||
0,
|
|
||||||
request.budget.derivation_digest,
|
|
||||||
)
|
|
||||||
total_reply_ms = _total_reply_ms(elapsed_before_entry_ms, started_ns)
|
|
||||||
_enforce_validated_deadline(request, total_reply_ms)
|
|
||||||
return replace(provisional, total_reply_ms=total_reply_ms)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"ConsumerContractError",
|
|
||||||
"PushRequest",
|
|
||||||
"PushVerdict",
|
|
||||||
"consume_records",
|
|
||||||
"derive_push_request_id",
|
|
||||||
"make_push_request",
|
|
||||||
]
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
"""Explicit temporary exports for the closed detection-core v1 namespace."""
|
|
||||||
|
|
||||||
from openultrasast.core.legacy_v1.contracts import (
|
|
||||||
REPORT_SCHEMA_KEYS,
|
|
||||||
AdmissionManifest,
|
|
||||||
AdmissionManifestError,
|
|
||||||
BoundedSilence,
|
|
||||||
Budget,
|
|
||||||
BudgetGateFailure,
|
|
||||||
Complexity,
|
|
||||||
Coverage,
|
|
||||||
FactPackError,
|
|
||||||
FittedComponentBlocked,
|
|
||||||
Guard,
|
|
||||||
Hop,
|
|
||||||
InstrumentFailure,
|
|
||||||
Judgment,
|
|
||||||
LegacyFactPack,
|
|
||||||
LegacyProbeResult,
|
|
||||||
ProvenanceMismatch,
|
|
||||||
ProvenClaim,
|
|
||||||
PushRequest,
|
|
||||||
PushVerdict,
|
|
||||||
ResolutionOutcome,
|
|
||||||
ShortcutDetected,
|
|
||||||
TraceRecord,
|
|
||||||
Witness,
|
|
||||||
admit_fact_pack,
|
|
||||||
admit_pack_file,
|
|
||||||
as_clean,
|
|
||||||
assert_no_shortcut,
|
|
||||||
check_budget,
|
|
||||||
check_reads,
|
|
||||||
consume_verdict,
|
|
||||||
derive_budget,
|
|
||||||
load_pack_file,
|
|
||||||
require_admission_evidence,
|
|
||||||
reversed_pair_probe,
|
|
||||||
stamp,
|
|
||||||
validate_admission_manifest,
|
|
||||||
validate_report_schema,
|
|
||||||
verify_fact_pack,
|
|
||||||
verify_stamp,
|
|
||||||
)
|
|
||||||
|
|
||||||
FactPack = LegacyFactPack
|
|
||||||
ProbeResult = LegacyProbeResult
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"REPORT_SCHEMA_KEYS",
|
|
||||||
"AdmissionManifest",
|
|
||||||
"AdmissionManifestError",
|
|
||||||
"BoundedSilence",
|
|
||||||
"Budget",
|
|
||||||
"BudgetGateFailure",
|
|
||||||
"Complexity",
|
|
||||||
"Coverage",
|
|
||||||
"FactPack",
|
|
||||||
"FactPackError",
|
|
||||||
"FittedComponentBlocked",
|
|
||||||
"Guard",
|
|
||||||
"Hop",
|
|
||||||
"Judgment",
|
|
||||||
"LegacyFactPack",
|
|
||||||
"LegacyProbeResult",
|
|
||||||
"InstrumentFailure",
|
|
||||||
"ProvenClaim",
|
|
||||||
"PushRequest",
|
|
||||||
"PushVerdict",
|
|
||||||
"ProvenanceMismatch",
|
|
||||||
"ProbeResult",
|
|
||||||
"ResolutionOutcome",
|
|
||||||
"ShortcutDetected",
|
|
||||||
"TraceRecord",
|
|
||||||
"Witness",
|
|
||||||
"admit_fact_pack",
|
|
||||||
"admit_pack_file",
|
|
||||||
"as_clean",
|
|
||||||
"assert_no_shortcut",
|
|
||||||
"check_budget",
|
|
||||||
"check_reads",
|
|
||||||
"consume_verdict",
|
|
||||||
"derive_budget",
|
|
||||||
"load_pack_file",
|
|
||||||
"require_admission_evidence",
|
|
||||||
"reversed_pair_probe",
|
|
||||||
"stamp",
|
|
||||||
"validate_admission_manifest",
|
|
||||||
"validate_report_schema",
|
|
||||||
"verify_fact_pack",
|
|
||||||
"verify_stamp",
|
|
||||||
]
|
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
"""Closed temporary owner of detection-core v1 contracts and behavior."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import enum
|
|
||||||
from collections.abc import Callable, Mapping, Sequence
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class AdmissionManifestError(ValueError):
|
|
||||||
"""A legacy admission manifest was refused."""
|
|
||||||
|
|
||||||
|
|
||||||
class FactPackError(ValueError):
|
|
||||||
"""A legacy fact pack is unreviewed or inconsistent."""
|
|
||||||
|
|
||||||
|
|
||||||
class InstrumentFailure(ValueError):
|
|
||||||
"""A legacy run did not read every tracked input."""
|
|
||||||
|
|
||||||
|
|
||||||
class BudgetGateFailure(ValueError):
|
|
||||||
"""A legacy change exceeded its measured budget."""
|
|
||||||
|
|
||||||
|
|
||||||
class ProvenanceMismatch(ValueError):
|
|
||||||
"""A legacy recorded digest differs from its re-derived identity."""
|
|
||||||
|
|
||||||
|
|
||||||
class ShortcutDetected(ValueError):
|
|
||||||
"""A legacy component separates reversed surface shapes."""
|
|
||||||
|
|
||||||
|
|
||||||
class FittedComponentBlocked(ValueError):
|
|
||||||
"""A legacy fitted component lacks release evidence."""
|
|
||||||
|
|
||||||
|
|
||||||
class ResolutionOutcome(enum.Enum):
|
|
||||||
COMPLETE = "complete"
|
|
||||||
INCOMPLETE = "incomplete"
|
|
||||||
UNRESOLVED = "unresolved"
|
|
||||||
|
|
||||||
|
|
||||||
class Judgment(enum.Enum):
|
|
||||||
UNGUARDED = "unguarded"
|
|
||||||
DISCHARGED = "discharged"
|
|
||||||
INDETERMINATE = "indeterminate"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ProvenClaim:
|
|
||||||
trace_id: str
|
|
||||||
witness: str
|
|
||||||
path: str
|
|
||||||
line: int
|
|
||||||
family: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class BoundedSilence:
|
|
||||||
path: str
|
|
||||||
reason: str
|
|
||||||
coverage: dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Hop:
|
|
||||||
function: str
|
|
||||||
call_line: int | None
|
|
||||||
complete: bool
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Witness:
|
|
||||||
kind: str
|
|
||||||
provenance: str
|
|
||||||
relation: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Guard:
|
|
||||||
kind: str
|
|
||||||
effect: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class TraceRecord:
|
|
||||||
entry: str
|
|
||||||
hops: tuple[Hop, ...]
|
|
||||||
operation_kind: str
|
|
||||||
resource: str
|
|
||||||
authority: str
|
|
||||||
witnesses: tuple[Witness, ...] = ()
|
|
||||||
guards: tuple[Guard, ...] = ()
|
|
||||||
completeness_reasons: tuple[str, ...] = ()
|
|
||||||
judgment: Judgment = Judgment.INDETERMINATE
|
|
||||||
digests: dict[str, str] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def record(self) -> dict[str, Any]:
|
|
||||||
from openultrasast.core.legacy_v1 import writer
|
|
||||||
|
|
||||||
return writer.trace_record(self)
|
|
||||||
|
|
||||||
def canonical(self) -> bytes:
|
|
||||||
from openultrasast.core.legacy_v1 import writer
|
|
||||||
|
|
||||||
return writer.trace_bytes(self)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PushRequest:
|
|
||||||
base: str
|
|
||||||
head: str
|
|
||||||
budget_class: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PushVerdict:
|
|
||||||
claims: tuple[ProvenClaim, ...]
|
|
||||||
silence: tuple[BoundedSilence, ...]
|
|
||||||
digests: dict[str, str]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class AdmissionManifest:
|
|
||||||
produced_by: str
|
|
||||||
reviewed: bool
|
|
||||||
label_isolated: bool
|
|
||||||
spent: bool
|
|
||||||
population_role: str
|
|
||||||
analyzer_revision: str
|
|
||||||
fact_set_digest: str
|
|
||||||
acquisition_record_digest: str
|
|
||||||
configuration_digest: str
|
|
||||||
population_digest: str
|
|
||||||
labels: tuple[str, ...]
|
|
||||||
families: tuple[str, ...] = ()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class LegacyFactPack:
|
|
||||||
family: str
|
|
||||||
facts: tuple[dict[str, Any], ...]
|
|
||||||
review_digest: str
|
|
||||||
fact_set_digest: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Coverage:
|
|
||||||
entry_points: int
|
|
||||||
frameworks_recognised: dict[str, int] = field(default_factory=dict)
|
|
||||||
calls: int = 0
|
|
||||||
complete: int = 0
|
|
||||||
incomplete: int = 0
|
|
||||||
unresolved: int = 0
|
|
||||||
unchecked_reasons: dict[str, int] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def validate(self) -> None:
|
|
||||||
buckets = (self.complete, self.incomplete, self.unresolved)
|
|
||||||
if min(buckets) < 0:
|
|
||||||
raise ValueError(f"negative resolution bucket in {buckets}")
|
|
||||||
if sum(buckets) != self.calls:
|
|
||||||
raise ValueError(
|
|
||||||
f"resolution buckets ({self.complete}+{self.incomplete}+{self.unresolved}) must equal the call count ({self.calls})"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Complexity:
|
|
||||||
regions: int
|
|
||||||
calls: int
|
|
||||||
changed_bytes: int
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Budget:
|
|
||||||
workload_class: str
|
|
||||||
seconds: float
|
|
||||||
n: int
|
|
||||||
percentile: float
|
|
||||||
hardware: str
|
|
||||||
|
|
||||||
def describe(self) -> str:
|
|
||||||
return f"class={self.workload_class} n={self.n} p{int(self.percentile * 100)} hardware={self.hardware} budget={self.seconds:g}s"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class LegacyProbeResult:
|
|
||||||
separated: bool
|
|
||||||
fix_score: float
|
|
||||||
reversed_score: float
|
|
||||||
|
|
||||||
|
|
||||||
REPORT_SCHEMA_KEYS = frozenset(
|
|
||||||
{
|
|
||||||
"manifest",
|
|
||||||
"families",
|
|
||||||
"pairs",
|
|
||||||
"pair_correctness",
|
|
||||||
"fix_recall",
|
|
||||||
"verified_location_recall",
|
|
||||||
"flagged_ordinary_per_100",
|
|
||||||
"precision",
|
|
||||||
"indeterminate_rate",
|
|
||||||
"coverage",
|
|
||||||
"runtime",
|
|
||||||
"instrument_failures",
|
|
||||||
"failed_gates",
|
|
||||||
"decision",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
_REQUIRED_MANIFEST_FIELDS = (
|
|
||||||
"produced_by",
|
|
||||||
"reviewed",
|
|
||||||
"label_isolated",
|
|
||||||
"spent",
|
|
||||||
"population_role",
|
|
||||||
"analyzer_revision",
|
|
||||||
"fact_set_digest",
|
|
||||||
"acquisition_record_digest",
|
|
||||||
"configuration_digest",
|
|
||||||
"population_digest",
|
|
||||||
"labels",
|
|
||||||
)
|
|
||||||
_BENCHMARK_PRODUCER = "valid-detection-benchmark"
|
|
||||||
_FORBIDDEN_LABEL_MARKERS = ("vulnerability", "fix-location", "ordinary-change")
|
|
||||||
|
|
||||||
|
|
||||||
def as_clean(judgment: Judgment) -> Judgment:
|
|
||||||
if judgment is Judgment.INDETERMINATE:
|
|
||||||
raise ValueError("indeterminate never converts to a clean judgment")
|
|
||||||
return judgment
|
|
||||||
|
|
||||||
|
|
||||||
def consume_verdict(
|
|
||||||
request: PushRequest,
|
|
||||||
record: TraceRecord,
|
|
||||||
claims: Sequence[ProvenClaim],
|
|
||||||
silence: Sequence[BoundedSilence],
|
|
||||||
) -> PushVerdict:
|
|
||||||
del request
|
|
||||||
for claim in claims:
|
|
||||||
if not claim.witness or not claim.path or claim.line <= 0:
|
|
||||||
raise ValueError(f"proven claim lacks witness or exact location: {claim}")
|
|
||||||
for entry in silence:
|
|
||||||
if not entry.reason:
|
|
||||||
raise ValueError(f"bounded silence lacks a coverage reason: {entry}")
|
|
||||||
return PushVerdict(tuple(claims), tuple(silence), dict(record.digests))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_admission_manifest(
|
|
||||||
payload: dict[str, object],
|
|
||||||
*,
|
|
||||||
expected_acquisition_digest: str | None = None,
|
|
||||||
) -> AdmissionManifest:
|
|
||||||
if not all(field in payload for field in _REQUIRED_MANIFEST_FIELDS):
|
|
||||||
raise AdmissionManifestError("malformed: missing required manifest fields")
|
|
||||||
if payload["produced_by"] != _BENCHMARK_PRODUCER:
|
|
||||||
raise AdmissionManifestError(f"producer: only {_BENCHMARK_PRODUCER} manifests are consumed")
|
|
||||||
if payload["reviewed"] is not True:
|
|
||||||
raise AdmissionManifestError("unreviewed: manifest lacks external review")
|
|
||||||
if payload["label_isolated"] is not True:
|
|
||||||
raise AdmissionManifestError("label-isolated: isolation attestation missing")
|
|
||||||
raw_labels = payload["labels"]
|
|
||||||
if not isinstance(raw_labels, (tuple, list)):
|
|
||||||
raise AdmissionManifestError("label-exposure: manifest carries admission labels")
|
|
||||||
if any(marker in str(label) for label in raw_labels for marker in _FORBIDDEN_LABEL_MARKERS):
|
|
||||||
raise AdmissionManifestError("label-exposure: manifest carries admission labels")
|
|
||||||
if expected_acquisition_digest is not None and payload["acquisition_record_digest"] != expected_acquisition_digest:
|
|
||||||
raise AdmissionManifestError("acquisition-output-changed: digest mismatch")
|
|
||||||
if payload["spent"] is True:
|
|
||||||
raise AdmissionManifestError("spent: population already consumed")
|
|
||||||
raw_families = payload.get("families", ())
|
|
||||||
if not isinstance(raw_families, (tuple, list)):
|
|
||||||
raise AdmissionManifestError("malformed: families must be a sequence")
|
|
||||||
return _manifest(payload, raw_labels, raw_families)
|
|
||||||
|
|
||||||
|
|
||||||
def _manifest(payload: dict[str, object], labels: Sequence[object], families: Sequence[object]) -> AdmissionManifest:
|
|
||||||
return AdmissionManifest(
|
|
||||||
produced_by=str(payload["produced_by"]),
|
|
||||||
reviewed=True,
|
|
||||||
label_isolated=True,
|
|
||||||
spent=False,
|
|
||||||
population_role=str(payload["population_role"]),
|
|
||||||
analyzer_revision=str(payload["analyzer_revision"]),
|
|
||||||
fact_set_digest=str(payload["fact_set_digest"]),
|
|
||||||
acquisition_record_digest=str(payload["acquisition_record_digest"]),
|
|
||||||
configuration_digest=str(payload["configuration_digest"]),
|
|
||||||
population_digest=str(payload["population_digest"]),
|
|
||||||
labels=tuple(str(label) for label in labels),
|
|
||||||
families=tuple(str(family) for family in families),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_report_schema(report: dict[str, Any]) -> None:
|
|
||||||
missing = sorted(REPORT_SCHEMA_KEYS - set(report))
|
|
||||||
if missing:
|
|
||||||
raise AdmissionManifestError(f"report schema: missing keys {missing}")
|
|
||||||
for rate_key in (
|
|
||||||
"pair_correctness",
|
|
||||||
"fix_recall",
|
|
||||||
"verified_location_recall",
|
|
||||||
"flagged_ordinary_per_100",
|
|
||||||
"precision",
|
|
||||||
"indeterminate_rate",
|
|
||||||
):
|
|
||||||
if not {"n", "count", "rate", "wilson95"} <= set(report[rate_key]):
|
|
||||||
raise AdmissionManifestError(f"report schema: {rate_key} lacks n/count/rate/wilson95")
|
|
||||||
|
|
||||||
|
|
||||||
def admit_fact_pack(family: str, facts: Sequence[Mapping[str, Any]], *, review_digest: str) -> LegacyFactPack:
|
|
||||||
from openultrasast.core.legacy_v1 import writer
|
|
||||||
|
|
||||||
if not review_digest:
|
|
||||||
raise FactPackError("unreviewed fact pack refused: no review digest")
|
|
||||||
normalized = tuple(dict(fact) for fact in facts)
|
|
||||||
return LegacyFactPack(family, normalized, review_digest, writer.fact_pack_digest(family, normalized))
|
|
||||||
|
|
||||||
|
|
||||||
def verify_fact_pack(pack: LegacyFactPack, facts: Sequence[Mapping[str, Any]]) -> None:
|
|
||||||
from openultrasast.core.legacy_v1 import writer
|
|
||||||
|
|
||||||
normalized = tuple(dict(fact) for fact in facts)
|
|
||||||
if writer.fact_pack_digest(pack.family, normalized) != pack.fact_set_digest:
|
|
||||||
raise FactPackError(f"fact content does not match the stamped digest of pack {pack.family!r}")
|
|
||||||
|
|
||||||
|
|
||||||
def load_pack_file(path: str | Path) -> LegacyFactPack:
|
|
||||||
from openultrasast.core.legacy_v1 import writer
|
|
||||||
|
|
||||||
payload = writer.read_json(Path(path))
|
|
||||||
review = payload.get("review", {})
|
|
||||||
if not isinstance(review, dict):
|
|
||||||
raise FactPackError(f"pack {path} lacks a verifiable review attestation")
|
|
||||||
note = review.get("note", "")
|
|
||||||
if not isinstance(note, str) or not note or review.get("digest") != writer.text_digest(note):
|
|
||||||
raise FactPackError(f"pack {path} lacks a verifiable review attestation")
|
|
||||||
facts = payload.get("facts")
|
|
||||||
if not isinstance(facts, list) or not all(isinstance(fact, dict) for fact in facts):
|
|
||||||
raise FactPackError(f"pack {path}: facts must be a list of objects")
|
|
||||||
return admit_fact_pack(str(payload.get("family", "")), facts, review_digest=str(review["digest"]))
|
|
||||||
|
|
||||||
|
|
||||||
def admit_pack_file(path: str | Path, *, review_digest: str) -> LegacyFactPack:
|
|
||||||
pack = load_pack_file(path)
|
|
||||||
if review_digest != pack.review_digest:
|
|
||||||
raise FactPackError(f"pack {path}: supplied review digest does not match the embedded attestation")
|
|
||||||
return pack
|
|
||||||
|
|
||||||
|
|
||||||
_STAMP_FIELDS = (
|
|
||||||
"analyzer_revision",
|
|
||||||
"fact_set_digest",
|
|
||||||
"acquisition_record_digest",
|
|
||||||
"configuration_digest",
|
|
||||||
"population_digest",
|
|
||||||
"input_digest",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_reads(reads: Mapping[str, int]) -> None:
|
|
||||||
zero = sorted(path for path, size in reads.items() if size <= 0)
|
|
||||||
if zero:
|
|
||||||
raise InstrumentFailure(f"zero-byte read of tracked input: {', '.join(zero)}")
|
|
||||||
|
|
||||||
|
|
||||||
def _quantile(values: list[float], percentile: float) -> float:
|
|
||||||
position = (len(values) - 1) * percentile
|
|
||||||
lower = int(position)
|
|
||||||
upper = min(lower + 1, len(values) - 1)
|
|
||||||
weight = position - lower
|
|
||||||
return values[lower] * (1 - weight) + values[upper] * weight
|
|
||||||
|
|
||||||
|
|
||||||
def derive_budget(samples: Sequence[tuple[Complexity, float]], *, workload_class: str, percentile: float, hardware: str) -> Budget:
|
|
||||||
if not samples:
|
|
||||||
raise ValueError("budget derivation needs at least one measured sample")
|
|
||||||
if not 0.0 < percentile < 1.0:
|
|
||||||
raise ValueError(f"percentile {percentile} outside (0, 1)")
|
|
||||||
seconds = sorted(float(item[1]) for item in samples)
|
|
||||||
return Budget(workload_class, _quantile(seconds, percentile), len(samples), percentile, hardware)
|
|
||||||
|
|
||||||
|
|
||||||
def check_budget(change_id: str, seconds: float, budget: Budget) -> None:
|
|
||||||
if seconds > budget.seconds:
|
|
||||||
raise BudgetGateFailure(f"budget:{change_id}:{seconds:g}s>{budget.seconds:g}s ({budget.describe()})")
|
|
||||||
|
|
||||||
|
|
||||||
def stamp(
|
|
||||||
*,
|
|
||||||
analyzer_revision: str,
|
|
||||||
fact_set_digest: str,
|
|
||||||
acquisition_record_digest: str,
|
|
||||||
configuration_digest: str,
|
|
||||||
population_digest: str,
|
|
||||||
change_id: str,
|
|
||||||
) -> dict[str, str]:
|
|
||||||
from openultrasast.core.legacy_v1 import writer
|
|
||||||
|
|
||||||
return {
|
|
||||||
"analyzer_revision": analyzer_revision,
|
|
||||||
"fact_set_digest": fact_set_digest,
|
|
||||||
"acquisition_record_digest": acquisition_record_digest,
|
|
||||||
"configuration_digest": configuration_digest,
|
|
||||||
"population_digest": population_digest,
|
|
||||||
"input_digest": writer.input_digest(change_id, population_digest),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def verify_stamp(recorded: Mapping[str, object], rederived: Mapping[str, object]) -> None:
|
|
||||||
for field_name in _STAMP_FIELDS:
|
|
||||||
if recorded.get(field_name) != rederived.get(field_name):
|
|
||||||
raise ProvenanceMismatch(f"{field_name}: recorded {recorded.get(field_name)!r} != rederived {rederived.get(field_name)!r}")
|
|
||||||
|
|
||||||
|
|
||||||
def reversed_pair_probe(
|
|
||||||
score_fn: Callable[[Mapping[str, Any]], float], fix_change: Mapping[str, Any], reversed_change: Mapping[str, Any]
|
|
||||||
) -> LegacyProbeResult:
|
|
||||||
fix_score = float(score_fn(fix_change))
|
|
||||||
reversed_score = float(score_fn(reversed_change))
|
|
||||||
return LegacyProbeResult(fix_score != reversed_score, fix_score, reversed_score)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_no_shortcut(
|
|
||||||
score_fn: Callable[[Mapping[str, Any]], float], fix_change: Mapping[str, Any], reversed_change: Mapping[str, Any]
|
|
||||||
) -> None:
|
|
||||||
result = reversed_pair_probe(score_fn, fix_change, reversed_change)
|
|
||||||
if result.separated:
|
|
||||||
raise ShortcutDetected(f"shape shortcut: fix={result.fix_score} vs reversed={result.reversed_score}")
|
|
||||||
|
|
||||||
|
|
||||||
def require_admission_evidence(*, holdout_passed: bool, admission_decision: str) -> None:
|
|
||||||
if not holdout_passed:
|
|
||||||
raise FittedComponentBlocked("fitted component lacks per-repository holdout evidence")
|
|
||||||
if admission_decision != "GO":
|
|
||||||
raise FittedComponentBlocked(f"fitted component admission decision is {admission_decision!r}, not GO")
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"""The sole temporary detection-core v1 JSON serialization capability."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
from collections.abc import Mapping, Sequence
|
|
||||||
from dataclasses import asdict
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
from openultrasast.core.legacy_v1 import contracts
|
|
||||||
|
|
||||||
|
|
||||||
def trace_record(value: contracts.TraceRecord) -> dict[str, Any]:
|
|
||||||
payload: dict[str, Any] = asdict(value)
|
|
||||||
payload["judgment"] = value.judgment.value
|
|
||||||
return cast(dict[str, Any], json.loads(json.dumps(payload, sort_keys=True)))
|
|
||||||
|
|
||||||
|
|
||||||
def trace_bytes(value: contracts.TraceRecord) -> bytes:
|
|
||||||
return json.dumps(trace_record(value), sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
|
|
||||||
|
|
||||||
|
|
||||||
def fact_pack_digest(family: str, facts: Sequence[Mapping[str, Any]]) -> str:
|
|
||||||
encoded = json.dumps([family, tuple(dict(fact) for fact in facts)], sort_keys=True, separators=(",", ":")).encode()
|
|
||||||
return hashlib.sha256(encoded).hexdigest()[:12]
|
|
||||||
|
|
||||||
|
|
||||||
def text_digest(value: str) -> str:
|
|
||||||
return hashlib.sha256(value.encode()).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def input_digest(change_id: str, population_digest: str) -> str:
|
|
||||||
payload = json.dumps([population_digest, change_id], sort_keys=True, separators=(",", ":")).encode()
|
|
||||||
return hashlib.sha256(payload).hexdigest()[:12]
|
|
||||||
|
|
||||||
|
|
||||||
def read_json(path: Path) -> dict[str, Any]:
|
|
||||||
value = json.loads(path.read_text(encoding="utf-8"))
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
raise contracts.FactPackError(f"pack {path}: root must be an object")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["fact_pack_digest", "input_digest", "read_json", "text_digest", "trace_bytes", "trace_record"]
|
|
||||||
@@ -8,7 +8,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from openultrasast.core import canonical
|
from openultrasast.core import canonical
|
||||||
from openultrasast.core.semantics import SourceLocation
|
from openultrasast.core.semantics import SourceLocation
|
||||||
from openultrasast.core.verdict_v2 import DefectClaim
|
from openultrasast.core.verdict import DefectClaim
|
||||||
|
|
||||||
DIFFERENTIAL_PROOF_SCHEMA = "openultrasast.detection-core.differential-proof.v2"
|
DIFFERENTIAL_PROOF_SCHEMA = "openultrasast.detection-core.differential-proof.v2"
|
||||||
_DIGEST = re.compile(r"[0-9a-f]{64}")
|
_DIGEST = re.compile(r"[0-9a-f]{64}")
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
"""Temporary explicit import shim for the pre-v2 trace contract."""
|
|
||||||
|
|
||||||
from openultrasast.core.legacy_v1.contracts import Guard, Hop, TraceRecord, Witness
|
|
||||||
|
|
||||||
__all__ = ["Guard", "Hop", "TraceRecord", "Witness"]
|
|
||||||
@@ -1,5 +1,271 @@
|
|||||||
"""Temporary explicit import shim for the pre-v2 verdict contract."""
|
"""Bounded v2 verdict outputs derived from complete evidence records."""
|
||||||
|
|
||||||
from openultrasast.core.legacy_v1.contracts import BoundedSilence, Judgment, ProvenClaim, as_clean
|
from __future__ import annotations
|
||||||
|
|
||||||
__all__ = ["BoundedSilence", "Judgment", "ProvenClaim", "as_clean"]
|
import re
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from openultrasast.core.evidence import (
|
||||||
|
AnalyzedScope,
|
||||||
|
EvidenceBasis,
|
||||||
|
EvidenceRecord,
|
||||||
|
EvidenceValidationError,
|
||||||
|
NeutralOutcome,
|
||||||
|
ScopeValidationError,
|
||||||
|
SourceRegion,
|
||||||
|
derive_record_id,
|
||||||
|
validate_scope,
|
||||||
|
)
|
||||||
|
from openultrasast.core.family import FamilyContract, FamilyContractError, ResidualQuestion, validate_contract, validate_record
|
||||||
|
from openultrasast.core.semantics import Observation, RelationStatus, SemanticRelation, SourceLocation
|
||||||
|
|
||||||
|
_DIGEST = re.compile(r"[0-9a-f]{64}")
|
||||||
|
|
||||||
|
|
||||||
|
class VerdictContractError(ValueError):
|
||||||
|
"""Validated evidence cannot be represented as a bounded verdict."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DefectClaim:
|
||||||
|
record: EvidenceRecord
|
||||||
|
basis: EvidenceBasis
|
||||||
|
location: SourceLocation
|
||||||
|
change_anchor: SourceRegion
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not isinstance(self.record, EvidenceRecord) or self.record.decision.outcome is not NeutralOutcome.VIOLATION:
|
||||||
|
raise VerdictContractError("defect claim requires a violation evidence record")
|
||||||
|
try:
|
||||||
|
self.record.__post_init__()
|
||||||
|
validate_scope(self.record.packet, self.record.packet.scope)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise VerdictContractError(f"defect claim evidence record is invalid: {exc}") from exc
|
||||||
|
if not isinstance(self.basis, EvidenceBasis) or self.basis is not self.record.decision.basis:
|
||||||
|
raise VerdictContractError("defect claim basis must equal its evidence decision basis")
|
||||||
|
if not isinstance(self.location, SourceLocation) or not isinstance(self.change_anchor, SourceRegion):
|
||||||
|
raise VerdictContractError("defect claim requires an exact location and change anchor")
|
||||||
|
members: tuple[SemanticRelation | Observation, ...] = (
|
||||||
|
*self.record.packet.transitions,
|
||||||
|
*self.record.packet.observations,
|
||||||
|
)
|
||||||
|
if self.record.packet.completeness_reasons or any(item.status is not RelationStatus.COMPLETE for item in members):
|
||||||
|
raise VerdictContractError("defect claim requires complete evidence")
|
||||||
|
if self.location not in tuple(item.location for item in members):
|
||||||
|
raise VerdictContractError("defect location must name an exact evidence location")
|
||||||
|
if self.change_anchor not in self.record.decision.bounded_scope.regions:
|
||||||
|
raise VerdictContractError("defect change anchor must belong to the bounded decision scope")
|
||||||
|
if not _region_contains(self.change_anchor, self.location):
|
||||||
|
raise VerdictContractError("defect location is outside its change anchor")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BoundedSatisfaction:
|
||||||
|
question_ids: tuple[str, ...]
|
||||||
|
records: tuple[EvidenceRecord, ...]
|
||||||
|
scope: AnalyzedScope
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_text_tuple(self.question_ids, "satisfaction question")
|
||||||
|
if (
|
||||||
|
not isinstance(self.records, tuple)
|
||||||
|
or not self.records
|
||||||
|
or not all(isinstance(record, EvidenceRecord) for record in self.records)
|
||||||
|
):
|
||||||
|
raise VerdictContractError("bounded satisfaction requires evidence records")
|
||||||
|
if not isinstance(self.scope, AnalyzedScope):
|
||||||
|
raise VerdictContractError("bounded satisfaction requires an AnalyzedScope")
|
||||||
|
if any(record.decision.outcome is not NeutralOutcome.SATISFIED for record in self.records):
|
||||||
|
raise VerdictContractError("bounded satisfaction accepts only satisfied evidence records")
|
||||||
|
if any(record.decision.bounded_scope != self.scope for record in self.records):
|
||||||
|
raise VerdictContractError("satisfaction scope must equal every supporting record scope")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BoundedSilence:
|
||||||
|
question_ids: tuple[str, ...]
|
||||||
|
reasons: tuple[str, ...]
|
||||||
|
evidence_record_digests: tuple[str, ...]
|
||||||
|
coverage_digest: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
_text_tuple(self.question_ids, "silence question")
|
||||||
|
_text_tuple(self.reasons, "silence reason")
|
||||||
|
_digest_tuple(self.evidence_record_digests, "silence evidence record")
|
||||||
|
_digest(self.coverage_digest, "coverage")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class VerdictOutputs:
|
||||||
|
defects: tuple[DefectClaim, ...]
|
||||||
|
satisfactions: tuple[BoundedSatisfaction, ...]
|
||||||
|
silence: tuple[BoundedSilence, ...]
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
values = (
|
||||||
|
(self.defects, DefectClaim, "defects"),
|
||||||
|
(self.satisfactions, BoundedSatisfaction, "satisfactions"),
|
||||||
|
(self.silence, BoundedSilence, "silence"),
|
||||||
|
)
|
||||||
|
for items, item_type, field in values:
|
||||||
|
if not isinstance(items, tuple) or not all(isinstance(item, item_type) for item in items):
|
||||||
|
raise VerdictContractError(f"{field} must be a tuple of {item_type.__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
def _digest(value: object, field: str) -> None:
|
||||||
|
if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
|
||||||
|
raise VerdictContractError(f"{field} must be a full lowercase SHA-256 digest")
|
||||||
|
|
||||||
|
|
||||||
|
def _text_tuple(value: object, field: str) -> None:
|
||||||
|
if not isinstance(value, tuple) or not value or not all(isinstance(item, str) and item for item in value):
|
||||||
|
raise VerdictContractError(f"{field} identities must be a nonempty tuple")
|
||||||
|
if len(set(value)) != len(value):
|
||||||
|
raise VerdictContractError(f"duplicate {field} identity")
|
||||||
|
|
||||||
|
|
||||||
|
def _digest_tuple(value: object, field: str) -> None:
|
||||||
|
_text_tuple(value, field)
|
||||||
|
assert isinstance(value, tuple)
|
||||||
|
for item in value:
|
||||||
|
_digest(item, field)
|
||||||
|
|
||||||
|
|
||||||
|
def _region_contains(region: SourceRegion, location: SourceLocation) -> bool:
|
||||||
|
return location.path == region.path and region.start_line <= location.line <= region.end_line
|
||||||
|
|
||||||
|
|
||||||
|
def _scope_contains(scope: AnalyzedScope, location: SourceLocation) -> bool:
|
||||||
|
return any(_region_contains(region, location) for region in scope.regions)
|
||||||
|
|
||||||
|
|
||||||
|
def _members(record: EvidenceRecord, role: str) -> tuple[SemanticRelation | Observation, ...]:
|
||||||
|
transitions = tuple(item for item in record.packet.transitions if item.role == role)
|
||||||
|
observations = tuple(item for item in record.packet.observations if item.role == role)
|
||||||
|
return (*transitions, *observations)
|
||||||
|
|
||||||
|
|
||||||
|
def _question_state(record: EvidenceRecord, question: ResidualQuestion) -> tuple[bool, tuple[str, ...]]:
|
||||||
|
reasons: set[str] = set()
|
||||||
|
if record.decision.family_code not in question.allowed_decisions:
|
||||||
|
reasons.add(f"decision_not_declared_for_question:{record.decision.family_code}")
|
||||||
|
for role in question.required_roles:
|
||||||
|
members = _members(record, role)
|
||||||
|
if not members:
|
||||||
|
reasons.add(f"missing_required_role:{role}")
|
||||||
|
elif not any(
|
||||||
|
item.status is RelationStatus.COMPLETE and _scope_contains(record.decision.bounded_scope, item.location) for item in members
|
||||||
|
):
|
||||||
|
relation_reasons = {reason for item in members if isinstance(item, SemanticRelation) for reason in item.reasons}
|
||||||
|
complete_outside = any(item.status is RelationStatus.COMPLETE for item in members)
|
||||||
|
fallback = f"evidence_outside_bounded_scope:{role}" if complete_outside else f"incomplete_required_role:{role}"
|
||||||
|
reasons.update(relation_reasons or record.packet.completeness_reasons or (fallback,))
|
||||||
|
if record.decision.outcome is NeutralOutcome.INDETERMINATE:
|
||||||
|
reasons.update(record.decision.reasons)
|
||||||
|
complete = not reasons and record.decision.outcome is not NeutralOutcome.INDETERMINATE
|
||||||
|
return complete, tuple(sorted(reasons))
|
||||||
|
|
||||||
|
|
||||||
|
def _location(record: EvidenceRecord, question: ResidualQuestion) -> tuple[SourceLocation, SourceRegion]:
|
||||||
|
candidates = tuple(
|
||||||
|
item.location for role in question.required_roles for item in _members(record, role) if item.status is RelationStatus.COMPLETE
|
||||||
|
)
|
||||||
|
ordered = sorted(candidates, key=lambda item: (item.path, item.line, -1 if item.column is None else item.column, item.symbol or ""))
|
||||||
|
for location in ordered:
|
||||||
|
anchors = tuple(region for region in record.decision.bounded_scope.regions if _region_contains(region, location))
|
||||||
|
if anchors:
|
||||||
|
anchor = min(anchors, key=lambda item: (item.end_line - item.start_line, item.path, item.start_line))
|
||||||
|
return location, anchor
|
||||||
|
raise VerdictContractError("complete defect evidence has no exact location inside its bounded change scope")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_inputs(records: Sequence[EvidenceRecord], contract: FamilyContract, coverage_digest: str) -> tuple[EvidenceRecord, ...]:
|
||||||
|
_digest(coverage_digest, "coverage")
|
||||||
|
try:
|
||||||
|
validate_contract(contract)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise VerdictContractError(f"family contract is invalid: {exc}") from exc
|
||||||
|
values = tuple(records)
|
||||||
|
if not values or not all(isinstance(record, EvidenceRecord) for record in values):
|
||||||
|
raise VerdictContractError("records must be a nonempty sequence of EvidenceRecord values")
|
||||||
|
identities: set[str] = set()
|
||||||
|
try:
|
||||||
|
for record in values:
|
||||||
|
record.__post_init__()
|
||||||
|
if record.record_id != derive_record_id(record.packet, record.decision):
|
||||||
|
raise EvidenceValidationError("record identity cannot be re-derived")
|
||||||
|
if record.record_id in identities:
|
||||||
|
raise EvidenceValidationError("duplicate evidence record identity")
|
||||||
|
identities.add(record.record_id)
|
||||||
|
validate_scope(record.packet, record.packet.scope)
|
||||||
|
validate_record(contract, record)
|
||||||
|
except (TypeError, ValueError, EvidenceValidationError, ScopeValidationError, FamilyContractError) as exc:
|
||||||
|
raise VerdictContractError(f"evidence record validation failed: {exc}") from exc
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _silence(
|
||||||
|
question: ResidualQuestion,
|
||||||
|
reasons: set[str],
|
||||||
|
record_ids: set[str],
|
||||||
|
coverage_digest: str,
|
||||||
|
) -> BoundedSilence | None:
|
||||||
|
if not reasons:
|
||||||
|
return None
|
||||||
|
return BoundedSilence(
|
||||||
|
(question.question_id,),
|
||||||
|
tuple(sorted(reasons)),
|
||||||
|
tuple(sorted(record_ids)),
|
||||||
|
coverage_digest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def derive_verdict_outputs(
|
||||||
|
records: Sequence[EvidenceRecord],
|
||||||
|
contract: FamilyContract,
|
||||||
|
coverage_digest: str,
|
||||||
|
) -> VerdictOutputs:
|
||||||
|
"""Derive bounded outputs without converting incomplete evidence to safety."""
|
||||||
|
validated = _validate_inputs(records, contract, coverage_digest)
|
||||||
|
defects: dict[str, DefectClaim] = {}
|
||||||
|
satisfactions: dict[str, set[str]] = {}
|
||||||
|
silence: list[BoundedSilence] = []
|
||||||
|
for question in contract.residual_questions:
|
||||||
|
reasons: set[str] = set()
|
||||||
|
undecided: set[str] = set()
|
||||||
|
for record in validated:
|
||||||
|
complete, record_reasons = _question_state(record, question)
|
||||||
|
if not complete:
|
||||||
|
reasons.update(record_reasons)
|
||||||
|
undecided.add(record.record_id)
|
||||||
|
continue
|
||||||
|
if record.decision.outcome is NeutralOutcome.VIOLATION:
|
||||||
|
if record.record_id not in defects:
|
||||||
|
location, anchor = _location(record, question)
|
||||||
|
defects[record.record_id] = DefectClaim(record, record.decision.basis, location, anchor)
|
||||||
|
elif record.decision.outcome is NeutralOutcome.SATISFIED:
|
||||||
|
satisfactions.setdefault(record.record_id, set()).add(question.question_id)
|
||||||
|
bounded = _silence(question, reasons, undecided, coverage_digest)
|
||||||
|
if bounded is not None:
|
||||||
|
silence.append(bounded)
|
||||||
|
records_by_id = {record.record_id: record for record in validated}
|
||||||
|
satisfied = tuple(
|
||||||
|
BoundedSatisfaction(tuple(sorted(question_ids)), (records_by_id[record_id],), records_by_id[record_id].decision.bounded_scope)
|
||||||
|
for record_id, question_ids in sorted(satisfactions.items())
|
||||||
|
)
|
||||||
|
return VerdictOutputs(
|
||||||
|
tuple(defects[identity] for identity in sorted(defects)),
|
||||||
|
satisfied,
|
||||||
|
tuple(sorted(silence, key=lambda item: item.question_ids)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BoundedSatisfaction",
|
||||||
|
"BoundedSilence",
|
||||||
|
"DefectClaim",
|
||||||
|
"VerdictContractError",
|
||||||
|
"VerdictOutputs",
|
||||||
|
"derive_verdict_outputs",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
"""Bounded v2 verdict outputs derived from complete evidence records."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from collections.abc import Sequence
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from openultrasast.core.evidence import (
|
|
||||||
AnalyzedScope,
|
|
||||||
EvidenceBasis,
|
|
||||||
EvidenceRecord,
|
|
||||||
EvidenceValidationError,
|
|
||||||
NeutralOutcome,
|
|
||||||
ScopeValidationError,
|
|
||||||
SourceRegion,
|
|
||||||
derive_record_id,
|
|
||||||
validate_scope,
|
|
||||||
)
|
|
||||||
from openultrasast.core.family import FamilyContract, FamilyContractError, ResidualQuestion, validate_contract, validate_record
|
|
||||||
from openultrasast.core.semantics import Observation, RelationStatus, SemanticRelation, SourceLocation
|
|
||||||
|
|
||||||
_DIGEST = re.compile(r"[0-9a-f]{64}")
|
|
||||||
|
|
||||||
|
|
||||||
class VerdictContractError(ValueError):
|
|
||||||
"""Validated evidence cannot be represented as a bounded verdict."""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DefectClaim:
|
|
||||||
record: EvidenceRecord
|
|
||||||
basis: EvidenceBasis
|
|
||||||
location: SourceLocation
|
|
||||||
change_anchor: SourceRegion
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
if not isinstance(self.record, EvidenceRecord) or self.record.decision.outcome is not NeutralOutcome.VIOLATION:
|
|
||||||
raise VerdictContractError("defect claim requires a violation evidence record")
|
|
||||||
try:
|
|
||||||
self.record.__post_init__()
|
|
||||||
validate_scope(self.record.packet, self.record.packet.scope)
|
|
||||||
except (TypeError, ValueError) as exc:
|
|
||||||
raise VerdictContractError(f"defect claim evidence record is invalid: {exc}") from exc
|
|
||||||
if not isinstance(self.basis, EvidenceBasis) or self.basis is not self.record.decision.basis:
|
|
||||||
raise VerdictContractError("defect claim basis must equal its evidence decision basis")
|
|
||||||
if not isinstance(self.location, SourceLocation) or not isinstance(self.change_anchor, SourceRegion):
|
|
||||||
raise VerdictContractError("defect claim requires an exact location and change anchor")
|
|
||||||
members: tuple[SemanticRelation | Observation, ...] = (
|
|
||||||
*self.record.packet.transitions,
|
|
||||||
*self.record.packet.observations,
|
|
||||||
)
|
|
||||||
if self.record.packet.completeness_reasons or any(item.status is not RelationStatus.COMPLETE for item in members):
|
|
||||||
raise VerdictContractError("defect claim requires complete evidence")
|
|
||||||
if self.location not in tuple(item.location for item in members):
|
|
||||||
raise VerdictContractError("defect location must name an exact evidence location")
|
|
||||||
if self.change_anchor not in self.record.decision.bounded_scope.regions:
|
|
||||||
raise VerdictContractError("defect change anchor must belong to the bounded decision scope")
|
|
||||||
if not _region_contains(self.change_anchor, self.location):
|
|
||||||
raise VerdictContractError("defect location is outside its change anchor")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class BoundedSatisfaction:
|
|
||||||
question_ids: tuple[str, ...]
|
|
||||||
records: tuple[EvidenceRecord, ...]
|
|
||||||
scope: AnalyzedScope
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
_text_tuple(self.question_ids, "satisfaction question")
|
|
||||||
if (
|
|
||||||
not isinstance(self.records, tuple)
|
|
||||||
or not self.records
|
|
||||||
or not all(isinstance(record, EvidenceRecord) for record in self.records)
|
|
||||||
):
|
|
||||||
raise VerdictContractError("bounded satisfaction requires evidence records")
|
|
||||||
if not isinstance(self.scope, AnalyzedScope):
|
|
||||||
raise VerdictContractError("bounded satisfaction requires an AnalyzedScope")
|
|
||||||
if any(record.decision.outcome is not NeutralOutcome.SATISFIED for record in self.records):
|
|
||||||
raise VerdictContractError("bounded satisfaction accepts only satisfied evidence records")
|
|
||||||
if any(record.decision.bounded_scope != self.scope for record in self.records):
|
|
||||||
raise VerdictContractError("satisfaction scope must equal every supporting record scope")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class BoundedSilence:
|
|
||||||
question_ids: tuple[str, ...]
|
|
||||||
reasons: tuple[str, ...]
|
|
||||||
evidence_record_digests: tuple[str, ...]
|
|
||||||
coverage_digest: str
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
_text_tuple(self.question_ids, "silence question")
|
|
||||||
_text_tuple(self.reasons, "silence reason")
|
|
||||||
_digest_tuple(self.evidence_record_digests, "silence evidence record")
|
|
||||||
_digest(self.coverage_digest, "coverage")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class VerdictOutputs:
|
|
||||||
defects: tuple[DefectClaim, ...]
|
|
||||||
satisfactions: tuple[BoundedSatisfaction, ...]
|
|
||||||
silence: tuple[BoundedSilence, ...]
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
values = (
|
|
||||||
(self.defects, DefectClaim, "defects"),
|
|
||||||
(self.satisfactions, BoundedSatisfaction, "satisfactions"),
|
|
||||||
(self.silence, BoundedSilence, "silence"),
|
|
||||||
)
|
|
||||||
for items, item_type, field in values:
|
|
||||||
if not isinstance(items, tuple) or not all(isinstance(item, item_type) for item in items):
|
|
||||||
raise VerdictContractError(f"{field} must be a tuple of {item_type.__name__}")
|
|
||||||
|
|
||||||
|
|
||||||
def _digest(value: object, field: str) -> None:
|
|
||||||
if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
|
|
||||||
raise VerdictContractError(f"{field} must be a full lowercase SHA-256 digest")
|
|
||||||
|
|
||||||
|
|
||||||
def _text_tuple(value: object, field: str) -> None:
|
|
||||||
if not isinstance(value, tuple) or not value or not all(isinstance(item, str) and item for item in value):
|
|
||||||
raise VerdictContractError(f"{field} identities must be a nonempty tuple")
|
|
||||||
if len(set(value)) != len(value):
|
|
||||||
raise VerdictContractError(f"duplicate {field} identity")
|
|
||||||
|
|
||||||
|
|
||||||
def _digest_tuple(value: object, field: str) -> None:
|
|
||||||
_text_tuple(value, field)
|
|
||||||
assert isinstance(value, tuple)
|
|
||||||
for item in value:
|
|
||||||
_digest(item, field)
|
|
||||||
|
|
||||||
|
|
||||||
def _region_contains(region: SourceRegion, location: SourceLocation) -> bool:
|
|
||||||
return location.path == region.path and region.start_line <= location.line <= region.end_line
|
|
||||||
|
|
||||||
|
|
||||||
def _scope_contains(scope: AnalyzedScope, location: SourceLocation) -> bool:
|
|
||||||
return any(_region_contains(region, location) for region in scope.regions)
|
|
||||||
|
|
||||||
|
|
||||||
def _members(record: EvidenceRecord, role: str) -> tuple[SemanticRelation | Observation, ...]:
|
|
||||||
transitions = tuple(item for item in record.packet.transitions if item.role == role)
|
|
||||||
observations = tuple(item for item in record.packet.observations if item.role == role)
|
|
||||||
return (*transitions, *observations)
|
|
||||||
|
|
||||||
|
|
||||||
def _question_state(record: EvidenceRecord, question: ResidualQuestion) -> tuple[bool, tuple[str, ...]]:
|
|
||||||
reasons: set[str] = set()
|
|
||||||
if record.decision.family_code not in question.allowed_decisions:
|
|
||||||
reasons.add(f"decision_not_declared_for_question:{record.decision.family_code}")
|
|
||||||
for role in question.required_roles:
|
|
||||||
members = _members(record, role)
|
|
||||||
if not members:
|
|
||||||
reasons.add(f"missing_required_role:{role}")
|
|
||||||
elif not any(
|
|
||||||
item.status is RelationStatus.COMPLETE and _scope_contains(record.decision.bounded_scope, item.location) for item in members
|
|
||||||
):
|
|
||||||
relation_reasons = {reason for item in members if isinstance(item, SemanticRelation) for reason in item.reasons}
|
|
||||||
complete_outside = any(item.status is RelationStatus.COMPLETE for item in members)
|
|
||||||
fallback = f"evidence_outside_bounded_scope:{role}" if complete_outside else f"incomplete_required_role:{role}"
|
|
||||||
reasons.update(relation_reasons or record.packet.completeness_reasons or (fallback,))
|
|
||||||
if record.decision.outcome is NeutralOutcome.INDETERMINATE:
|
|
||||||
reasons.update(record.decision.reasons)
|
|
||||||
complete = not reasons and record.decision.outcome is not NeutralOutcome.INDETERMINATE
|
|
||||||
return complete, tuple(sorted(reasons))
|
|
||||||
|
|
||||||
|
|
||||||
def _location(record: EvidenceRecord, question: ResidualQuestion) -> tuple[SourceLocation, SourceRegion]:
|
|
||||||
candidates = tuple(
|
|
||||||
item.location for role in question.required_roles for item in _members(record, role) if item.status is RelationStatus.COMPLETE
|
|
||||||
)
|
|
||||||
ordered = sorted(candidates, key=lambda item: (item.path, item.line, -1 if item.column is None else item.column, item.symbol or ""))
|
|
||||||
for location in ordered:
|
|
||||||
anchors = tuple(region for region in record.decision.bounded_scope.regions if _region_contains(region, location))
|
|
||||||
if anchors:
|
|
||||||
anchor = min(anchors, key=lambda item: (item.end_line - item.start_line, item.path, item.start_line))
|
|
||||||
return location, anchor
|
|
||||||
raise VerdictContractError("complete defect evidence has no exact location inside its bounded change scope")
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_inputs(records: Sequence[EvidenceRecord], contract: FamilyContract, coverage_digest: str) -> tuple[EvidenceRecord, ...]:
|
|
||||||
_digest(coverage_digest, "coverage")
|
|
||||||
try:
|
|
||||||
validate_contract(contract)
|
|
||||||
except (TypeError, ValueError) as exc:
|
|
||||||
raise VerdictContractError(f"family contract is invalid: {exc}") from exc
|
|
||||||
values = tuple(records)
|
|
||||||
if not values or not all(isinstance(record, EvidenceRecord) for record in values):
|
|
||||||
raise VerdictContractError("records must be a nonempty sequence of EvidenceRecord values")
|
|
||||||
identities: set[str] = set()
|
|
||||||
try:
|
|
||||||
for record in values:
|
|
||||||
record.__post_init__()
|
|
||||||
if record.record_id != derive_record_id(record.packet, record.decision):
|
|
||||||
raise EvidenceValidationError("record identity cannot be re-derived")
|
|
||||||
if record.record_id in identities:
|
|
||||||
raise EvidenceValidationError("duplicate evidence record identity")
|
|
||||||
identities.add(record.record_id)
|
|
||||||
validate_scope(record.packet, record.packet.scope)
|
|
||||||
validate_record(contract, record)
|
|
||||||
except (TypeError, ValueError, EvidenceValidationError, ScopeValidationError, FamilyContractError) as exc:
|
|
||||||
raise VerdictContractError(f"evidence record validation failed: {exc}") from exc
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
def _silence(
|
|
||||||
question: ResidualQuestion,
|
|
||||||
reasons: set[str],
|
|
||||||
record_ids: set[str],
|
|
||||||
coverage_digest: str,
|
|
||||||
) -> BoundedSilence | None:
|
|
||||||
if not reasons:
|
|
||||||
return None
|
|
||||||
return BoundedSilence(
|
|
||||||
(question.question_id,),
|
|
||||||
tuple(sorted(reasons)),
|
|
||||||
tuple(sorted(record_ids)),
|
|
||||||
coverage_digest,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def derive_verdict_outputs(
|
|
||||||
records: Sequence[EvidenceRecord],
|
|
||||||
contract: FamilyContract,
|
|
||||||
coverage_digest: str,
|
|
||||||
) -> VerdictOutputs:
|
|
||||||
"""Derive bounded outputs without converting incomplete evidence to safety."""
|
|
||||||
validated = _validate_inputs(records, contract, coverage_digest)
|
|
||||||
defects: dict[str, DefectClaim] = {}
|
|
||||||
satisfactions: dict[str, set[str]] = {}
|
|
||||||
silence: list[BoundedSilence] = []
|
|
||||||
for question in contract.residual_questions:
|
|
||||||
reasons: set[str] = set()
|
|
||||||
undecided: set[str] = set()
|
|
||||||
for record in validated:
|
|
||||||
complete, record_reasons = _question_state(record, question)
|
|
||||||
if not complete:
|
|
||||||
reasons.update(record_reasons)
|
|
||||||
undecided.add(record.record_id)
|
|
||||||
continue
|
|
||||||
if record.decision.outcome is NeutralOutcome.VIOLATION:
|
|
||||||
if record.record_id not in defects:
|
|
||||||
location, anchor = _location(record, question)
|
|
||||||
defects[record.record_id] = DefectClaim(record, record.decision.basis, location, anchor)
|
|
||||||
elif record.decision.outcome is NeutralOutcome.SATISFIED:
|
|
||||||
satisfactions.setdefault(record.record_id, set()).add(question.question_id)
|
|
||||||
bounded = _silence(question, reasons, undecided, coverage_digest)
|
|
||||||
if bounded is not None:
|
|
||||||
silence.append(bounded)
|
|
||||||
records_by_id = {record.record_id: record for record in validated}
|
|
||||||
satisfied = tuple(
|
|
||||||
BoundedSatisfaction(tuple(sorted(question_ids)), (records_by_id[record_id],), records_by_id[record_id].decision.bounded_scope)
|
|
||||||
for record_id, question_ids in sorted(satisfactions.items())
|
|
||||||
)
|
|
||||||
return VerdictOutputs(
|
|
||||||
tuple(defects[identity] for identity in sorted(defects)),
|
|
||||||
satisfied,
|
|
||||||
tuple(sorted(silence, key=lambda item: item.question_ids)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"BoundedSatisfaction",
|
|
||||||
"BoundedSilence",
|
|
||||||
"DefectClaim",
|
|
||||||
"VerdictContractError",
|
|
||||||
"VerdictOutputs",
|
|
||||||
"derive_verdict_outputs",
|
|
||||||
]
|
|
||||||
@@ -7,6 +7,8 @@ import subprocess
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core_migration_scope import CUTOVER_DELETIONS
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parents[1]
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
_EXCLUDED_PREFIXES = ("benchmarks/fixtures/", "benchmarks/pairs/", "tests/fixtures/")
|
_EXCLUDED_PREFIXES = ("benchmarks/fixtures/", "benchmarks/pairs/", "tests/fixtures/")
|
||||||
_LEGACY_IMPORTS: dict[str, frozenset[str] | None] = {
|
_LEGACY_IMPORTS: dict[str, frozenset[str] | None] = {
|
||||||
@@ -47,7 +49,7 @@ _LEGACY_IMPORTS: dict[str, frozenset[str] | None] = {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
"openultrasast.core.budget": frozenset({"Budget", "Complexity"}),
|
"openultrasast.core.budget": frozenset({"Budget", "Complexity"}),
|
||||||
"openultrasast.core.consumer": None,
|
"openultrasast.core.consumer": frozenset({"consume_verdict"}),
|
||||||
"openultrasast.core.coverage": frozenset({"Coverage", "InstrumentFailure", "ResolutionOutcome", "check_reads"}),
|
"openultrasast.core.coverage": frozenset({"Coverage", "InstrumentFailure", "ResolutionOutcome", "check_reads"}),
|
||||||
"openultrasast.core.factpack": frozenset({"FactPackError", "admit_fact_pack", "admit_pack_file", "load_pack_file", "verify_fact_pack"}),
|
"openultrasast.core.factpack": frozenset({"FactPackError", "admit_fact_pack", "admit_pack_file", "load_pack_file", "verify_fact_pack"}),
|
||||||
"openultrasast.core.probes": frozenset(
|
"openultrasast.core.probes": frozenset(
|
||||||
@@ -62,7 +64,7 @@ _LEGACY_IMPORTS: dict[str, frozenset[str] | None] = {
|
|||||||
),
|
),
|
||||||
"openultrasast.core.provenance": frozenset({"ProvenanceMismatch", "stamp", "verify_stamp"}),
|
"openultrasast.core.provenance": frozenset({"ProvenanceMismatch", "stamp", "verify_stamp"}),
|
||||||
"openultrasast.core.trace": None,
|
"openultrasast.core.trace": None,
|
||||||
"openultrasast.core.verdict": None,
|
"openultrasast.core.verdict": frozenset({"Judgment", "ProvenClaim", "as_clean"}),
|
||||||
}
|
}
|
||||||
_TOP_LEVEL_COMPATIBILITY = _LEGACY_IMPORTS["openultrasast.core"] or frozenset()
|
_TOP_LEVEL_COMPATIBILITY = _LEGACY_IMPORTS["openultrasast.core"] or frozenset()
|
||||||
|
|
||||||
@@ -80,10 +82,22 @@ class ReaderAuditReport:
|
|||||||
|
|
||||||
def tracked_python() -> tuple[Path, ...]:
|
def tracked_python() -> tuple[Path, ...]:
|
||||||
completed = subprocess.run(["git", "ls-files", "*.py"], cwd=REPO, check=True, capture_output=True, text=True)
|
completed = subprocess.run(["git", "ls-files", "*.py"], cwd=REPO, check=True, capture_output=True, text=True)
|
||||||
|
deleted = set(
|
||||||
|
subprocess.run(
|
||||||
|
["git", "ls-files", "--deleted", "--", "*.py"],
|
||||||
|
cwd=REPO,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
).stdout.splitlines()
|
||||||
|
)
|
||||||
|
unexpected = deleted - CUTOVER_DELETIONS
|
||||||
|
if unexpected:
|
||||||
|
raise LegacyReaderAuditError(f"unexpected tracked Python deletion: {min(unexpected)}")
|
||||||
return tuple(
|
return tuple(
|
||||||
REPO / relative
|
REPO / relative
|
||||||
for relative in completed.stdout.splitlines()
|
for relative in completed.stdout.splitlines()
|
||||||
if relative.endswith(".py") and not relative.startswith(_EXCLUDED_PREFIXES)
|
if relative.endswith(".py") and relative not in deleted and not relative.startswith(_EXCLUDED_PREFIXES)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from core_migration_scope import (
|
|||||||
COMPATIBILITY_PATHS,
|
COMPATIBILITY_PATHS,
|
||||||
COMPATIBILITY_SHIMS,
|
COMPATIBILITY_SHIMS,
|
||||||
FROZEN_TOP_LEVEL_V1,
|
FROZEN_TOP_LEVEL_V1,
|
||||||
|
REPLACED_SHIM_MEMBERS,
|
||||||
REPO,
|
REPO,
|
||||||
MigrationScopeError,
|
MigrationScopeError,
|
||||||
authoritative_cutover_scope,
|
authoritative_cutover_scope,
|
||||||
@@ -29,9 +30,7 @@ from core_migration_scope import (
|
|||||||
relative_path,
|
relative_path,
|
||||||
scope_identity,
|
scope_identity,
|
||||||
)
|
)
|
||||||
from core_migration_scope import (
|
from core_migration_scope import static_top_level_exports as _static_top_level_exports
|
||||||
static_top_level_v1 as _static_top_level_v1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyBoundaryError(ValueError):
|
class LegacyBoundaryError(ValueError):
|
||||||
@@ -71,9 +70,9 @@ class _Bindings:
|
|||||||
import_functions: frozenset[str]
|
import_functions: frozenset[str]
|
||||||
|
|
||||||
|
|
||||||
def static_top_level_v1(root: Path = REPO) -> tuple[str, ...]:
|
def static_top_level_exports(root: Path = REPO) -> tuple[str, ...]:
|
||||||
try:
|
try:
|
||||||
return _static_top_level_v1(root)
|
return _static_top_level_exports(root)
|
||||||
except MigrationScopeError as exc:
|
except MigrationScopeError as exc:
|
||||||
raise LegacyBoundaryError(str(exc)) from exc
|
raise LegacyBoundaryError(str(exc)) from exc
|
||||||
|
|
||||||
@@ -215,6 +214,19 @@ def _classify_import_from(
|
|||||||
legacy.add(_site(relative, node, parents, "compatibility_shim_import", shim))
|
legacy.add(_site(relative, node, parents, "compatibility_shim_import", shim))
|
||||||
if module in COMPATIBILITY_SHIMS:
|
if module in COMPATIBILITY_SHIMS:
|
||||||
legacy.add(_site(relative, node, parents, "compatibility_shim_import", module))
|
legacy.add(_site(relative, node, parents, "compatibility_shim_import", module))
|
||||||
|
_record_replaced_shim_members(relative, node, parents, module, names, legacy)
|
||||||
|
|
||||||
|
|
||||||
|
def _record_replaced_shim_members(
|
||||||
|
relative: str,
|
||||||
|
node: ast.ImportFrom,
|
||||||
|
parents: dict[ast.AST, ast.AST],
|
||||||
|
module: str,
|
||||||
|
names: set[str],
|
||||||
|
legacy: set[str],
|
||||||
|
) -> None:
|
||||||
|
for name in sorted(names.intersection(REPLACED_SHIM_MEMBERS.get(module, frozenset()))):
|
||||||
|
legacy.add(_site(relative, node, parents, "retired_shim_member_import", f"{module}.{name}"))
|
||||||
|
|
||||||
|
|
||||||
def _reference_sites(
|
def _reference_sites(
|
||||||
@@ -449,5 +461,5 @@ __all__ = [
|
|||||||
"boundary_python",
|
"boundary_python",
|
||||||
"inspect_legacy_boundary",
|
"inspect_legacy_boundary",
|
||||||
"require_cutover_ready",
|
"require_cutover_ready",
|
||||||
"static_top_level_v1",
|
"static_top_level_exports",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from benchmark_migration_evidence import BENCHMARK_TERMINAL_EVIDENCE, BenchmarkTerminalEvidence
|
from benchmark_migration_evidence import BENCHMARK_TERMINAL_EVIDENCE, BenchmarkTerminalEvidence
|
||||||
from consumer_migration_evidence import CONSUMER_TERMINAL_EVIDENCE, REVIEWED_SUCCESSOR_MODULES, ConsumerTerminalEvidence
|
from consumer_migration_evidence import CONSUMER_TERMINAL_EVIDENCE, REVIEWED_SUCCESSOR_MODULES, ConsumerTerminalEvidence
|
||||||
|
from core_migration_scope import CUTOVER_DELETIONS
|
||||||
|
|
||||||
from openultrasast.core.legacy import LegacyArtifact, decode_legacy_artifact
|
from openultrasast.core.legacy import LegacyArtifact, decode_legacy_artifact
|
||||||
|
|
||||||
@@ -830,16 +831,43 @@ def _approved_terminal_source_closure(entry: LegacyInventoryEntry, *, root: Path
|
|||||||
return True
|
return True
|
||||||
if _benchmark_terminal_source_closure(entry, root=root, tree=tree):
|
if _benchmark_terminal_source_closure(entry, root=root, tree=tree):
|
||||||
return True
|
return True
|
||||||
|
transfer = _APPROVED_MIXED_TRANSFERS.get(entry.entry_id)
|
||||||
|
if (
|
||||||
|
transfer is not None
|
||||||
|
and entry.disposition is not MigrationDisposition.OPEN
|
||||||
|
and entry.entry_id == entry_identity(entry)
|
||||||
|
and _stable_entry_fields(entry) == transfer[0]
|
||||||
|
and _cutover_complete(root)
|
||||||
|
and not any(_exact_anchor_present(anchor, tree) for anchor in entry.capability_anchors)
|
||||||
|
):
|
||||||
|
return True
|
||||||
approved = _APPROVED_TERMINAL_SOURCE_CLOSURES.get(entry.entry_id) is entry.disposition
|
approved = _APPROVED_TERMINAL_SOURCE_CLOSURES.get(entry.entry_id) is entry.disposition
|
||||||
return approved and entry.entry_id == entry_identity(entry)
|
return approved and entry.entry_id == entry_identity(entry)
|
||||||
|
|
||||||
|
|
||||||
|
def _cutover_complete(root: Path) -> bool:
|
||||||
|
if any((root / relative).exists() for relative in CUTOVER_DELETIONS):
|
||||||
|
return False
|
||||||
|
public = tuple(root / f"src/openultrasast/core/{name}.py" for name in ("__init__", "admission", "consumer", "verdict"))
|
||||||
|
return all(path.is_file() and path.stat().st_size > 0 for path in public)
|
||||||
|
|
||||||
|
|
||||||
|
def _approved_cutover_deletion(entry: LegacyInventoryEntry, *, root: Path) -> bool:
|
||||||
|
if entry.disposition is MigrationDisposition.OPEN or entry.path not in CUTOVER_DELETIONS or entry.entry_id != entry_identity(entry):
|
||||||
|
return False
|
||||||
|
return _cutover_complete(root)
|
||||||
|
|
||||||
|
|
||||||
def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Path) -> None:
|
def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Path) -> None:
|
||||||
"""Prove sources are readable and every open migration target remains present."""
|
"""Prove sources are readable and every open migration target remains present."""
|
||||||
for entry in inventory.entries:
|
for entry in inventory.entries:
|
||||||
path = root / entry.path
|
path = root / entry.path
|
||||||
try:
|
try:
|
||||||
content = path.read_bytes()
|
content = path.read_bytes()
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
if _approved_cutover_deletion(entry, root=root):
|
||||||
|
continue
|
||||||
|
raise LegacyInventoryError(f"inventory source is not readable: {entry.path}") from exc
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise LegacyInventoryError(f"inventory source is not readable: {entry.path}") from exc
|
raise LegacyInventoryError(f"inventory source is not readable: {entry.path}") from exc
|
||||||
if not content:
|
if not content:
|
||||||
@@ -849,8 +877,8 @@ def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Pat
|
|||||||
except (SyntaxError, UnicodeDecodeError) as exc:
|
except (SyntaxError, UnicodeDecodeError) as exc:
|
||||||
raise LegacyInventoryError(f"inventory source is not parseable: {entry.path}") from exc
|
raise LegacyInventoryError(f"inventory source is not parseable: {entry.path}") from exc
|
||||||
is_shim = _explicit_legacy_shim(tree, entry.symbol)
|
is_shim = _explicit_legacy_shim(tree, entry.symbol)
|
||||||
moved_owner = _moved_mixed_v1_owner(entry, root=root)
|
|
||||||
terminal_closure = _approved_terminal_source_closure(entry, root=root, tree=tree)
|
terminal_closure = _approved_terminal_source_closure(entry, root=root, tree=tree)
|
||||||
|
moved_owner = False if terminal_closure else _moved_mixed_v1_owner(entry, root=root)
|
||||||
if entry.symbol not in _defined_symbols(tree) and not is_shim and not moved_owner and not terminal_closure:
|
if entry.symbol not in _defined_symbols(tree) and not is_shim and not moved_owner and not terminal_closure:
|
||||||
raise LegacyInventoryError(f"inventory target is absent: {entry.path}:{entry.symbol}")
|
raise LegacyInventoryError(f"inventory target is absent: {entry.path}:{entry.symbol}")
|
||||||
missing = _missing_anchors(entry, tree)
|
missing = _missing_anchors(entry, tree)
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ COMPATIBILITY_PATHS = frozenset(
|
|||||||
src/openultrasast/core/admission.py src/openultrasast/core/consumer.py
|
src/openultrasast/core/admission.py src/openultrasast/core/consumer.py
|
||||||
src/openultrasast/core/trace.py src/openultrasast/core/verdict.py""".split() # noqa: SIM905
|
src/openultrasast/core/trace.py src/openultrasast/core/verdict.py""".split() # noqa: SIM905
|
||||||
)
|
)
|
||||||
COMPATIBILITY_SHIMS = frozenset(
|
COMPATIBILITY_SHIMS = frozenset(["openultrasast.core._legacy_factpack", "openultrasast.core.trace"])
|
||||||
"""openultrasast.core._legacy_factpack openultrasast.core.admission
|
REPLACED_SHIM_MEMBERS = {
|
||||||
openultrasast.core.consumer openultrasast.core.trace openultrasast.core.verdict""".split() # noqa: SIM905
|
"openultrasast.core.admission": frozenset(
|
||||||
)
|
{"REPORT_SCHEMA_KEYS", "AdmissionManifest", "AdmissionManifestError", "validate_admission_manifest", "validate_report_schema"}
|
||||||
|
),
|
||||||
|
"openultrasast.core.consumer": frozenset({"consume_verdict"}),
|
||||||
|
"openultrasast.core.verdict": frozenset({"Judgment", "ProvenClaim", "as_clean"}),
|
||||||
|
}
|
||||||
FROZEN_TOP_LEVEL_V1 = tuple(
|
FROZEN_TOP_LEVEL_V1 = tuple(
|
||||||
"""REPORT_SCHEMA_KEYS AdmissionManifest AdmissionManifestError BoundedSilence Budget BudgetGateFailure
|
"""REPORT_SCHEMA_KEYS AdmissionManifest AdmissionManifestError BoundedSilence Budget BudgetGateFailure
|
||||||
Complexity Coverage FactPack FactPackError FittedComponentBlocked Guard Hop InstrumentFailure Judgment ProbeResult
|
Complexity Coverage FactPack FactPackError FittedComponentBlocked Guard Hop InstrumentFailure Judgment ProbeResult
|
||||||
@@ -27,6 +31,17 @@ FROZEN_TOP_LEVEL_V1 = tuple(
|
|||||||
require_admission_evidence reversed_pair_probe stamp validate_admission_manifest validate_report_schema
|
require_admission_evidence reversed_pair_probe stamp validate_admission_manifest validate_report_schema
|
||||||
verify_fact_pack verify_stamp""".split() # noqa: SIM905
|
verify_fact_pack verify_stamp""".split() # noqa: SIM905
|
||||||
)
|
)
|
||||||
|
CUTOVER_DELETIONS = frozenset(
|
||||||
|
{
|
||||||
|
"src/openultrasast/core/_legacy_factpack.py",
|
||||||
|
"src/openultrasast/core/consumer_v2.py",
|
||||||
|
"src/openultrasast/core/legacy_v1/__init__.py",
|
||||||
|
"src/openultrasast/core/legacy_v1/contracts.py",
|
||||||
|
"src/openultrasast/core/legacy_v1/writer.py",
|
||||||
|
"src/openultrasast/core/trace.py",
|
||||||
|
"src/openultrasast/core/verdict_v2.py",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MigrationScopeError(ValueError):
|
class MigrationScopeError(ValueError):
|
||||||
@@ -47,6 +62,19 @@ def boundary_python(root: Path = REPO) -> tuple[Path, ...]:
|
|||||||
commands = (("git", "ls-files", "*.py"), ("git", "ls-files", "--others", "--exclude-standard", "--", "*.py"))
|
commands = (("git", "ls-files", "*.py"), ("git", "ls-files", "--others", "--exclude-standard", "--", "*.py"))
|
||||||
outputs = (subprocess.run(command, cwd=root, check=True, capture_output=True, text=True).stdout for command in commands)
|
outputs = (subprocess.run(command, cwd=root, check=True, capture_output=True, text=True).stdout for command in commands)
|
||||||
relatives = {value for output in outputs for value in output.splitlines() if _owned_python(value)}
|
relatives = {value for output in outputs for value in output.splitlines() if _owned_python(value)}
|
||||||
|
deleted = set(
|
||||||
|
subprocess.run(
|
||||||
|
("git", "ls-files", "--deleted", "--", "*.py"),
|
||||||
|
cwd=root,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
).stdout.splitlines()
|
||||||
|
)
|
||||||
|
unexpected = deleted - CUTOVER_DELETIONS
|
||||||
|
if unexpected:
|
||||||
|
raise MigrationScopeError(f"unexpected tracked Python deletion: {min(unexpected)}")
|
||||||
|
relatives.difference_update(deleted)
|
||||||
return tuple(root / relative for relative in sorted(relatives))
|
return tuple(root / relative for relative in sorted(relatives))
|
||||||
|
|
||||||
|
|
||||||
@@ -149,8 +177,8 @@ def relative_path(path: Path, root: Path) -> str:
|
|||||||
return path.as_posix()
|
return path.as_posix()
|
||||||
|
|
||||||
|
|
||||||
def static_top_level_v1(root: Path = REPO) -> tuple[str, ...]:
|
def static_top_level_exports(root: Path = REPO) -> tuple[str, ...]:
|
||||||
"""Read the migration shim's literal ``__all__`` without importing it."""
|
"""Read the public package's literal ``__all__`` without importing it."""
|
||||||
path = root / "src/openultrasast/core/__init__.py"
|
path = root / "src/openultrasast/core/__init__.py"
|
||||||
try:
|
try:
|
||||||
content = path.read_bytes()
|
content = path.read_bytes()
|
||||||
@@ -185,6 +213,7 @@ __all__ = [
|
|||||||
"FROZEN_TOP_LEVEL_V1",
|
"FROZEN_TOP_LEVEL_V1",
|
||||||
"COMPATIBILITY_PATHS",
|
"COMPATIBILITY_PATHS",
|
||||||
"COMPATIBILITY_SHIMS",
|
"COMPATIBILITY_SHIMS",
|
||||||
|
"CUTOVER_DELETIONS",
|
||||||
"MigrationScopeError",
|
"MigrationScopeError",
|
||||||
"REPO",
|
"REPO",
|
||||||
"authoritative_cutover_scope",
|
"authoritative_cutover_scope",
|
||||||
@@ -197,5 +226,5 @@ __all__ = [
|
|||||||
"relative_path",
|
"relative_path",
|
||||||
"scope_identity_error",
|
"scope_identity_error",
|
||||||
"scope_identity",
|
"scope_identity",
|
||||||
"static_top_level_v1",
|
"static_top_level_exports",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ from openultrasast.core.family_catalog import FamilyCatalog, FamilyCatalogEntry,
|
|||||||
from openultrasast.core.measurement import DenominatorPolicy, IntervalMethod, MetricId, ScopeKey, standard_metric_rules
|
from openultrasast.core.measurement import DenominatorPolicy, IntervalMethod, MetricId, ScopeKey, standard_metric_rules
|
||||||
from openultrasast.core.readiness import ReadinessResult
|
from openultrasast.core.readiness import ReadinessResult
|
||||||
from openultrasast.core.semantics import RelationStatus
|
from openultrasast.core.semantics import RelationStatus
|
||||||
from openultrasast.core.verdict_v2 import derive_verdict_outputs
|
from openultrasast.core.verdict import derive_verdict_outputs
|
||||||
|
|
||||||
|
|
||||||
class _InputManifestSubclass(InputManifest):
|
class _InputManifestSubclass(InputManifest):
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ from test_core_validation_v2 import (
|
|||||||
_with_fact_set,
|
_with_fact_set,
|
||||||
)
|
)
|
||||||
|
|
||||||
import openultrasast.core.consumer_v2 as consumer_v2
|
import openultrasast.core.consumer as consumer_v2
|
||||||
from openultrasast.core import budget, canonical, evidence, validation
|
from openultrasast.core import budget, canonical, evidence, validation
|
||||||
from openultrasast.core._proof_attestation import proof_attestation_bytes
|
from openultrasast.core._proof_attestation import proof_attestation_bytes
|
||||||
from openultrasast.core.consumer_v2 import ConsumerContractError, PushRequest, consume_records, make_push_request
|
from openultrasast.core.consumer import ConsumerContractError, PushRequest, consume_records, make_push_request
|
||||||
from openultrasast.core.family import FamilyContract
|
from openultrasast.core.family import FamilyContract
|
||||||
from openultrasast.core.proof import (
|
from openultrasast.core.proof import (
|
||||||
DIFFERENTIAL_PROOF_SCHEMA,
|
DIFFERENTIAL_PROOF_SCHEMA,
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
"""Final deletion-backed gate for the detection-core v1 migration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core_migration_boundary import BoundaryPhase, boundary_python, inspect_legacy_boundary, require_cutover_ready
|
||||||
|
from core_migration_inventory import load_migration_inventory, validate_historical_artifacts
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
CORE = REPO / "src/openultrasast/core"
|
||||||
|
INVENTORY = REPO / "tests/fixtures/detection_core/v1/migration_inventory.json"
|
||||||
|
REMOVED_PATHS = (
|
||||||
|
CORE / "legacy_v1",
|
||||||
|
CORE / "trace.py",
|
||||||
|
CORE / "_legacy_factpack.py",
|
||||||
|
CORE / "consumer_v2.py",
|
||||||
|
CORE / "verdict_v2.py",
|
||||||
|
)
|
||||||
|
CANONICAL_V2_MODULES = ("admission", "consumer", "verdict")
|
||||||
|
OLD_TOP_LEVEL = (
|
||||||
|
"AdmissionManifest",
|
||||||
|
"Budget",
|
||||||
|
"Coverage",
|
||||||
|
"FactPack",
|
||||||
|
"PushRequest",
|
||||||
|
"PushVerdict",
|
||||||
|
"TraceRecord",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _module_exports(path: Path) -> tuple[str, ...]:
|
||||||
|
content = path.read_bytes()
|
||||||
|
assert content, f"public-surface instrument must read nonzero bytes from {path}"
|
||||||
|
tree = ast.parse(content, filename=path.relative_to(REPO).as_posix())
|
||||||
|
declarations = [
|
||||||
|
node
|
||||||
|
for node in tree.body
|
||||||
|
if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "__all__" for target in node.targets)
|
||||||
|
]
|
||||||
|
assert len(declarations) == 1
|
||||||
|
value = declarations[0].value
|
||||||
|
assert isinstance(value, (ast.List, ast.Tuple))
|
||||||
|
assert all(isinstance(item, ast.Constant) and isinstance(item.value, str) for item in value.elts)
|
||||||
|
return tuple(item.value for item in value.elts)
|
||||||
|
|
||||||
|
|
||||||
|
def _fresh_import_probe(*module_names: str) -> dict[str, object]:
|
||||||
|
program = """
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
core = importlib.import_module("openultrasast" + ".core")
|
||||||
|
result = {
|
||||||
|
"all": tuple(core.__all__),
|
||||||
|
"old_top_level": tuple(name for name in sys.argv[1].split(",") if hasattr(core, name)),
|
||||||
|
"modules": {},
|
||||||
|
}
|
||||||
|
for name in sys.argv[2:]:
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(name)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
result["modules"][name] = None
|
||||||
|
else:
|
||||||
|
result["modules"][name] = tuple(module.__all__)
|
||||||
|
print(json.dumps(result, sort_keys=True))
|
||||||
|
"""
|
||||||
|
completed = subprocess.run(
|
||||||
|
[sys.executable, "-c", program, ",".join(OLD_TOP_LEVEL), *module_names],
|
||||||
|
cwd=REPO,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env={"PYTHONPATH": str(REPO / "src")},
|
||||||
|
)
|
||||||
|
assert completed.stdout, "fresh import probe must produce nonzero output"
|
||||||
|
value = json.loads(completed.stdout)
|
||||||
|
assert isinstance(value, dict)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def test_cutover_deletes_every_v1_write_and_temporary_shim_path() -> None:
|
||||||
|
for path in REMOVED_PATHS:
|
||||||
|
assert not path.exists(), f"cutover residue remains: {path.relative_to(REPO)}"
|
||||||
|
for name in CANONICAL_V2_MODULES:
|
||||||
|
path = CORE / f"{name}.py"
|
||||||
|
assert path.is_file()
|
||||||
|
assert path.stat().st_size > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_fresh_interpreter_rejects_old_imports_and_loads_canonical_v2() -> None:
|
||||||
|
old_modules = (
|
||||||
|
"openultrasast.core.legacy_v1",
|
||||||
|
"openultrasast.core.trace",
|
||||||
|
"openultrasast.core._legacy_factpack",
|
||||||
|
"openultrasast.core." + "consumer_v2",
|
||||||
|
"openultrasast.core." + "verdict_v2",
|
||||||
|
)
|
||||||
|
canonical = tuple(f"openultrasast.core.{name}" for name in CANONICAL_V2_MODULES)
|
||||||
|
result = _fresh_import_probe(*old_modules, *canonical)
|
||||||
|
|
||||||
|
assert result["old_top_level"] == []
|
||||||
|
assert set(CANONICAL_V2_MODULES) <= set(result["all"])
|
||||||
|
modules = result["modules"]
|
||||||
|
assert isinstance(modules, dict)
|
||||||
|
assert all(modules[name] is None for name in old_modules)
|
||||||
|
assert "AdmissionManifestV2" in modules[canonical[0]]
|
||||||
|
assert "consume_records" in modules[canonical[1]]
|
||||||
|
assert "derive_verdict_outputs" in modules[canonical[2]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_v2_surface_has_no_frozen_v1_names() -> None:
|
||||||
|
exports = _module_exports(CORE / "__init__.py")
|
||||||
|
|
||||||
|
assert set(CANONICAL_V2_MODULES) <= set(exports)
|
||||||
|
assert set(OLD_TOP_LEVEL).isdisjoint(exports)
|
||||||
|
|
||||||
|
|
||||||
|
def test_repository_post_deletion_boundary_is_nonzero_and_exact_zero() -> None:
|
||||||
|
paths = boundary_python()
|
||||||
|
inventory = load_migration_inventory(INVENTORY)
|
||||||
|
report = inspect_legacy_boundary(
|
||||||
|
paths,
|
||||||
|
frozen_inventory=inventory,
|
||||||
|
candidate_inventory=inventory,
|
||||||
|
expected_scope=paths,
|
||||||
|
root=REPO,
|
||||||
|
phase=BoundaryPhase.POST_DELETION,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert report.files_read == len(paths) > 0
|
||||||
|
assert report.bytes_read > 0
|
||||||
|
assert report.open_inventory_entries == ()
|
||||||
|
assert report.legacy_import_sites == ()
|
||||||
|
assert report.legacy_writer_sites == ()
|
||||||
|
assert report.consumer_legacy_import_sites == ()
|
||||||
|
assert report.consumer_legacy_writer_sites == ()
|
||||||
|
assert report.compatibility_implementation_sites == ()
|
||||||
|
assert report.forbidden_dynamic_sites == ()
|
||||||
|
require_cutover_ready(
|
||||||
|
report,
|
||||||
|
frozen_inventory=inventory,
|
||||||
|
candidate_inventory=inventory,
|
||||||
|
root=REPO,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_historical_decoder_remains_exact_read_only_and_outside_public_v2() -> None:
|
||||||
|
inventory_bytes = INVENTORY.read_bytes()
|
||||||
|
assert inventory_bytes, "historical gate must read nonzero inventory bytes"
|
||||||
|
inventory = load_migration_inventory(INVENTORY)
|
||||||
|
artifacts = validate_historical_artifacts(inventory, root=REPO)
|
||||||
|
|
||||||
|
assert len(artifacts) == 2
|
||||||
|
assert all(artifact.content for artifact in artifacts)
|
||||||
|
assert all(hashlib.sha256(artifact.content).hexdigest() == artifact.content_digest for artifact in artifacts)
|
||||||
|
legacy_exports = _module_exports(CORE / "legacy.py")
|
||||||
|
assert {"write", "serialize", "to_v2", "compare_identity"}.isdisjoint(legacy_exports)
|
||||||
|
assert "legacy" not in _module_exports(CORE / "__init__.py")
|
||||||
@@ -14,7 +14,7 @@ from core_migration_boundary import (
|
|||||||
boundary_python,
|
boundary_python,
|
||||||
inspect_legacy_boundary,
|
inspect_legacy_boundary,
|
||||||
require_cutover_ready,
|
require_cutover_ready,
|
||||||
static_top_level_v1,
|
static_top_level_exports,
|
||||||
)
|
)
|
||||||
from core_migration_inventory import (
|
from core_migration_inventory import (
|
||||||
LegacyInventoryError,
|
LegacyInventoryError,
|
||||||
@@ -214,7 +214,9 @@ def test_cutover_refuses_empty_scope_and_migration_compatibility(tmp_path: Path)
|
|||||||
|
|
||||||
|
|
||||||
def test_complete_frozen_top_level_surface_is_reported_and_star_is_forbidden(tmp_path: Path) -> None:
|
def test_complete_frozen_top_level_surface_is_reported_and_star_is_forbidden(tmp_path: Path) -> None:
|
||||||
assert static_top_level_v1(REPO) == FROZEN_TOP_LEVEL_V1
|
exports = static_top_level_exports(REPO)
|
||||||
|
assert exports == ("admission", "consumer", "verdict")
|
||||||
|
assert set(exports).isdisjoint(FROZEN_TOP_LEVEL_V1)
|
||||||
assert len(FROZEN_TOP_LEVEL_V1) == 38
|
assert len(FROZEN_TOP_LEVEL_V1) == 38
|
||||||
imports = ", ".join(FROZEN_TOP_LEVEL_V1)
|
imports = ", ".join(FROZEN_TOP_LEVEL_V1)
|
||||||
attributes = "\n".join(f"value_{index} = core_api.{name}" for index, name in enumerate(FROZEN_TOP_LEVEL_V1))
|
attributes = "\n".join(f"value_{index} = core_api.{name}" for index, name in enumerate(FROZEN_TOP_LEVEL_V1))
|
||||||
@@ -283,16 +285,25 @@ def test_repository_scope_proves_nonzero_inputs_before_reporting_sites() -> None
|
|||||||
paths = boundary_python()
|
paths = boundary_python()
|
||||||
frozen = load_migration_inventory(INVENTORY)
|
frozen = load_migration_inventory(INVENTORY)
|
||||||
|
|
||||||
report = inspect_legacy_boundary(paths, frozen_inventory=frozen, root=REPO)
|
report = inspect_legacy_boundary(
|
||||||
|
paths,
|
||||||
|
frozen_inventory=frozen,
|
||||||
|
candidate_inventory=frozen,
|
||||||
|
expected_scope=paths,
|
||||||
|
root=REPO,
|
||||||
|
phase=BoundaryPhase.POST_DELETION,
|
||||||
|
)
|
||||||
|
|
||||||
assert report.files_read == len(paths) > 0
|
assert report.files_read == len(paths) > 0
|
||||||
assert report.bytes_read > 0
|
assert report.bytes_read > 0
|
||||||
assert report.legacy_import_sites
|
assert report.legacy_import_sites == ()
|
||||||
|
assert report.legacy_writer_sites == ()
|
||||||
assert report.open_inventory_entries == ()
|
assert report.open_inventory_entries == ()
|
||||||
assert report.consumer_legacy_import_sites == ()
|
assert report.consumer_legacy_import_sites == ()
|
||||||
assert report.consumer_legacy_writer_sites == ()
|
assert report.consumer_legacy_writer_sites == ()
|
||||||
assert report.forbidden_dynamic_sites == ()
|
assert report.forbidden_dynamic_sites == ()
|
||||||
assert report.compatibility_implementation_sites
|
assert report.compatibility_implementation_sites == ()
|
||||||
|
_require_cutover(report, frozen=frozen, candidate=frozen, root=REPO)
|
||||||
|
|
||||||
|
|
||||||
def test_cutover_rejects_positive_but_incomplete_repository_scope() -> None:
|
def test_cutover_rejects_positive_but_incomplete_repository_scope() -> None:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Structural ownership tests for the temporary v1 compatibility implementation."""
|
"""Structural ownership tests for the final v2-only core namespace."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
REPO = Path(__file__).resolve().parents[1]
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
CORE = REPO / "src/openultrasast/core"
|
CORE = REPO / "src/openultrasast/core"
|
||||||
PURE_COMPATIBILITY_MODULES = ("admission.py", "consumer.py", "trace.py", "verdict.py", "_legacy_factpack.py")
|
REMOVED_COMPATIBILITY = ("trace.py", "_legacy_factpack.py", "consumer_v2.py", "verdict_v2.py")
|
||||||
MIXED_LEGACY_NAMES = {
|
MIXED_LEGACY_NAMES = {
|
||||||
"budget": {"Budget", "Complexity"},
|
"budget": {"Budget", "Complexity"},
|
||||||
"coverage": {"Coverage", "ResolutionOutcome", "check_reads"},
|
"coverage": {"Coverage", "ResolutionOutcome", "check_reads"},
|
||||||
@@ -38,76 +38,58 @@ def _imported(tree: ast.Module) -> set[str]:
|
|||||||
return {alias.asname or alias.name for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom)) for alias in node.names}
|
return {alias.asname or alias.name for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom)) for alias in node.names}
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_contract_values_have_one_compatibility_owner() -> None:
|
def _literal_all(tree: ast.Module) -> tuple[str, ...]:
|
||||||
contracts = _tree(CORE / "legacy_v1/contracts.py")
|
values = [
|
||||||
expected = {
|
node.value
|
||||||
"AdmissionManifest",
|
for node in tree.body
|
||||||
"AdmissionManifestError",
|
if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "__all__" for target in node.targets)
|
||||||
"BoundedSilence",
|
|
||||||
"Budget",
|
|
||||||
"BudgetGateFailure",
|
|
||||||
"Complexity",
|
|
||||||
"Coverage",
|
|
||||||
"FactPackError",
|
|
||||||
"FittedComponentBlocked",
|
|
||||||
"Guard",
|
|
||||||
"Hop",
|
|
||||||
"InstrumentFailure",
|
|
||||||
"Judgment",
|
|
||||||
"LegacyFactPack",
|
|
||||||
"LegacyProbeResult",
|
|
||||||
"ProvenClaim",
|
|
||||||
"ProvenanceMismatch",
|
|
||||||
"PushRequest",
|
|
||||||
"PushVerdict",
|
|
||||||
"ResolutionOutcome",
|
|
||||||
"ShortcutDetected",
|
|
||||||
"TraceRecord",
|
|
||||||
"Witness",
|
|
||||||
}
|
|
||||||
assert expected <= _defined(contracts)
|
|
||||||
|
|
||||||
|
|
||||||
def test_pure_compatibility_modules_define_no_contract_or_serializer() -> None:
|
|
||||||
for name in PURE_COMPATIBILITY_MODULES:
|
|
||||||
tree = _tree(CORE / name)
|
|
||||||
assert not _defined(tree)
|
|
||||||
assert not any(isinstance(node, (ast.Import, ast.ImportFrom)) and "json" in ast.unparse(node) for node in tree.body)
|
|
||||||
assert any(isinstance(node, ast.ImportFrom) and node.module == "openultrasast.core.legacy_v1.contracts" for node in tree.body)
|
|
||||||
|
|
||||||
|
|
||||||
def test_writer_is_the_only_compatibility_json_serializer() -> None:
|
|
||||||
namespace = CORE / "legacy_v1"
|
|
||||||
paths = sorted(namespace.glob("*.py"))
|
|
||||||
assert {path.name for path in paths} == {"__init__.py", "contracts.py", "writer.py"}
|
|
||||||
json_importers = [
|
|
||||||
path.name
|
|
||||||
for path in paths
|
|
||||||
if any(isinstance(node, (ast.Import, ast.ImportFrom)) and "json" in ast.unparse(node) for node in _tree(path).body)
|
|
||||||
]
|
]
|
||||||
assert json_importers == ["writer.py"]
|
assert len(values) == 1 and isinstance(values[0], (ast.List, ast.Tuple))
|
||||||
|
assert all(isinstance(item, ast.Constant) and isinstance(item.value, str) for item in values[0].elts)
|
||||||
|
return tuple(item.value for item in values[0].elts)
|
||||||
|
|
||||||
|
|
||||||
def test_namespace_and_v2_dependency_directions_are_closed() -> None:
|
def test_compatibility_namespace_writer_and_temporary_shims_are_absent() -> None:
|
||||||
for path in (CORE / "legacy_v1").glob("*.py"):
|
assert not (CORE / "legacy_v1").exists()
|
||||||
imports = {node.module for node in ast.walk(_tree(path)) if isinstance(node, ast.ImportFrom) and node.module}
|
for name in REMOVED_COMPATIBILITY:
|
||||||
assert all(module.startswith("openultrasast.core.legacy_v1") for module in imports if module.startswith("openultrasast"))
|
assert not (CORE / name).exists()
|
||||||
compatibility = {*PURE_COMPATIBILITY_MODULES, "legacy.py", "__init__.py"}
|
|
||||||
for path in CORE.glob("*.py"):
|
|
||||||
if path.name not in compatibility:
|
|
||||||
imports = {node.module for node in ast.walk(_tree(path)) if isinstance(node, ast.ImportFrom) and node.module}
|
|
||||||
assert not any(module.startswith("openultrasast.core.legacy_v1") for module in imports)
|
|
||||||
|
|
||||||
|
|
||||||
def test_historical_decoder_stays_independent_from_compatibility_namespace() -> None:
|
def test_canonical_v2_replacements_own_the_public_paths() -> None:
|
||||||
imports = {node.module for node in ast.walk(_tree(CORE / "legacy.py")) if isinstance(node, ast.ImportFrom)}
|
consumer = _tree(CORE / "consumer.py")
|
||||||
|
verdict = _tree(CORE / "verdict.py")
|
||||||
|
admission = _tree(CORE / "admission.py")
|
||||||
|
|
||||||
|
assert {"PushRequest", "PushVerdict", "consume_records"} <= _defined(consumer)
|
||||||
|
assert {"BoundedSilence", "DefectClaim", "derive_verdict_outputs"} <= _defined(verdict)
|
||||||
|
assert {"AdmissionManifestV2", "authorize_admission", "score_admission"} <= set(_literal_all(admission))
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_modules_have_no_legacy_namespace_dependency() -> None:
|
||||||
|
paths = tuple(sorted(CORE.glob("*.py")))
|
||||||
|
assert paths and all(path.read_bytes() for path in paths)
|
||||||
|
for path in paths:
|
||||||
|
tree = _tree(path)
|
||||||
|
imports = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module}
|
||||||
|
imported_modules = {alias.name for node in ast.walk(tree) if isinstance(node, ast.Import) for alias in node.names}
|
||||||
|
assert not any(module.startswith("openultrasast.core.legacy_v1") for module in imports | imported_modules)
|
||||||
|
|
||||||
|
|
||||||
|
def test_historical_decoder_stays_independent_and_read_only() -> None:
|
||||||
|
tree = _tree(CORE / "legacy.py")
|
||||||
|
imports = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom)}
|
||||||
|
exports = set(_literal_all(tree))
|
||||||
|
|
||||||
assert "openultrasast.core.legacy_v1" not in imports
|
assert "openultrasast.core.legacy_v1" not in imports
|
||||||
|
assert {"write", "serialize", "to_v2", "compare_identity"}.isdisjoint(exports)
|
||||||
|
|
||||||
|
|
||||||
def test_top_level_v1_api_is_only_a_closed_namespace_shim() -> None:
|
def test_top_level_api_publishes_only_canonical_v2_modules() -> None:
|
||||||
tree = _tree(CORE / "__init__.py")
|
tree = _tree(CORE / "__init__.py")
|
||||||
modules = {node.module for node in tree.body if isinstance(node, ast.ImportFrom)}
|
modules = {node.module for node in tree.body if isinstance(node, ast.ImportFrom)}
|
||||||
assert modules == {"openultrasast.core.legacy_v1"}
|
|
||||||
|
assert modules == {"openultrasast.core"}
|
||||||
|
assert _literal_all(tree) == ("admission", "consumer", "verdict")
|
||||||
assert not _defined(tree)
|
assert not _defined(tree)
|
||||||
|
|
||||||
|
|
||||||
@@ -115,9 +97,3 @@ def test_mixed_v2_modules_expose_no_legacy_compatibility_names() -> None:
|
|||||||
for module_name, legacy_names in MIXED_LEGACY_NAMES.items():
|
for module_name, legacy_names in MIXED_LEGACY_NAMES.items():
|
||||||
tree = _tree(CORE / f"{module_name}.py")
|
tree = _tree(CORE / f"{module_name}.py")
|
||||||
assert legacy_names.isdisjoint(_defined(tree) | _imported(tree))
|
assert legacy_names.isdisjoint(_defined(tree) | _imported(tree))
|
||||||
|
|
||||||
|
|
||||||
def test_closed_namespace_is_the_only_owner_of_moved_mixed_names() -> None:
|
|
||||||
contracts = _tree(CORE / "legacy_v1/contracts.py")
|
|
||||||
expected = set().union(*MIXED_LEGACY_NAMES.values())
|
|
||||||
assert expected <= _defined(contracts)
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ from openultrasast.core.semantics import (
|
|||||||
make_observation,
|
make_observation,
|
||||||
make_relation,
|
make_relation,
|
||||||
)
|
)
|
||||||
from openultrasast.core.verdict_v2 import DefectClaim
|
from openultrasast.core.verdict import DefectClaim
|
||||||
|
|
||||||
|
|
||||||
def _digest(value: str) -> str:
|
def _digest(value: str) -> str:
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ from openultrasast.core.semantics import (
|
|||||||
SourceLocation,
|
SourceLocation,
|
||||||
make_relation,
|
make_relation,
|
||||||
)
|
)
|
||||||
from openultrasast.core.verdict_v2 import DefectClaim, VerdictContractError, derive_verdict_outputs
|
from openultrasast.core.verdict import DefectClaim, VerdictContractError, derive_verdict_outputs
|
||||||
|
|
||||||
|
|
||||||
def _digest(value: str) -> str:
|
def _digest(value: str) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user