mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(detection-core): complete single-writer v2 cutover
This commit is contained in:
@@ -7,6 +7,8 @@ import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from core_migration_scope import CUTOVER_DELETIONS
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
_EXCLUDED_PREFIXES = ("benchmarks/fixtures/", "benchmarks/pairs/", "tests/fixtures/")
|
||||
_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.consumer": None,
|
||||
"openultrasast.core.consumer": frozenset({"consume_verdict"}),
|
||||
"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.probes": frozenset(
|
||||
@@ -62,7 +64,7 @@ _LEGACY_IMPORTS: dict[str, frozenset[str] | None] = {
|
||||
),
|
||||
"openultrasast.core.provenance": frozenset({"ProvenanceMismatch", "stamp", "verify_stamp"}),
|
||||
"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()
|
||||
|
||||
@@ -80,10 +82,22 @@ class ReaderAuditReport:
|
||||
|
||||
def tracked_python() -> tuple[Path, ...]:
|
||||
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(
|
||||
REPO / relative
|
||||
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_SHIMS,
|
||||
FROZEN_TOP_LEVEL_V1,
|
||||
REPLACED_SHIM_MEMBERS,
|
||||
REPO,
|
||||
MigrationScopeError,
|
||||
authoritative_cutover_scope,
|
||||
@@ -29,9 +30,7 @@ from core_migration_scope import (
|
||||
relative_path,
|
||||
scope_identity,
|
||||
)
|
||||
from core_migration_scope import (
|
||||
static_top_level_v1 as _static_top_level_v1,
|
||||
)
|
||||
from core_migration_scope import static_top_level_exports as _static_top_level_exports
|
||||
|
||||
|
||||
class LegacyBoundaryError(ValueError):
|
||||
@@ -71,9 +70,9 @@ class _Bindings:
|
||||
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:
|
||||
return _static_top_level_v1(root)
|
||||
return _static_top_level_exports(root)
|
||||
except MigrationScopeError as 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))
|
||||
if module in COMPATIBILITY_SHIMS:
|
||||
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(
|
||||
@@ -449,5 +461,5 @@ __all__ = [
|
||||
"boundary_python",
|
||||
"inspect_legacy_boundary",
|
||||
"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 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
|
||||
|
||||
@@ -830,16 +831,43 @@ def _approved_terminal_source_closure(entry: LegacyInventoryEntry, *, root: Path
|
||||
return True
|
||||
if _benchmark_terminal_source_closure(entry, root=root, tree=tree):
|
||||
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
|
||||
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:
|
||||
"""Prove sources are readable and every open migration target remains present."""
|
||||
for entry in inventory.entries:
|
||||
path = root / entry.path
|
||||
try:
|
||||
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:
|
||||
raise LegacyInventoryError(f"inventory source is not readable: {entry.path}") from exc
|
||||
if not content:
|
||||
@@ -849,8 +877,8 @@ def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Pat
|
||||
except (SyntaxError, UnicodeDecodeError) as exc:
|
||||
raise LegacyInventoryError(f"inventory source is not parseable: {entry.path}") from exc
|
||||
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)
|
||||
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:
|
||||
raise LegacyInventoryError(f"inventory target is absent: {entry.path}:{entry.symbol}")
|
||||
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/trace.py src/openultrasast/core/verdict.py""".split() # noqa: SIM905
|
||||
)
|
||||
COMPATIBILITY_SHIMS = frozenset(
|
||||
"""openultrasast.core._legacy_factpack openultrasast.core.admission
|
||||
openultrasast.core.consumer openultrasast.core.trace openultrasast.core.verdict""".split() # noqa: SIM905
|
||||
)
|
||||
COMPATIBILITY_SHIMS = frozenset(["openultrasast.core._legacy_factpack", "openultrasast.core.trace"])
|
||||
REPLACED_SHIM_MEMBERS = {
|
||||
"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(
|
||||
"""REPORT_SCHEMA_KEYS AdmissionManifest AdmissionManifestError BoundedSilence Budget BudgetGateFailure
|
||||
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
|
||||
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):
|
||||
@@ -47,6 +62,19 @@ def boundary_python(root: Path = REPO) -> tuple[Path, ...]:
|
||||
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)
|
||||
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))
|
||||
|
||||
|
||||
@@ -149,8 +177,8 @@ def relative_path(path: Path, root: Path) -> str:
|
||||
return path.as_posix()
|
||||
|
||||
|
||||
def static_top_level_v1(root: Path = REPO) -> tuple[str, ...]:
|
||||
"""Read the migration shim's literal ``__all__`` without importing it."""
|
||||
def static_top_level_exports(root: Path = REPO) -> tuple[str, ...]:
|
||||
"""Read the public package's literal ``__all__`` without importing it."""
|
||||
path = root / "src/openultrasast/core/__init__.py"
|
||||
try:
|
||||
content = path.read_bytes()
|
||||
@@ -185,6 +213,7 @@ __all__ = [
|
||||
"FROZEN_TOP_LEVEL_V1",
|
||||
"COMPATIBILITY_PATHS",
|
||||
"COMPATIBILITY_SHIMS",
|
||||
"CUTOVER_DELETIONS",
|
||||
"MigrationScopeError",
|
||||
"REPO",
|
||||
"authoritative_cutover_scope",
|
||||
@@ -197,5 +226,5 @@ __all__ = [
|
||||
"relative_path",
|
||||
"scope_identity_error",
|
||||
"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.readiness import ReadinessResult
|
||||
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):
|
||||
|
||||
@@ -19,10 +19,10 @@ from test_core_validation_v2 import (
|
||||
_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._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.proof import (
|
||||
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,
|
||||
inspect_legacy_boundary,
|
||||
require_cutover_ready,
|
||||
static_top_level_v1,
|
||||
static_top_level_exports,
|
||||
)
|
||||
from core_migration_inventory import (
|
||||
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:
|
||||
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
|
||||
imports = ", ".join(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()
|
||||
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.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.consumer_legacy_import_sites == ()
|
||||
assert report.consumer_legacy_writer_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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
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 = {
|
||||
"budget": {"Budget", "Complexity"},
|
||||
"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}
|
||||
|
||||
|
||||
def test_legacy_contract_values_have_one_compatibility_owner() -> None:
|
||||
contracts = _tree(CORE / "legacy_v1/contracts.py")
|
||||
expected = {
|
||||
"AdmissionManifest",
|
||||
"AdmissionManifestError",
|
||||
"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)
|
||||
def _literal_all(tree: ast.Module) -> tuple[str, ...]:
|
||||
values = [
|
||||
node.value
|
||||
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 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:
|
||||
for path in (CORE / "legacy_v1").glob("*.py"):
|
||||
imports = {node.module for node in ast.walk(_tree(path)) if isinstance(node, ast.ImportFrom) and node.module}
|
||||
assert all(module.startswith("openultrasast.core.legacy_v1") for module in imports if module.startswith("openultrasast"))
|
||||
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_compatibility_namespace_writer_and_temporary_shims_are_absent() -> None:
|
||||
assert not (CORE / "legacy_v1").exists()
|
||||
for name in REMOVED_COMPATIBILITY:
|
||||
assert not (CORE / name).exists()
|
||||
|
||||
|
||||
def test_historical_decoder_stays_independent_from_compatibility_namespace() -> None:
|
||||
imports = {node.module for node in ast.walk(_tree(CORE / "legacy.py")) if isinstance(node, ast.ImportFrom)}
|
||||
def test_canonical_v2_replacements_own_the_public_paths() -> None:
|
||||
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 {"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")
|
||||
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)
|
||||
|
||||
|
||||
@@ -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():
|
||||
tree = _tree(CORE / f"{module_name}.py")
|
||||
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_relation,
|
||||
)
|
||||
from openultrasast.core.verdict_v2 import DefectClaim
|
||||
from openultrasast.core.verdict import DefectClaim
|
||||
|
||||
|
||||
def _digest(value: str) -> str:
|
||||
|
||||
@@ -42,7 +42,7 @@ from openultrasast.core.semantics import (
|
||||
SourceLocation,
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user