feat(detection-core): isolate legacy compatibility shims

This commit is contained in:
Marius Ciepluch
2026-11-02 12:04:13 +01:00
parent 856312c36e
commit ff8912a2a2
22 changed files with 569 additions and 355 deletions
+1 -1
View File
@@ -361,7 +361,7 @@
- Done when old writes remain byte-identical, the compatibility namespace is the only live v1 contract and write owner, and ownership/import tests refuse any duplicate implementation. - Done when old writes remain byte-identical, the compatibility namespace is the only live v1 contract and write owner, and ownership/import tests refuse any duplicate implementation.
- _Requirements: 14.3, 15.3_ - _Requirements: 14.3, 15.3_
- _Depends: 1.1_ - _Depends: 1.1_
- [ ] 10.2 Convert the temporary public and module compatibility surfaces into explicit shims - [x] 10.2 Convert the temporary public and module compatibility surfaces into explicit shims
- Keep the old named top-level API and designated compatibility modules as explicit imports from the isolated namespace during migration. - Keep the old named top-level API and designated compatibility modules as explicit imports from the isolated namespace during migration.
- Remove legacy definitions and re-exports from mixed v2 modules so each compatibility name has exactly one owner. - Remove legacy definitions and re-exports from mixed v2 modules so each compatibility name has exactly one owner.
- Keep the canonical v2 paths private until the final atomic cutover. - Keep the canonical v2 paths private until the final atomic cutover.
+2 -2
View File
@@ -26,8 +26,8 @@ from benchmarks.unseen.replay import canonical
from benchmarks.valid.catalog import local_git from benchmarks.valid.catalog import local_git
from benchmarks.valid.development_runner import AnalyzerBundle, stage_funnel from benchmarks.valid.development_runner import AnalyzerBundle, stage_funnel
from benchmarks.valid.prover_arm import _write_json from benchmarks.valid.prover_arm import _write_json
from openultrasast.core.coverage import InstrumentFailure as CoreInstrumentFailure from openultrasast.core.legacy_v1 import InstrumentFailure as CoreInstrumentFailure
from openultrasast.core.coverage import check_reads from openultrasast.core.legacy_v1 import check_reads
from openultrasast.hunt.producers import Context, _Absent, _safe_path from openultrasast.hunt.producers import Context, _Absent, _safe_path
from openultrasast.hunt.prover import prove_change from openultrasast.hunt.prover import prove_change
+31 -16
View File
@@ -4,31 +4,46 @@ Owns contracts only -- no analysis logic. Arms implement the trace contract;
runners and consumers adopt coverage, budget, provenance, and admission contracts. runners and consumers adopt coverage, budget, provenance, and admission contracts.
""" """
from openultrasast.core._legacy_factpack import LegacyFactPack as FactPack from openultrasast.core.legacy_v1 import (
from openultrasast.core.admission import (
REPORT_SCHEMA_KEYS, REPORT_SCHEMA_KEYS,
AdmissionManifest, AdmissionManifest,
AdmissionManifestError, AdmissionManifestError,
validate_admission_manifest, BoundedSilence,
validate_report_schema, Budget,
) BudgetGateFailure,
from openultrasast.core.budget import Budget, BudgetGateFailure, Complexity, check_budget, derive_budget Complexity,
from openultrasast.core.consumer import PushRequest, PushVerdict, consume_verdict Coverage,
from openultrasast.core.coverage import Coverage, InstrumentFailure, ResolutionOutcome, check_reads FactPack,
from openultrasast.core.factpack import FactPackError, admit_fact_pack, verify_fact_pack FactPackError,
from openultrasast.core.probes import (
FittedComponentBlocked, FittedComponentBlocked,
Guard,
Hop,
InstrumentFailure,
Judgment,
ProbeResult,
ProvenanceMismatch,
ProvenClaim,
PushRequest,
PushVerdict,
ResolutionOutcome,
ShortcutDetected, ShortcutDetected,
TraceRecord,
Witness,
admit_fact_pack,
as_clean,
assert_no_shortcut, assert_no_shortcut,
check_budget,
check_reads,
consume_verdict,
derive_budget,
require_admission_evidence, require_admission_evidence,
reversed_pair_probe, reversed_pair_probe,
stamp,
validate_admission_manifest,
validate_report_schema,
verify_fact_pack,
verify_stamp,
) )
from openultrasast.core.probes import (
LegacyProbeResult as ProbeResult,
)
from openultrasast.core.provenance import ProvenanceMismatch, stamp, verify_stamp
from openultrasast.core.trace import Guard, Hop, TraceRecord, Witness
from openultrasast.core.verdict import BoundedSilence, Judgment, ProvenClaim, as_clean
__all__ = [ __all__ = [
"REPORT_SCHEMA_KEYS", "REPORT_SCHEMA_KEYS",
+7 -123
View File
@@ -7,7 +7,6 @@ import math
import re import re
from collections.abc import Sequence from collections.abc import Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import overload
from openultrasast.core import canonical from openultrasast.core import canonical
@@ -48,29 +47,6 @@ def _finite_number(value: object, field: str, *, positive: bool = False) -> floa
return number return number
@dataclass(frozen=True)
class Complexity:
"""Legacy measured change complexity."""
regions: int
calls: int
changed_bytes: int
@dataclass(frozen=True)
class Budget:
"""Legacy derived per-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) @dataclass(frozen=True)
class ChangeComplexity: class ChangeComplexity:
change_id: str change_id: str
@@ -302,73 +278,12 @@ def _compatible(value: ChangeComplexity, reference: ReferenceWorkload) -> bool:
) )
@overload
def derive_budget(samples: Sequence[tuple[Complexity, float]], *, workload_class: str, percentile: float, hardware: str) -> Budget: ...
@overload
def derive_budget( def derive_budget(
samples: Sequence[MeasuredSample], samples: Sequence[MeasuredSample],
workload_class: str, workload_class: str,
percentile: float, percentile: float,
reference_workload: ReferenceWorkload, reference_workload: ReferenceWorkload,
reference_hardware: ReferenceHardware, reference_hardware: ReferenceHardware,
) -> BudgetDerivation: ...
def derive_budget(
samples: Sequence[tuple[Complexity, float]] | Sequence[MeasuredSample],
workload_class: str | None = None,
percentile: float | None = None,
*v2_context: object,
hardware: str | None = None,
reference_workload: ReferenceWorkload | None = None,
reference_hardware: ReferenceHardware | None = None,
) -> Budget | BudgetDerivation:
if v2_context:
if len(v2_context) != 2 or reference_workload is not None or reference_hardware is not None:
raise TypeError("v2 positional context must be reference workload and hardware")
positional_workload, positional_hardware = v2_context
if not isinstance(positional_workload, ReferenceWorkload) or not isinstance(positional_hardware, ReferenceHardware):
raise TypeError("v2 positional context must be reference workload and hardware")
reference_workload, reference_hardware = positional_workload, positional_hardware
if workload_class is None or percentile is None:
raise TypeError("workload_class and percentile are required")
if reference_workload is not None or reference_hardware is not None:
if (
not isinstance(reference_workload, ReferenceWorkload)
or not isinstance(reference_hardware, ReferenceHardware)
or hardware is not None
):
raise ValueError("v2 derivation requires reference workload and hardware only")
return _derive_v2(samples, workload_class, percentile, reference_workload, reference_hardware)
if hardware is None:
raise ValueError("legacy derivation requires hardware")
return _derive_legacy(samples, workload_class, percentile, hardware)
def _derive_legacy(
samples: Sequence[tuple[Complexity, float]] | Sequence[MeasuredSample],
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 if isinstance(item, tuple))
if len(seconds) != len(samples):
raise TypeError("legacy samples must be (Complexity, seconds) tuples")
return Budget(workload_class, _quantile(seconds, percentile), len(samples), percentile, hardware)
def _derive_v2(
samples: Sequence[tuple[Complexity, float]] | Sequence[MeasuredSample],
workload_class: str,
percentile: float,
reference_workload: ReferenceWorkload,
reference_hardware: ReferenceHardware,
) -> BudgetDerivation: ) -> BudgetDerivation:
_text(workload_class, "workload_class") _text(workload_class, "workload_class")
value = _finite_number(percentile, "percentile", positive=True) value = _finite_number(percentile, "percentile", positive=True)
@@ -415,63 +330,32 @@ def _validate_measurement(change: ChangeComplexity, resolver: canonical.ContentR
raise ValueError("changed-byte measurement does not match complexity") raise ValueError("changed-byte measurement does not match complexity")
@overload
def check_budget(change_id: str, seconds: float, budget: Budget) -> None: ...
@overload
def check_budget( def check_budget(
change: ChangeComplexity, change: ChangeComplexity,
total_reply_ms: int, total_reply_ms: int,
budget: BudgetDerivation, budget: BudgetDerivation,
resolver: canonical.ContentResolver, resolver: canonical.ContentResolver,
/,
) -> None: ...
def check_budget(
change_id: str | ChangeComplexity | None = None,
seconds: float | int | None = None,
budget: Budget | BudgetDerivation | None = None,
resolver: canonical.ContentResolver | None = None,
**v2_names: object,
) -> None: ) -> None:
if v2_names: if not isinstance(change, ChangeComplexity) or not isinstance(budget, BudgetDerivation):
if set(v2_names) != {"change", "total_reply_ms"} or change_id is not None or seconds is not None:
raise TypeError("use either legacy or v2 budget parameter names")
candidate_change = v2_names["change"]
candidate_elapsed = v2_names["total_reply_ms"]
if not isinstance(candidate_change, ChangeComplexity) or not isinstance(candidate_elapsed, int):
raise TypeError("v2 budget parameter values have invalid types")
change_id, seconds = candidate_change, candidate_elapsed
if change_id is None or seconds is None or budget is None:
raise TypeError("change, elapsed time, and budget are required")
if isinstance(change_id, str) and isinstance(budget, Budget):
if seconds > budget.seconds:
raise BudgetGateFailure(f"budget:{change_id}:{seconds:g}s>{budget.seconds:g}s ({budget.describe()})")
return
if not isinstance(change_id, ChangeComplexity) or not isinstance(budget, BudgetDerivation) or resolver is None:
raise TypeError("v2 budget check requires complexity, derivation, and resolver") raise TypeError("v2 budget check requires complexity, derivation, and resolver")
_validate_complexity(change_id) _validate_complexity(change)
_validate_derivation(budget) _validate_derivation(budget)
_positive_int(seconds, "total_reply_ms") _positive_int(total_reply_ms, "total_reply_ms")
if change_id.workload_class != budget.workload_class or not _compatible(change_id, budget.reference_workload): if change.workload_class != budget.workload_class or not _compatible(change, budget.reference_workload):
raise ValueError("change complexity is incompatible with budget derivation") raise ValueError("change complexity is incompatible with budget derivation")
_validate_measurement(change_id, resolver) _validate_measurement(change, resolver)
if seconds > budget.budget_ms: if total_reply_ms > budget.budget_ms:
raise BudgetGateFailure( raise BudgetGateFailure(
f"budget:{change_id.change_id}:{seconds}ms>{budget.budget_ms}ms " f"budget:{change.change_id}:{total_reply_ms}ms>{budget.budget_ms}ms "
f"class={budget.workload_class} derivation={budget.derivation_digest}" f"class={budget.workload_class} derivation={budget.derivation_digest}"
) )
__all__ = [ __all__ = [
"Budget",
"BudgetDerivation", "BudgetDerivation",
"BudgetGateFailure", "BudgetGateFailure",
"CHANGED_BYTES_MEASUREMENT_SCHEMA", "CHANGED_BYTES_MEASUREMENT_SCHEMA",
"ChangeComplexity", "ChangeComplexity",
"Complexity",
"MeasuredSample", "MeasuredSample",
"ReferenceHardware", "ReferenceHardware",
"ReferenceWorkload", "ReferenceWorkload",
+3 -44
View File
@@ -9,8 +9,8 @@ from __future__ import annotations
import enum import enum
import re import re
from collections.abc import Mapping, Sequence from collections.abc import Sequence
from dataclasses import dataclass, field from dataclasses import dataclass
from openultrasast.core.canonical import JsonValue, content_digest, require_unique_identities from openultrasast.core.canonical import JsonValue, content_digest, require_unique_identities
from openultrasast.core.semantics import RelationStatus from openultrasast.core.semantics import RelationStatus
@@ -20,45 +20,7 @@ _DIGEST = re.compile(r"[0-9a-f]{64}")
class InstrumentFailure(ValueError): class InstrumentFailure(ValueError):
"""The run's input was not read; any result from it is invalid.""" """The v2 run did not read every tracked input."""
class ResolutionOutcome(enum.Enum):
"""Exactly one bucket per call (Req 3.1)."""
COMPLETE = "complete"
INCOMPLETE = "incomplete"
UNRESOLVED = "unresolved"
@dataclass(frozen=True)
class Coverage:
"""Coverage of one analyzed change."""
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:
"""Refuse coverage where calls lack exactly one resolution bucket."""
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})"
)
def check_reads(reads: Mapping[str, int]) -> None:
"""Fail the run when any tracked input was read as zero bytes (Req 3.2)."""
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 _require_text(value: object, field_name: str) -> None: def _require_text(value: object, field_name: str) -> None:
@@ -378,7 +340,6 @@ def diagnose_first_zero(funnel: CoverageFunnel, expected: Sequence[StageExpectat
__all__ = [ __all__ = [
"Coverage",
"CoverageFunnel", "CoverageFunnel",
"CoverageStage", "CoverageStage",
"InputManifest", "InputManifest",
@@ -386,10 +347,8 @@ __all__ = [
"InstrumentFailure", "InstrumentFailure",
"ReasonCount", "ReasonCount",
"RelationStatusCount", "RelationStatusCount",
"ResolutionOutcome",
"StageDiagnosis", "StageDiagnosis",
"StageExpectation", "StageExpectation",
"check_reads",
"derive_input_manifest_digest", "derive_input_manifest_digest",
"diagnose_first_zero", "diagnose_first_zero",
"validate_instrument", "validate_instrument",
-6
View File
@@ -10,7 +10,6 @@ from types import MappingProxyType
from openultrasast.core import _factpack_digest as digest_projection from openultrasast.core import _factpack_digest as digest_projection
from openultrasast.core import canonical from openultrasast.core import canonical
from openultrasast.core._legacy_factpack import FactPackError, admit_fact_pack, admit_pack_file, load_pack_file, verify_fact_pack
from openultrasast.core.custody import ( from openultrasast.core.custody import (
LineagePurpose, LineagePurpose,
PopulationCustodyPolicy, PopulationCustodyPolicy,
@@ -497,7 +496,6 @@ def validate_fact_pack(
__all__ = [ __all__ = [
"FACT_PACK_SCHEMA", "FACT_PACK_SCHEMA",
"FactPack", "FactPack",
"FactPackError",
"FactPackValidationError", "FactPackValidationError",
"IndependenceAttestation", "IndependenceAttestation",
"KnowledgeEvidence", "KnowledgeEvidence",
@@ -507,13 +505,9 @@ __all__ = [
"SemanticFact", "SemanticFact",
"StructuralRequirement", "StructuralRequirement",
"StructuralRequirementKind", "StructuralRequirementKind",
"admit_fact_pack",
"admit_pack_file",
"derive_fact_pack_content_digest", "derive_fact_pack_content_digest",
"derive_independence_attestation_digest", "derive_independence_attestation_digest",
"derive_review_evidence_digest", "derive_review_evidence_digest",
"derive_review_independence_policy_digest", "derive_review_independence_policy_digest",
"load_pack_file",
"validate_fact_pack", "validate_fact_pack",
"verify_fact_pack",
] ]
@@ -5,47 +5,88 @@ from openultrasast.core.legacy_v1.contracts import (
AdmissionManifest, AdmissionManifest,
AdmissionManifestError, AdmissionManifestError,
BoundedSilence, BoundedSilence,
Budget,
BudgetGateFailure,
Complexity,
Coverage,
FactPackError, FactPackError,
FittedComponentBlocked,
Guard, Guard,
Hop, Hop,
InstrumentFailure,
Judgment, Judgment,
LegacyFactPack, LegacyFactPack,
LegacyProbeResult,
ProvenanceMismatch,
ProvenClaim, ProvenClaim,
PushRequest, PushRequest,
PushVerdict, PushVerdict,
ResolutionOutcome,
ShortcutDetected,
TraceRecord, TraceRecord,
Witness, Witness,
admit_fact_pack, admit_fact_pack,
admit_pack_file, admit_pack_file,
as_clean, as_clean,
assert_no_shortcut,
check_budget,
check_reads,
consume_verdict, consume_verdict,
derive_budget,
load_pack_file, load_pack_file,
require_admission_evidence,
reversed_pair_probe,
stamp,
validate_admission_manifest, validate_admission_manifest,
validate_report_schema, validate_report_schema,
verify_fact_pack, verify_fact_pack,
verify_stamp,
) )
FactPack = LegacyFactPack
ProbeResult = LegacyProbeResult
__all__ = [ __all__ = [
"REPORT_SCHEMA_KEYS", "REPORT_SCHEMA_KEYS",
"AdmissionManifest", "AdmissionManifest",
"AdmissionManifestError", "AdmissionManifestError",
"BoundedSilence", "BoundedSilence",
"Budget",
"BudgetGateFailure",
"Complexity",
"Coverage",
"FactPack",
"FactPackError", "FactPackError",
"FittedComponentBlocked",
"Guard", "Guard",
"Hop", "Hop",
"Judgment", "Judgment",
"LegacyFactPack", "LegacyFactPack",
"LegacyProbeResult",
"InstrumentFailure",
"ProvenClaim", "ProvenClaim",
"PushRequest", "PushRequest",
"PushVerdict", "PushVerdict",
"ProvenanceMismatch",
"ProbeResult",
"ResolutionOutcome",
"ShortcutDetected",
"TraceRecord", "TraceRecord",
"Witness", "Witness",
"admit_fact_pack", "admit_fact_pack",
"admit_pack_file", "admit_pack_file",
"as_clean", "as_clean",
"assert_no_shortcut",
"check_budget",
"check_reads",
"consume_verdict", "consume_verdict",
"derive_budget",
"load_pack_file", "load_pack_file",
"require_admission_evidence",
"reversed_pair_probe",
"stamp",
"validate_admission_manifest", "validate_admission_manifest",
"validate_report_schema", "validate_report_schema",
"verify_fact_pack", "verify_fact_pack",
"verify_stamp",
] ]
+159 -25
View File
@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
import enum import enum
from collections.abc import Mapping, Sequence from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -17,6 +17,32 @@ class FactPackError(ValueError):
"""A legacy fact pack is unreviewed or inconsistent.""" """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): class Judgment(enum.Enum):
UNGUARDED = "unguarded" UNGUARDED = "unguarded"
DISCHARGED = "discharged" DISCHARGED = "discharged"
@@ -121,6 +147,52 @@ class LegacyFactPack:
fact_set_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( REPORT_SCHEMA_KEYS = frozenset(
{ {
"manifest", "manifest",
@@ -279,27 +351,89 @@ def admit_pack_file(path: str | Path, *, review_digest: str) -> LegacyFactPack:
return pack return pack
__all__ = [ _STAMP_FIELDS = (
"REPORT_SCHEMA_KEYS", "analyzer_revision",
"AdmissionManifest", "fact_set_digest",
"AdmissionManifestError", "acquisition_record_digest",
"BoundedSilence", "configuration_digest",
"FactPackError", "population_digest",
"Guard", "input_digest",
"Hop", )
"Judgment",
"LegacyFactPack",
"ProvenClaim", def check_reads(reads: Mapping[str, int]) -> None:
"PushRequest", zero = sorted(path for path, size in reads.items() if size <= 0)
"PushVerdict", if zero:
"TraceRecord", raise InstrumentFailure(f"zero-byte read of tracked input: {', '.join(zero)}")
"Witness",
"admit_fact_pack",
"admit_pack_file", def _quantile(values: list[float], percentile: float) -> float:
"as_clean", position = (len(values) - 1) * percentile
"consume_verdict", lower = int(position)
"load_pack_file", upper = min(lower + 1, len(values) - 1)
"validate_admission_manifest", weight = position - lower
"validate_report_schema", return values[lower] * (1 - weight) + values[upper] * weight
"verify_fact_pack",
]
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")
+6 -1
View File
@@ -31,6 +31,11 @@ def text_digest(value: str) -> str:
return hashlib.sha256(value.encode()).hexdigest() 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]: def read_json(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text(encoding="utf-8")) value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict): if not isinstance(value, dict):
@@ -38,4 +43,4 @@ def read_json(path: Path) -> dict[str, Any]:
return value return value
__all__ = ["fact_pack_digest", "read_json", "text_digest", "trace_bytes", "trace_record"] __all__ = ["fact_pack_digest", "input_digest", "read_json", "text_digest", "trace_bytes", "trace_record"]
-55
View File
@@ -4,9 +4,7 @@ from __future__ import annotations
import math import math
import re import re
from collections.abc import Callable, Mapping
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from typing import Any
from openultrasast.core import canonical from openultrasast.core import canonical
from openultrasast.core.evidence import EvidenceRecord from openultrasast.core.evidence import EvidenceRecord
@@ -21,23 +19,6 @@ class ProbeValidationError(ValueError):
"""A probe input or claimed result cannot be reproduced.""" """A probe input or claimed result cannot be reproduced."""
class ShortcutDetected(ValueError):
"""A legacy component separates a fix from its reversed surface."""
class FittedComponentBlocked(ValueError):
"""A legacy fitted component lacks its required release evidence."""
@dataclass(frozen=True)
class LegacyProbeResult:
"""Frozen v1 shortcut result retained until the migration cutover."""
separated: bool
fix_score: float
reversed_score: float
def _digest(value: object, field: str) -> None: def _digest(value: object, field: str) -> None:
if not isinstance(value, str) or _DIGEST.fullmatch(value) is None: if not isinstance(value, str) or _DIGEST.fullmatch(value) is None:
raise ProbeValidationError(f"{field} must be a full lowercase SHA-256 digest") raise ProbeValidationError(f"{field} must be a full lowercase SHA-256 digest")
@@ -379,39 +360,7 @@ def validate_probe_result(
raise ProbeValidationError("claimed probe result does not match canonical evaluation") raise ProbeValidationError("claimed probe result does not match canonical evaluation")
def reversed_pair_probe(
score_fn: Callable[[Mapping[str, Any]], float],
fix_change: Mapping[str, Any],
reversed_change: Mapping[str, Any],
) -> LegacyProbeResult:
"""Run the frozen v1 shape probe without granting it v2 authority."""
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:
"""Preserve the v1 exception behavior during migration."""
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:
"""Preserve the v1 fitted-component release guard during migration."""
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")
__all__ = [ __all__ = [
"FittedComponentBlocked",
"LegacyProbeResult",
"ProbeEvaluationInput", "ProbeEvaluationInput",
"ProbeObservation", "ProbeObservation",
"ProbeResult", "ProbeResult",
@@ -420,12 +369,8 @@ __all__ = [
"RoleProjection", "RoleProjection",
"SemanticEquivalenceReview", "SemanticEquivalenceReview",
"SemanticProjection", "SemanticProjection",
"ShortcutDetected",
"assert_no_shortcut",
"derive_equivalence_subject_digest", "derive_equivalence_subject_digest",
"evaluate_probe", "evaluate_probe",
"project_semantics", "project_semantics",
"require_admission_evidence",
"reversed_pair_probe",
"validate_probe_result", "validate_probe_result",
] ]
-52
View File
@@ -2,10 +2,7 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import json
import re import re
from collections.abc import Mapping
from dataclasses import dataclass, fields from dataclasses import dataclass, fields
from enum import Enum from enum import Enum
@@ -50,52 +47,6 @@ PROVENANCE_POLICY_SCHEMA = "openultrasast.detection-core.provenance-policy.v2"
_DIGEST = re.compile(r"[0-9a-f]{64}") _DIGEST = re.compile(r"[0-9a-f]{64}")
class ProvenanceMismatch(ValueError):
"""A legacy recorded digest does not match its re-derived identity."""
_LEGACY_DIGEST_FIELDS = (
"analyzer_revision",
"fact_set_digest",
"acquisition_record_digest",
"configuration_digest",
"population_digest",
"input_digest",
)
def _input_digest(change_id: str, population_digest: str) -> str:
payload = json.dumps([population_digest, change_id], sort_keys=True, separators=(",", ":"))
return hashlib.sha256(payload.encode()).hexdigest()[:12]
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]:
"""Preserve the v1 provenance stamp during migration."""
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": _input_digest(change_id, population_digest),
}
def verify_stamp(recorded: Mapping[str, object], rederived: Mapping[str, object]) -> None:
"""Preserve v1 refusal when a legacy digest cannot be re-derived."""
for field_name in _LEGACY_DIGEST_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}")
class ProvenanceValidationError(ValueError): class ProvenanceValidationError(ValueError):
"""A provenance identity cannot be reproduced from its named inputs.""" """A provenance identity cannot be reproduced from its named inputs."""
@@ -357,7 +308,6 @@ __all__ = [
"NamedInputs", "NamedInputs",
"PROVENANCE_POLICY_SCHEMA", "PROVENANCE_POLICY_SCHEMA",
"ProvenanceInputRole", "ProvenanceInputRole",
"ProvenanceMismatch",
"ProvenanceStamp", "ProvenanceStamp",
"ProvenanceTrustPolicy", "ProvenanceTrustPolicy",
"ProvenanceValidationError", "ProvenanceValidationError",
@@ -365,8 +315,6 @@ __all__ = [
"derive_lineage_decision_digest", "derive_lineage_decision_digest",
"derive_lineage_manifest_digest", "derive_lineage_manifest_digest",
"derive_trust_policy_digest", "derive_trust_policy_digest",
"stamp",
"validate_provenance", "validate_provenance",
"validate_lineage", "validate_lineage",
"verify_stamp",
] ]
+1 -1
View File
@@ -9,7 +9,7 @@ from dataclasses import asdict, dataclass, field, replace
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from openultrasast.core.coverage import Coverage as CoreCoverage from openultrasast.core.legacy_v1 import Coverage as CoreCoverage
from .. import mapping, source from .. import mapping, source
from ..preprocess import FileTarget, count_loc, detect_language, detect_tags from ..preprocess import FileTarget, count_loc, detect_language, detect_tags
+129 -2
View File
@@ -74,6 +74,85 @@ class LegacyMigrationInventory:
entries: tuple[LegacyInventoryEntry, ...] entries: tuple[LegacyInventoryEntry, ...]
_APPROVED_MIXED_TRANSFERS: dict[
str,
tuple[tuple[str, str, str, str, tuple[str, ...], str], str, tuple[str, ...]],
] = {
"9eb4cba855044fe025daa7a523458c4fba0b12758ca604c894849f824f55ccb4": (
(
"src/openultrasast/core/factpack.py",
"<module>",
"reader",
"v1-contract",
("import_from:openultrasast.core._legacy_factpack:FactPackError",),
"core",
),
"openultrasast.core.legacy_v1.contracts",
("FactPackError",),
),
"1b90ea5474dff89121dc1317a86892bb0d68f2c64117923c5979cf4bb0cd8b76": (
(
"src/openultrasast/core/factpack.py",
"<module>",
"reader",
"v1-contract",
("import_from:openultrasast.core._legacy_factpack:admit_fact_pack",),
"core",
),
"openultrasast.core.legacy_v1.contracts",
("admit_fact_pack",),
),
"005f1fee3a1a659a65a45d8134bc71f061e314bdc28ba95516e870997c2648cd": (
(
"src/openultrasast/core/factpack.py",
"<module>",
"reader",
"v1-contract",
("import_from:openultrasast.core._legacy_factpack:admit_pack_file",),
"core",
),
"openultrasast.core.legacy_v1.contracts",
("admit_pack_file",),
),
"4c846ba1d97e5a5b4bbf8f1e99e10c40da10906ca93d68596cb987738438582a": (
(
"src/openultrasast/core/factpack.py",
"<module>",
"reader",
"v1-contract",
("import_from:openultrasast.core._legacy_factpack:load_pack_file",),
"core",
),
"openultrasast.core.legacy_v1.contracts",
("load_pack_file",),
),
"23e6ccf25c185ca231d73ab02570b14a9cddae27f7506e95d517e241bb693ae4": (
(
"src/openultrasast/core/factpack.py",
"<module>",
"reader",
"v1-contract",
("import_from:openultrasast.core._legacy_factpack:verify_fact_pack",),
"core",
),
"openultrasast.core.legacy_v1.contracts",
("verify_fact_pack",),
),
"c8aaf430009a592a8ed4361f38023d5faa12a6da6186fc0dcea9c7c522da85ad": (
(
"src/openultrasast/core/probes.py",
"reversed_pair_probe",
"writer",
"v1-probe",
("definition:reversed_pair_probe", "name:LegacyProbeResult"),
"core",
),
"openultrasast.core.legacy_v1.contracts",
("reversed_pair_probe", "LegacyProbeResult"),
),
}
def _object(value: object, fields: frozenset[str], name: str) -> dict[str, object]: def _object(value: object, fields: frozenset[str], name: str) -> dict[str, object]:
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise LegacyInventoryError(f"{name} must be an object") raise LegacyInventoryError(f"{name} must be an object")
@@ -311,6 +390,53 @@ def _explicit_legacy_shim(tree: ast.Module, symbol: str) -> bool:
) )
def _stable_entry_fields(entry: LegacyInventoryEntry) -> tuple[str, str, str, str, tuple[str, ...], str]:
return (
entry.path,
entry.symbol,
entry.dependency_kind.value,
entry.artifact_family,
entry.capability_anchors,
entry.owner_boundary,
)
def _read_owner_module(path: Path, description: str) -> ast.Module:
try:
content = path.read_bytes()
except OSError as exc:
raise LegacyInventoryError(f"{description} is not readable") from exc
if not content:
raise LegacyInventoryError(f"{description} must contain nonzero bytes")
try:
return ast.parse(content, filename=path.as_posix())
except (SyntaxError, UnicodeDecodeError) as exc:
raise LegacyInventoryError(f"{description} is not parseable") from exc
def _moved_mixed_v1_owner(entry: LegacyInventoryEntry, *, root: Path) -> bool:
transfer = _APPROVED_MIXED_TRANSFERS.get(entry.entry_id)
if transfer is None or entry.entry_id != entry_identity(entry):
return False
expected_fields, destination_module, destination_symbols = transfer
if _stable_entry_fields(entry) != expected_fields:
return False
facade_path = root / "src/openultrasast/core/legacy_v1/__init__.py"
facade = _read_owner_module(facade_path, "compatibility facade")
facade_imports = {
(node.module, alias.name, alias.asname or alias.name)
for node in facade.body
if isinstance(node, ast.ImportFrom)
for alias in node.names
}
if not all((destination_module, symbol, symbol) in facade_imports for symbol in destination_symbols):
return False
destination_path = root / "src" / Path(*destination_module.split("."))
destination = _read_owner_module(destination_path.with_suffix(".py"), "compatibility destination")
owned = {node.name for node in destination.body if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef))}
return set(destination_symbols) <= owned
def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Path) -> None: def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Path) -> None:
"""Prove every registered migration target is readable, nonzero, and present.""" """Prove every registered migration target is readable, nonzero, and present."""
for entry in inventory.entries: for entry in inventory.entries:
@@ -326,10 +452,11 @@ 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)
if entry.symbol not in _defined_symbols(tree) and not is_shim: moved_owner = _moved_mixed_v1_owner(entry, root=root)
if entry.symbol not in _defined_symbols(tree) and not is_shim and not moved_owner:
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)
if missing and not is_shim: if missing and not is_shim and not moved_owner:
raise LegacyInventoryError(f"inventory capability is absent: {entry.path}:{missing}") raise LegacyInventoryError(f"inventory capability is absent: {entry.path}:{missing}")
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from openultrasast.core.budget import ( # noqa: E402 from openultrasast.core.legacy_v1 import ( # noqa: E402
Budget, Budget,
BudgetGateFailure, BudgetGateFailure,
Complexity, Complexity,
+8 -4
View File
@@ -12,6 +12,10 @@ from detection_core_fixtures import InMemoryContentResolver, stored_artifact
from openultrasast.core import budget as budget_v2 from openultrasast.core import budget as budget_v2
from openultrasast.core import canonical, evidence, provenance, semantics from openultrasast.core import canonical, evidence, provenance, semantics
from openultrasast.core.legacy_v1 import Budget as LegacyBudget
from openultrasast.core.legacy_v1 import Complexity as LegacyComplexity
from openultrasast.core.legacy_v1 import check_budget as legacy_check_budget
from openultrasast.core.legacy_v1 import derive_budget as legacy_derive_budget
def _digest(value: str) -> str: def _digest(value: str) -> str:
@@ -342,7 +346,7 @@ def test_scope_revalidates_post_construction_tampering() -> None:
def test_legacy_budget_api_remains_available() -> None: def test_legacy_budget_api_remains_available() -> None:
samples = ((budget_v2.Complexity(1, 1, 10), 1.0), (budget_v2.Complexity(2, 2, 20), 2.0)) samples = ((LegacyComplexity(1, 1, 10), 1.0), (LegacyComplexity(2, 2, 20), 2.0))
legacy = budget_v2.derive_budget(samples, workload_class="legacy", percentile=0.5, hardware="runner") legacy = legacy_derive_budget(samples, workload_class="legacy", percentile=0.5, hardware="runner")
assert isinstance(legacy, budget_v2.Budget) assert isinstance(legacy, LegacyBudget)
budget_v2.check_budget(change_id="change", seconds=1.0, budget=legacy) legacy_check_budget(change_id="change", seconds=1.0, budget=legacy)
+16 -4
View File
@@ -5,10 +5,22 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from openultrasast.core.coverage import Coverage, InstrumentFailure, check_reads # noqa: E402 from openultrasast.core.legacy_v1 import ( # noqa: E402
from openultrasast.core.provenance import ProvenanceMismatch, stamp, verify_stamp # noqa: E402 BoundedSilence,
from openultrasast.core.trace import Guard, Hop, TraceRecord, Witness # noqa: E402 Coverage,
from openultrasast.core.verdict import BoundedSilence, Judgment, ProvenClaim, as_clean # noqa: E402 Guard,
Hop,
InstrumentFailure,
Judgment,
ProvenanceMismatch,
ProvenClaim,
TraceRecord,
Witness,
as_clean,
check_reads,
stamp,
verify_stamp,
)
def _trace() -> TraceRecord: def _trace() -> TraceRecord:
+14
View File
@@ -17,6 +17,7 @@ from openultrasast.core.coverage import (
diagnose_first_zero, diagnose_first_zero,
validate_instrument, validate_instrument,
) )
from openultrasast.core.legacy_v1 import InstrumentFailure as LegacyInstrumentFailure
from openultrasast.core.semantics import RelationStatus from openultrasast.core.semantics import RelationStatus
DIGEST_A = "a" * 64 DIGEST_A = "a" * 64
@@ -74,6 +75,15 @@ def test_input_manifest_and_receipts_validate_exactly() -> None:
validate_instrument(_manifest(), _receipts()) validate_instrument(_manifest(), _receipts())
def test_v2_instrument_failure_is_distinct_from_legacy_and_owns_input_refusal() -> None:
from openultrasast.core.coverage import InstrumentFailure
assert InstrumentFailure is not LegacyInstrumentFailure
receipt = InputReadReceipt("src/app.py", "src/app.py", False, 12, DIGEST_A)
with pytest.raises(InstrumentFailure, match="not opened"):
validate_instrument(_manifest(), (receipt, _receipts()[1]))
def test_zero_named_counts_are_allowed_for_measurements_but_not_unresolved_coverage() -> None: def test_zero_named_counts_are_allowed_for_measurements_but_not_unresolved_coverage() -> None:
with pytest.raises(ValueError, match="unresolved reason count must be positive"): with pytest.raises(ValueError, match="unresolved reason count must be positive"):
_funnel( _funnel(
@@ -84,6 +94,10 @@ def test_zero_named_counts_are_allowed_for_measurements_but_not_unresolved_cover
) )
def test_legacy_instrument_failure_is_available_only_through_compatibility_boundary() -> None:
assert LegacyInstrumentFailure.__module__ == "openultrasast.core.legacy_v1.contracts"
@pytest.mark.parametrize( @pytest.mark.parametrize(
("opened", "byte_count", "digest", "match"), ("opened", "byte_count", "digest", "match"),
[ [
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from openultrasast.core.consumer import PushRequest, consume_verdict # noqa: E402 from openultrasast.core.consumer import PushRequest, consume_verdict # noqa: E402
from openultrasast.core.factpack import FactPackError, admit_fact_pack, verify_fact_pack # noqa: E402 from openultrasast.core.legacy_v1 import FactPackError, admit_fact_pack, verify_fact_pack # noqa: E402
from openultrasast.core.trace import TraceRecord # noqa: E402 from openultrasast.core.trace import TraceRecord # noqa: E402
from openultrasast.core.verdict import BoundedSilence, Judgment, ProvenClaim # noqa: E402 from openultrasast.core.verdict import BoundedSilence, Judgment, ProvenClaim # noqa: E402
+1 -1
View File
@@ -9,7 +9,7 @@ import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from openultrasast.core.factpack import FactPackError, admit_pack_file, verify_fact_pack # noqa: E402 from openultrasast.core.legacy_v1 import FactPackError, admit_pack_file, verify_fact_pack # noqa: E402
from openultrasast.mapping.installed_fact_authority import ( # noqa: E402 from openultrasast.mapping.installed_fact_authority import ( # noqa: E402
InstalledAuthorityWireError, InstalledAuthorityWireError,
decode_fact_pack_bytes, decode_fact_pack_bytes,
+67
View File
@@ -14,6 +14,20 @@ PURE_COMPATIBILITY_MODULES = (
"verdict.py", "verdict.py",
"_legacy_factpack.py", "_legacy_factpack.py",
) )
MIXED_LEGACY_NAMES = {
"budget": {"Budget", "Complexity"},
"coverage": {"Coverage", "ResolutionOutcome", "check_reads"},
"factpack": {"FactPackError", "admit_fact_pack", "admit_pack_file", "load_pack_file", "verify_fact_pack"},
"probes": {
"FittedComponentBlocked",
"LegacyProbeResult",
"ShortcutDetected",
"assert_no_shortcut",
"require_admission_evidence",
"reversed_pair_probe",
},
"provenance": {"ProvenanceMismatch", "stamp", "verify_stamp"},
}
def _trace() -> object: def _trace() -> object:
@@ -40,14 +54,24 @@ def test_legacy_contract_values_have_one_compatibility_owner() -> None:
"AdmissionManifest", "AdmissionManifest",
"AdmissionManifestError", "AdmissionManifestError",
"BoundedSilence", "BoundedSilence",
"Budget",
"BudgetGateFailure",
"Complexity",
"Coverage",
"FactPackError", "FactPackError",
"FittedComponentBlocked",
"Guard", "Guard",
"Hop", "Hop",
"InstrumentFailure",
"Judgment", "Judgment",
"LegacyFactPack", "LegacyFactPack",
"LegacyProbeResult",
"ProvenClaim", "ProvenClaim",
"ProvenanceMismatch",
"PushRequest", "PushRequest",
"PushVerdict", "PushVerdict",
"ResolutionOutcome",
"ShortcutDetected",
"TraceRecord", "TraceRecord",
"Witness", "Witness",
} }
@@ -61,6 +85,7 @@ def test_pure_compatibility_modules_define_no_contract_or_serializer() -> None:
assert content, f"ownership instrument must read nonzero bytes from {name}" assert content, f"ownership instrument must read nonzero bytes from {name}"
tree = ast.parse(content, filename=path.as_posix()) tree = ast.parse(content, filename=path.as_posix())
assert not [node.name for node in tree.body if isinstance(node, ast.ClassDef)] assert not [node.name for node in tree.body if isinstance(node, ast.ClassDef)]
assert not [node.name for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))]
assert not any(isinstance(node, (ast.Import, ast.ImportFrom)) and "json" in ast.unparse(node) for node in tree.body) 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) assert any(isinstance(node, ast.ImportFrom) and node.module == "openultrasast.core.legacy_v1.contracts" for node in tree.body)
@@ -69,6 +94,7 @@ def test_writer_is_the_only_compatibility_json_serializer() -> None:
namespace = REPO / "src/openultrasast/core/legacy_v1" namespace = REPO / "src/openultrasast/core/legacy_v1"
paths = sorted(namespace.glob("*.py")) paths = sorted(namespace.glob("*.py"))
assert paths and all(path.read_bytes() for path in paths) assert paths and all(path.read_bytes() for path in paths)
assert {path.name for path in paths} == {"__init__.py", "contracts.py", "writer.py"}
json_importers = [] json_importers = []
for path in paths: for path in paths:
tree = ast.parse(path.read_bytes(), filename=path.as_posix()) tree = ast.parse(path.read_bytes(), filename=path.as_posix())
@@ -109,3 +135,44 @@ def test_historical_decoder_stays_independent_from_compatibility_namespace() ->
tree = ast.parse(content, filename=path.as_posix()) tree = ast.parse(content, filename=path.as_posix())
imports = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom)} imports = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom)}
assert "openultrasast.core.legacy_v1" not in imports assert "openultrasast.core.legacy_v1" not in imports
def test_top_level_v1_api_imports_only_from_closed_namespace() -> None:
import openultrasast.core as public_core
from openultrasast.core import legacy_v1
path = REPO / "src/openultrasast/core/__init__.py"
content = path.read_bytes()
assert content
tree = ast.parse(content, filename=path.as_posix())
legacy_imports = {
node.module
for node in tree.body
if isinstance(node, ast.ImportFrom) and any((alias.asname or alias.name) in public_core.__all__ for alias in node.names)
}
assert legacy_imports == {"openultrasast.core.legacy_v1"}
assert all(getattr(public_core, name) is getattr(legacy_v1, name) for name in public_core.__all__)
def test_mixed_v2_modules_expose_no_legacy_compatibility_names() -> None:
import importlib
for module_name, legacy_names in MIXED_LEGACY_NAMES.items():
module = importlib.import_module(f"openultrasast.core.{module_name}")
assert legacy_names.isdisjoint(vars(module)), f"{module_name} still exposes {legacy_names & vars(module).keys()}"
def test_closed_namespace_is_the_only_owner_of_moved_mixed_names() -> None:
from openultrasast.core import legacy_v1
expected = set().union(*MIXED_LEGACY_NAMES.values())
assert all(hasattr(legacy_v1, name) for name in expected)
operation_names = {
"assert_no_shortcut",
"check_reads",
"require_admission_evidence",
"reversed_pair_probe",
"stamp",
"verify_stamp",
}
assert {getattr(legacy_v1, name).__module__ for name in operation_names} == {"openultrasast.core.legacy_v1.contracts"}
+66 -9
View File
@@ -21,6 +21,15 @@ from core_migration_inventory import (
) )
INVENTORY = REPO / "tests/fixtures/detection_core/v1/migration_inventory.json" INVENTORY = REPO / "tests/fixtures/detection_core/v1/migration_inventory.json"
MIXED_V2_MODULES = frozenset(
{
"src/openultrasast/core/budget.py",
"src/openultrasast/core/coverage.py",
"src/openultrasast/core/factpack.py",
"src/openultrasast/core/probes.py",
"src/openultrasast/core/provenance.py",
}
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -51,22 +60,18 @@ def test_reader_audit_covers_direct_legacy_submodule_bindings(
def test_reader_audit_covers_real_direct_legacy_submodule_reads() -> None: def test_reader_audit_covers_real_direct_legacy_submodule_reads() -> None:
expected = { expected = {
("tests/test_core_budget_v2.py", "module_attr:openultrasast.core.budget:Budget"),
("tests/test_core_budget_v2.py", "module_attr:openultrasast.core.budget:Complexity"),
("tests/test_core_probes.py", "module_attr:openultrasast.core.probes:FittedComponentBlocked"),
("tests/test_core_probes.py", "module_attr:openultrasast.core.probes:LegacyProbeResult"),
("tests/test_core_probes.py", "module_attr:openultrasast.core.probes:ShortcutDetected"),
("tests/test_core_probes.py", "module_attr:openultrasast.core.probes:assert_no_shortcut"),
("tests/test_core_probes.py", "module_attr:openultrasast.core.probes:require_admission_evidence"),
("tests/test_core_probes.py", "module_attr:openultrasast.core.probes:reversed_pair_probe"),
("tests/test_core_contract.py", "module_attr:openultrasast.core.verdict:__all__"), ("tests/test_core_contract.py", "module_attr:openultrasast.core.verdict:__all__"),
} }
paths = tuple(REPO / path for path in sorted({path for path, _ in expected})) paths = tuple(REPO / path for path in ("tests/test_core_budget_v2.py", "tests/test_core_contract.py", "tests/test_core_probes.py"))
report = audit_legacy_readers(paths) report = audit_legacy_readers(paths)
assert report.files_read == 3 and report.bytes_read > 0 assert report.files_read == 3 and report.bytes_read > 0
assert expected <= set(report.capabilities) assert expected <= set(report.capabilities)
assert not any(
capability.startswith(("module_attr:openultrasast.core.budget:", "module_attr:openultrasast.core.probes:"))
for _path, capability in report.capabilities
)
def test_frozen_inventory_resolves_nonzero_sources_and_targets() -> None: def test_frozen_inventory_resolves_nonzero_sources_and_targets() -> None:
@@ -185,6 +190,9 @@ def test_independent_reader_audit_matches_split_inventory_capabilities() -> None
for path, _anchor in open_registered - current: for path, _anchor in open_registered - current:
source = (REPO / path).read_bytes() source = (REPO / path).read_bytes()
assert source, f"migration shim instrument must read nonzero bytes from {path}" assert source, f"migration shim instrument must read nonzero bytes from {path}"
if path in MIXED_V2_MODULES:
assert b"openultrasast.core.legacy_v1" not in source
continue
assert b"openultrasast.core.legacy_v1" in source, f"open reader disappeared without an explicit compatibility shim: {path}" assert b"openultrasast.core.legacy_v1" in source, f"open reader disappeared without an explicit compatibility shim: {path}"
@@ -293,6 +301,55 @@ def test_inventory_refuses_unreadable_zero_and_unknown_targets(tmp_path: Path) -
validate_inventory_sources(replace(frozen, entries=(unknown,)), root=tmp_path) validate_inventory_sources(replace(frozen, entries=(unknown,)), root=tmp_path)
@pytest.mark.parametrize("mutation", ("symbol", "anchor"))
def test_mixed_owner_transfer_refuses_rekeyed_inventory_entries(mutation: str) -> None:
frozen = load_migration_inventory(INVENTORY)
original = next(
entry for entry in frozen.entries if entry.entry_id == "9eb4cba855044fe025daa7a523458c4fba0b12758ca604c894849f824f55ccb4"
)
if mutation == "symbol":
changed = replace(original, symbol="InstrumentFailure", capability_anchors=("definition:InstrumentFailure",))
else:
changed = replace(
original,
capability_anchors=("import_from:openultrasast.core._legacy_factpack:InstrumentFailure",),
)
changed = replace(changed, entry_id=entry_identity(changed))
with pytest.raises(LegacyInventoryError, match="target|capability"):
validate_inventory_sources(replace(frozen, entries=(changed,)), root=REPO)
@pytest.mark.parametrize(
("facade_source", "owner_source"),
(
(
"from openultrasast.core.legacy_v1.writer import FactPackError\n",
"class FactPackError(ValueError):\n pass\n",
),
(
"from openultrasast.core.legacy_v1.contracts import FactPackError\n",
"class AnotherContract:\n pass\n",
),
),
)
def test_mixed_owner_transfer_refuses_wrong_or_missing_destination(tmp_path: Path, facade_source: str, owner_source: str) -> None:
frozen = load_migration_inventory(INVENTORY)
original = next(
entry for entry in frozen.entries if entry.entry_id == "9eb4cba855044fe025daa7a523458c4fba0b12758ca604c894849f824f55ccb4"
)
mixed = tmp_path / original.path
mixed.parent.mkdir(parents=True)
mixed.write_text("pass\n", encoding="utf-8")
namespace = tmp_path / "src/openultrasast/core/legacy_v1"
namespace.mkdir(parents=True)
(namespace / "__init__.py").write_text(facade_source, encoding="utf-8")
(namespace / "contracts.py").write_text(owner_source, encoding="utf-8")
with pytest.raises(LegacyInventoryError, match="capability"):
validate_inventory_sources(replace(frozen, entries=(original,)), root=tmp_path)
def test_inventory_refuses_changed_artifact_bytes(tmp_path: Path) -> None: def test_inventory_refuses_changed_artifact_bytes(tmp_path: Path) -> None:
frozen = load_migration_inventory(INVENTORY) frozen = load_migration_inventory(INVENTORY)
artifact = frozen.historical_artifacts[0] artifact = frozen.historical_artifacts[0]
+15 -7
View File
@@ -28,6 +28,14 @@ from openultrasast.core.family import (
derive_security_object_id, derive_security_object_id,
family_contract_digest, family_contract_digest,
) )
from openultrasast.core.legacy_v1 import (
FittedComponentBlocked,
LegacyProbeResult,
ShortcutDetected,
assert_no_shortcut,
require_admission_evidence,
reversed_pair_probe,
)
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -393,15 +401,15 @@ def test_legacy_reversed_pair_probe_remains_compatible() -> None:
def shape_score(change: Mapping[str, Any]) -> float: def shape_score(change: Mapping[str, Any]) -> float:
return float(change["lines"]) return float(change["lines"])
result = probes.reversed_pair_probe(shape_score, forward, reversed_change) result = reversed_pair_probe(shape_score, forward, reversed_change)
assert result.separated is True assert result.separated is True
with pytest.raises(probes.ShortcutDetected): with pytest.raises(ShortcutDetected):
probes.assert_no_shortcut(shape_score, forward, reversed_change) assert_no_shortcut(shape_score, forward, reversed_change)
assert public_core.ProbeResult is probes.LegacyProbeResult assert public_core.ProbeResult is LegacyProbeResult
def test_legacy_admission_guard_remains_compatible() -> None: def test_legacy_admission_guard_remains_compatible() -> None:
probes.require_admission_evidence(holdout_passed=True, admission_decision="GO") require_admission_evidence(holdout_passed=True, admission_decision="GO")
with pytest.raises(probes.FittedComponentBlocked): with pytest.raises(FittedComponentBlocked):
probes.require_admission_evidence(holdout_passed=False, admission_decision="GO") require_admission_evidence(holdout_passed=False, admission_decision="GO")