mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 09:48:18 +00:00
feat(detection-core): freeze v1 migration foundation
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
- Existing target facts may be schema-migrated without expanding vocabulary or behavior.
|
- Existing target facts may be schema-migrated without expanding vocabulary or behavior.
|
||||||
|
|
||||||
- [ ] 1. Establish migration-safe foundations
|
- [ ] 1. Establish migration-safe foundations
|
||||||
- [ ] 1.1 Freeze historical v1 artifacts and the reviewed migration inventory
|
- [x] 1.1 Freeze historical v1 artifacts and the reviewed migration inventory
|
||||||
- Pin representative v1 trace and report bytes and hashes as immutable regression fixtures.
|
- Pin representative v1 trace and report bytes and hashes as immutable regression fixtures.
|
||||||
- Provide an independent schema-routed read-only decoder that validates committed v1 artifacts without importing the compatibility namespace, constructing v2 determinate outcomes, or exposing a writer or converter.
|
- Provide an independent schema-routed read-only decoder that validates committed v1 artifacts without importing the compatibility namespace, constructing v2 determinate outcomes, or exposing a writer or converter.
|
||||||
- Freeze the reviewed initial set of v1 readers and writers with stable entry identities, paths, symbols or capabilities, owner boundaries, and open migration status.
|
- Freeze the reviewed initial set of v1 readers and writers with stable entry identities, paths, symbols or capabilities, owner boundaries, and open migration status.
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
"""Read-only validation for committed detection-core v1 artifacts.
|
||||||
|
|
||||||
|
The v1 schemas are selected by the caller because historical artifacts do not
|
||||||
|
self-identify. This module deliberately exposes no writer or v2 conversion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from openultrasast.core.canonical import bytes_digest
|
||||||
|
|
||||||
|
LEGACY_TRACE_SCHEMA = "trace-v1"
|
||||||
|
LEGACY_REPORT_SCHEMA = "admission-report-v1"
|
||||||
|
|
||||||
|
_TRACE_FIELDS = frozenset(
|
||||||
|
{
|
||||||
|
"authority",
|
||||||
|
"completeness_reasons",
|
||||||
|
"digests",
|
||||||
|
"entry",
|
||||||
|
"guards",
|
||||||
|
"hops",
|
||||||
|
"judgment",
|
||||||
|
"operation_kind",
|
||||||
|
"resource",
|
||||||
|
"witnesses",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_REPORT_FIELDS = frozenset(
|
||||||
|
{
|
||||||
|
"coverage",
|
||||||
|
"criteria_pre_registered",
|
||||||
|
"decision",
|
||||||
|
"failed_gates",
|
||||||
|
"families",
|
||||||
|
"fix_recall",
|
||||||
|
"flagged_ordinary_per_100",
|
||||||
|
"indeterminate_rate",
|
||||||
|
"instrument_failures",
|
||||||
|
"manifest",
|
||||||
|
"pair_correctness",
|
||||||
|
"pairs",
|
||||||
|
"precision",
|
||||||
|
"runtime",
|
||||||
|
"verified_location_recall",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_RATE_FIELDS = frozenset({"count", "n", "rate", "wilson95"})
|
||||||
|
_CRITERIA_FIELDS = frozenset(
|
||||||
|
{
|
||||||
|
"max_flagged_ordinary_per_100",
|
||||||
|
"max_indeterminate_rate",
|
||||||
|
"min_fix_recall",
|
||||||
|
"min_pair_correctness",
|
||||||
|
"min_precision",
|
||||||
|
"min_verified_location_recall",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_MANIFEST_FIELDS = frozenset(
|
||||||
|
{
|
||||||
|
"acquisition_record_digest",
|
||||||
|
"analyzer_revision",
|
||||||
|
"configuration_digest",
|
||||||
|
"fact_set_digest",
|
||||||
|
"population_digest",
|
||||||
|
"population_role",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyArtifactError(ValueError):
|
||||||
|
"""Historical bytes do not satisfy the explicitly selected v1 schema."""
|
||||||
|
|
||||||
|
|
||||||
|
def _reject_duplicate_fields(pairs: list[tuple[str, object]]) -> dict[str, object]:
|
||||||
|
value: dict[str, object] = {}
|
||||||
|
for key, item in pairs:
|
||||||
|
if key in value:
|
||||||
|
raise LegacyArtifactError(f"duplicate legacy artifact field: {key}")
|
||||||
|
value[key] = item
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_json(content: bytes) -> object:
|
||||||
|
try:
|
||||||
|
return json.loads(content, object_pairs_hook=_reject_duplicate_fields)
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||||
|
raise LegacyArtifactError("legacy artifact must be valid UTF-8 JSON") from exc
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LegacyArtifact:
|
||||||
|
"""Validated historical bytes, retained without normalization or rewriting."""
|
||||||
|
|
||||||
|
schema_version: str
|
||||||
|
content: bytes
|
||||||
|
content_digest: str
|
||||||
|
byte_length: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def payload(self) -> dict[str, object]:
|
||||||
|
"""Return a fresh inspection copy of the validated JSON object."""
|
||||||
|
value = _decode_json(self.content)
|
||||||
|
if not isinstance(value, dict): # Kept defensive; construction validates this.
|
||||||
|
raise LegacyArtifactError("legacy artifact must contain a JSON object")
|
||||||
|
return {str(key): item for key, item in value.items()}
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
raise LegacyArtifactError(f"{name} must be an object")
|
||||||
|
actual = set(value)
|
||||||
|
if actual != fields:
|
||||||
|
missing = sorted(fields - actual)
|
||||||
|
unknown = sorted(actual - fields)
|
||||||
|
raise LegacyArtifactError(f"{name} has missing fields {missing} and unknown fields {unknown}")
|
||||||
|
return {str(key): item for key, item in value.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def _text(value: object, name: str) -> str:
|
||||||
|
if not isinstance(value, str) or not value:
|
||||||
|
raise LegacyArtifactError(f"{name} must be nonempty text")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _text_list(value: object, name: str) -> list[str]:
|
||||||
|
if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value):
|
||||||
|
raise LegacyArtifactError(f"{name} must be a list of nonempty text")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _integer(value: object, name: str) -> int:
|
||||||
|
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
||||||
|
raise LegacyArtifactError(f"{name} must be a non-negative integer")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _number(value: object, name: str) -> float:
|
||||||
|
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
|
||||||
|
raise LegacyArtifactError(f"{name} must be a finite non-negative number")
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _nullable_number(value: object, name: str) -> float | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return _number(value, name)
|
||||||
|
|
||||||
|
|
||||||
|
def _objects(value: object, name: str) -> list[object]:
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise LegacyArtifactError(f"{name} must be a list")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_trace(value: object) -> None:
|
||||||
|
trace = _object(value, _TRACE_FIELDS, "trace-v1")
|
||||||
|
for name in ("entry", "operation_kind", "resource", "authority"):
|
||||||
|
_text(trace[name], name)
|
||||||
|
if trace["judgment"] not in {"unguarded", "discharged", "indeterminate"}:
|
||||||
|
raise LegacyArtifactError("judgment is not a recognized v1 value")
|
||||||
|
_text_list(trace["completeness_reasons"], "completeness_reasons")
|
||||||
|
_validate_hops(trace["hops"])
|
||||||
|
_validate_witnesses(trace["witnesses"])
|
||||||
|
_validate_guards(trace["guards"])
|
||||||
|
digests = trace["digests"]
|
||||||
|
if not isinstance(digests, dict) or not all(
|
||||||
|
isinstance(key, str) and key and isinstance(item, str) and item for key, item in digests.items()
|
||||||
|
):
|
||||||
|
raise LegacyArtifactError("digests must map nonempty text to nonempty text")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_hops(value: object) -> None:
|
||||||
|
for index, item in enumerate(_objects(value, "hops")):
|
||||||
|
hop = _object(item, frozenset({"call_line", "complete", "function"}), f"hops[{index}]")
|
||||||
|
_text(hop["function"], f"hops[{index}].function")
|
||||||
|
_integer(hop["call_line"], f"hops[{index}].call_line")
|
||||||
|
if not isinstance(hop["complete"], bool):
|
||||||
|
raise LegacyArtifactError(f"hops[{index}].complete must be boolean")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_witnesses(value: object) -> None:
|
||||||
|
fields = frozenset({"kind", "provenance", "relation"})
|
||||||
|
for index, item in enumerate(_objects(value, "witnesses")):
|
||||||
|
witness = _object(item, fields, f"witnesses[{index}]")
|
||||||
|
for name in fields:
|
||||||
|
_text(witness[name], f"witnesses[{index}].{name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_guards(value: object) -> None:
|
||||||
|
fields = frozenset({"kind", "effect"})
|
||||||
|
for index, item in enumerate(_objects(value, "guards")):
|
||||||
|
guard = _object(item, fields, f"guards[{index}]")
|
||||||
|
for name in fields:
|
||||||
|
_text(guard[name], f"guards[{index}].{name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_report(value: object) -> None:
|
||||||
|
report = _object(value, _REPORT_FIELDS, "admission-report-v1")
|
||||||
|
if _number(report["coverage"], "coverage") > 1:
|
||||||
|
raise LegacyArtifactError("coverage must be between zero and one")
|
||||||
|
if report["decision"] not in {"GO", "NO-GO"}:
|
||||||
|
raise LegacyArtifactError("decision is not a recognized v1 value")
|
||||||
|
failed_gates = _text_list(report["failed_gates"], "failed_gates")
|
||||||
|
if (report["decision"] == "GO") != (not failed_gates):
|
||||||
|
raise LegacyArtifactError("decision and failed_gates are inconsistent")
|
||||||
|
_text_list(report["families"], "families")
|
||||||
|
_integer(report["instrument_failures"], "instrument_failures")
|
||||||
|
_validate_criteria(report["criteria_pre_registered"])
|
||||||
|
_validate_manifest(report["manifest"])
|
||||||
|
pairs = _validate_pairs(report["pairs"])
|
||||||
|
rates: dict[str, tuple[int, int]] = {}
|
||||||
|
for name in (
|
||||||
|
"fix_recall",
|
||||||
|
"flagged_ordinary_per_100",
|
||||||
|
"indeterminate_rate",
|
||||||
|
"pair_correctness",
|
||||||
|
"precision",
|
||||||
|
"verified_location_recall",
|
||||||
|
):
|
||||||
|
rates[name] = _validate_rate(report[name], name)
|
||||||
|
if pairs != rates["pair_correctness"]:
|
||||||
|
raise LegacyArtifactError("pairs must agree with pair_correctness")
|
||||||
|
runtime_n = _validate_runtime(report["runtime"])
|
||||||
|
if runtime_n != pairs[1] or runtime_n != rates["fix_recall"][1]:
|
||||||
|
raise LegacyArtifactError("runtime.n must agree with the evaluated pair denominator")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_criteria(value: object) -> None:
|
||||||
|
criteria = _object(value, _CRITERIA_FIELDS, "criteria_pre_registered")
|
||||||
|
for name, item in criteria.items():
|
||||||
|
_number(item, f"criteria_pre_registered.{name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_manifest(value: object) -> None:
|
||||||
|
manifest = _object(value, _MANIFEST_FIELDS, "manifest")
|
||||||
|
for name, item in manifest.items():
|
||||||
|
_text(item, f"manifest.{name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_pairs(value: object) -> tuple[int, int]:
|
||||||
|
pairs = _object(value, frozenset({"correct", "n"}), "pairs")
|
||||||
|
correct = _integer(pairs["correct"], "pairs.correct")
|
||||||
|
total = _integer(pairs["n"], "pairs.n")
|
||||||
|
if correct > total:
|
||||||
|
raise LegacyArtifactError("pairs.correct must not exceed pairs.n")
|
||||||
|
return correct, total
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_runtime(value: object) -> int:
|
||||||
|
runtime = _object(value, frozenset({"max_seconds", "mean_seconds", "n"}), "runtime")
|
||||||
|
maximum = _number(runtime["max_seconds"], "runtime.max_seconds")
|
||||||
|
mean = _number(runtime["mean_seconds"], "runtime.mean_seconds")
|
||||||
|
if maximum < mean:
|
||||||
|
raise LegacyArtifactError("runtime.max_seconds must not be below runtime.mean_seconds")
|
||||||
|
return _integer(runtime["n"], "runtime.n")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_rate(value: object, name: str) -> tuple[int, int]:
|
||||||
|
rate = _object(value, _RATE_FIELDS, name)
|
||||||
|
count = _integer(rate["count"], f"{name}.count")
|
||||||
|
total = _integer(rate["n"], f"{name}.n")
|
||||||
|
if count > total:
|
||||||
|
raise LegacyArtifactError(f"{name}.count must not exceed {name}.n")
|
||||||
|
actual = _nullable_number(rate["rate"], f"{name}.rate")
|
||||||
|
interval = rate["wilson95"]
|
||||||
|
if not isinstance(interval, list) or len(interval) != 2:
|
||||||
|
raise LegacyArtifactError(f"{name}.wilson95 must contain two bounds")
|
||||||
|
bounds = tuple(_nullable_number(bound, f"{name}.wilson95[{index}]") for index, bound in enumerate(interval))
|
||||||
|
if total == 0:
|
||||||
|
if actual is not None or bounds != (None, None):
|
||||||
|
raise LegacyArtifactError(f"{name} must use null rate and interval when n is zero")
|
||||||
|
return count, total
|
||||||
|
if actual is None or any(bound is None for bound in bounds):
|
||||||
|
raise LegacyArtifactError(f"{name} must use non-null rate and interval when n is positive")
|
||||||
|
lower, upper = bounds
|
||||||
|
assert lower is not None and upper is not None
|
||||||
|
scale = 100.0 if name == "flagged_ordinary_per_100" else 1.0
|
||||||
|
expected = count / total * scale
|
||||||
|
if not math.isclose(actual, expected, rel_tol=1e-12, abs_tol=1e-12):
|
||||||
|
raise LegacyArtifactError(f"{name}.rate must be derived from count and n")
|
||||||
|
if not 0 <= lower <= upper <= scale:
|
||||||
|
raise LegacyArtifactError(f"{name}.wilson95 bounds are invalid")
|
||||||
|
return count, total
|
||||||
|
|
||||||
|
|
||||||
|
def decode_legacy_artifact(content: bytes, *, schema_version: str) -> LegacyArtifact:
|
||||||
|
"""Validate exact historical bytes through one explicit v1 schema route."""
|
||||||
|
if schema_version not in {LEGACY_TRACE_SCHEMA, LEGACY_REPORT_SCHEMA}:
|
||||||
|
raise LegacyArtifactError(f"unknown legacy schema: {schema_version!r}")
|
||||||
|
if type(content) is not bytes:
|
||||||
|
raise LegacyArtifactError("legacy artifact content must be exact bytes")
|
||||||
|
if not content:
|
||||||
|
raise LegacyArtifactError("legacy artifact content must be nonempty")
|
||||||
|
value = _decode_json(content)
|
||||||
|
if isinstance(value, dict) and "schema_version" in value:
|
||||||
|
raise LegacyArtifactError("legacy artifact schema is ambiguous; use the explicit route only")
|
||||||
|
validator = _validate_trace if schema_version == LEGACY_TRACE_SCHEMA else _validate_report
|
||||||
|
validator(value)
|
||||||
|
return LegacyArtifact(
|
||||||
|
schema_version=schema_version,
|
||||||
|
content=content,
|
||||||
|
content_digest=bytes_digest(content),
|
||||||
|
byte_length=len(content),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_legacy_artifact(path: str | Path, *, schema_version: str) -> LegacyArtifact:
|
||||||
|
"""Read nonzero historical bytes and validate them without any write path."""
|
||||||
|
try:
|
||||||
|
content = Path(path).read_bytes()
|
||||||
|
except OSError as exc:
|
||||||
|
raise LegacyArtifactError(f"legacy artifact is not readable: {path}") from exc
|
||||||
|
return decode_legacy_artifact(content, schema_version=schema_version)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LEGACY_REPORT_SCHEMA",
|
||||||
|
"LEGACY_TRACE_SCHEMA",
|
||||||
|
"LegacyArtifact",
|
||||||
|
"LegacyArtifactError",
|
||||||
|
"decode_legacy_artifact",
|
||||||
|
"read_legacy_artifact",
|
||||||
|
]
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"""Independent syntax-only audit of explicit detection-core v1 readers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import subprocess
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
_EXCLUDED_PREFIXES = ("benchmarks/fixtures/", "benchmarks/pairs/", "tests/fixtures/")
|
||||||
|
_LEGACY_IMPORTS: dict[str, frozenset[str] | None] = {
|
||||||
|
"openultrasast.core": frozenset(
|
||||||
|
{
|
||||||
|
"AdmissionManifest",
|
||||||
|
"AdmissionManifestError",
|
||||||
|
"BoundedSilence",
|
||||||
|
"Budget",
|
||||||
|
"Complexity",
|
||||||
|
"Coverage",
|
||||||
|
"FactPack",
|
||||||
|
"Guard",
|
||||||
|
"Hop",
|
||||||
|
"InstrumentFailure",
|
||||||
|
"Judgment",
|
||||||
|
"ProbeResult",
|
||||||
|
"ProvenClaim",
|
||||||
|
"PushRequest",
|
||||||
|
"PushVerdict",
|
||||||
|
"TraceRecord",
|
||||||
|
"Witness",
|
||||||
|
"admit_fact_pack",
|
||||||
|
"as_clean",
|
||||||
|
"check_reads",
|
||||||
|
"consume_verdict",
|
||||||
|
"validate_admission_manifest",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"openultrasast.core._legacy_factpack": None,
|
||||||
|
"openultrasast.core.admission": frozenset(
|
||||||
|
{
|
||||||
|
"AdmissionManifest",
|
||||||
|
"AdmissionManifestError",
|
||||||
|
"REPORT_SCHEMA_KEYS",
|
||||||
|
"validate_admission_manifest",
|
||||||
|
"validate_report_schema",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"openultrasast.core.budget": frozenset({"Budget", "Complexity"}),
|
||||||
|
"openultrasast.core.consumer": None,
|
||||||
|
"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(
|
||||||
|
{
|
||||||
|
"FittedComponentBlocked",
|
||||||
|
"LegacyProbeResult",
|
||||||
|
"ShortcutDetected",
|
||||||
|
"assert_no_shortcut",
|
||||||
|
"require_admission_evidence",
|
||||||
|
"reversed_pair_probe",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"openultrasast.core.provenance": frozenset({"ProvenanceMismatch", "stamp", "verify_stamp"}),
|
||||||
|
"openultrasast.core.trace": None,
|
||||||
|
"openultrasast.core.verdict": None,
|
||||||
|
}
|
||||||
|
_TOP_LEVEL_COMPATIBILITY = _LEGACY_IMPORTS["openultrasast.core"] or frozenset()
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyReaderAuditError(ValueError):
|
||||||
|
"""The explicit-reader audit could not read or parse its declared scope."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ReaderAuditReport:
|
||||||
|
files_read: int
|
||||||
|
bytes_read: int
|
||||||
|
capabilities: tuple[tuple[str, str], ...]
|
||||||
|
|
||||||
|
|
||||||
|
def tracked_python() -> tuple[Path, ...]:
|
||||||
|
completed = subprocess.run(["git", "ls-files", "*.py"], cwd=REPO, check=True, capture_output=True, text=True)
|
||||||
|
return tuple(
|
||||||
|
REPO / relative
|
||||||
|
for relative in completed.stdout.splitlines()
|
||||||
|
if relative.endswith(".py") and not relative.startswith(_EXCLUDED_PREFIXES)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _dotted(node: ast.AST) -> str:
|
||||||
|
if isinstance(node, ast.Name):
|
||||||
|
return node.id
|
||||||
|
if isinstance(node, ast.Attribute):
|
||||||
|
prefix = _dotted(node.value)
|
||||||
|
return f"{prefix}.{node.attr}" if prefix else ""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _explicit_imports(tree: ast.Module) -> set[str]:
|
||||||
|
anchors = set()
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ImportFrom) and node.module in _LEGACY_IMPORTS:
|
||||||
|
allowed = _LEGACY_IMPORTS[node.module]
|
||||||
|
anchors.update(f"import_from:{node.module}:{alias.name}" for alias in node.names if allowed is None or alias.name in allowed)
|
||||||
|
elif isinstance(node, ast.Import):
|
||||||
|
anchors.update(
|
||||||
|
f"import:{alias.name}" for alias in node.names if alias.name in _LEGACY_IMPORTS and _LEGACY_IMPORTS[alias.name] is None
|
||||||
|
)
|
||||||
|
return anchors
|
||||||
|
|
||||||
|
|
||||||
|
def _direct_submodule_bindings(tree: ast.Module) -> dict[str, str]:
|
||||||
|
bindings = {}
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
if alias.name in _LEGACY_IMPORTS and alias.name != "openultrasast.core":
|
||||||
|
bindings[alias.asname or alias.name] = alias.name
|
||||||
|
elif isinstance(node, ast.ImportFrom) and node.module == "openultrasast.core":
|
||||||
|
for alias in node.names:
|
||||||
|
module = f"openultrasast.core.{alias.name}"
|
||||||
|
if module in _LEGACY_IMPORTS:
|
||||||
|
bindings[alias.asname or alias.name] = module
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
|
def _module_attribute_reads(tree: ast.Module) -> set[str]:
|
||||||
|
bindings = _direct_submodule_bindings(tree)
|
||||||
|
reads = set()
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.Attribute):
|
||||||
|
continue
|
||||||
|
module = bindings.get(_dotted(node.value))
|
||||||
|
if module is None:
|
||||||
|
continue
|
||||||
|
allowed = _LEGACY_IMPORTS[module]
|
||||||
|
if allowed is None or node.attr in allowed:
|
||||||
|
reads.add(f"module_attr:{module}:{node.attr}")
|
||||||
|
return reads
|
||||||
|
|
||||||
|
|
||||||
|
def _top_level_aliases(tree: ast.Module) -> set[str]:
|
||||||
|
aliases = set()
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
aliases.update(alias.asname for alias in node.names if alias.name == "openultrasast.core" and alias.asname)
|
||||||
|
elif isinstance(node, ast.ImportFrom) and node.module == "openultrasast":
|
||||||
|
aliases.update(alias.asname or alias.name for alias in node.names if alias.name == "core")
|
||||||
|
return aliases
|
||||||
|
|
||||||
|
|
||||||
|
def _top_level_reads(tree: ast.Module) -> set[str]:
|
||||||
|
aliases = _top_level_aliases(tree)
|
||||||
|
reads = set()
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.Attribute) or node.attr not in _TOP_LEVEL_COMPATIBILITY:
|
||||||
|
continue
|
||||||
|
base = _dotted(node.value)
|
||||||
|
if base in aliases or base == "openultrasast.core":
|
||||||
|
reads.add(f"top_level:{node.attr}")
|
||||||
|
return reads
|
||||||
|
|
||||||
|
|
||||||
|
def audit_legacy_readers(paths: tuple[Path, ...]) -> ReaderAuditReport:
|
||||||
|
capabilities = set()
|
||||||
|
files_read = 0
|
||||||
|
bytes_read = 0
|
||||||
|
for path in paths:
|
||||||
|
relative = path.relative_to(REPO).as_posix()
|
||||||
|
try:
|
||||||
|
content = path.read_bytes()
|
||||||
|
except OSError as exc:
|
||||||
|
raise LegacyReaderAuditError(f"reader audit input is not readable: {relative}") from exc
|
||||||
|
if not content:
|
||||||
|
raise LegacyReaderAuditError(f"reader audit input must contain nonzero bytes: {relative}")
|
||||||
|
try:
|
||||||
|
tree = ast.parse(content, filename=relative)
|
||||||
|
except (SyntaxError, UnicodeDecodeError) as exc:
|
||||||
|
raise LegacyReaderAuditError(f"reader audit input is not parseable: {relative}") from exc
|
||||||
|
files_read += 1
|
||||||
|
bytes_read += len(content)
|
||||||
|
anchors = _explicit_imports(tree) | _module_attribute_reads(tree) | _top_level_reads(tree)
|
||||||
|
capabilities.update((relative, anchor) for anchor in anchors)
|
||||||
|
return ReaderAuditReport(files_read, bytes_read, tuple(sorted(capabilities)))
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["LegacyReaderAuditError", "ReaderAuditReport", "audit_legacy_readers", "tracked_python"]
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
"""Validation primitives for the frozen detection-core v1 migration worklist."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from openultrasast.core.legacy import LegacyArtifact, decode_legacy_artifact
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
INVENTORY_SCHEMA = "openultrasast.detection-core.legacy-migration.v1"
|
||||||
|
_ENTRY_FIELDS = frozenset(
|
||||||
|
{
|
||||||
|
"artifact_family",
|
||||||
|
"capability_anchors",
|
||||||
|
"dependency_kind",
|
||||||
|
"disposition",
|
||||||
|
"entry_id",
|
||||||
|
"owner_boundary",
|
||||||
|
"path",
|
||||||
|
"symbol",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_ARTIFACT_FIELDS = frozenset({"byte_length", "path", "schema_version", "sha256"})
|
||||||
|
_INVENTORY_FIELDS = frozenset({"entries", "historical_artifacts", "schema_version"})
|
||||||
|
_OWNER_BOUNDARIES = frozenset(
|
||||||
|
{"authorization-adapter", "benchmark", "core", "hunt", "operational-tooling", "push", "repository-tooling", "tests"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyInventoryError(ValueError):
|
||||||
|
"""The frozen migration worklist or one of its targets is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class MigrationDisposition(Enum):
|
||||||
|
OPEN = "open"
|
||||||
|
V2_MIGRATED = "v2_migrated"
|
||||||
|
RETIRED = "retired"
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyKind(Enum):
|
||||||
|
READER = "reader"
|
||||||
|
WRITER = "writer"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HistoricalArtifactRecord:
|
||||||
|
path: str
|
||||||
|
schema_version: str
|
||||||
|
sha256: str
|
||||||
|
byte_length: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LegacyInventoryEntry:
|
||||||
|
entry_id: str
|
||||||
|
path: str
|
||||||
|
symbol: str
|
||||||
|
dependency_kind: DependencyKind
|
||||||
|
artifact_family: str
|
||||||
|
capability_anchors: tuple[str, ...]
|
||||||
|
owner_boundary: str
|
||||||
|
disposition: MigrationDisposition
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LegacyMigrationInventory:
|
||||||
|
schema_version: str
|
||||||
|
historical_artifacts: tuple[HistoricalArtifactRecord, ...]
|
||||||
|
entries: tuple[LegacyInventoryEntry, ...]
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
raise LegacyInventoryError(f"{name} must be an object")
|
||||||
|
actual = set(value)
|
||||||
|
if actual != fields:
|
||||||
|
raise LegacyInventoryError(f"{name} fields differ: missing={sorted(fields - actual)}, unknown={sorted(actual - fields)}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _text(value: object, name: str) -> str:
|
||||||
|
if not isinstance(value, str) or not value:
|
||||||
|
raise LegacyInventoryError(f"{name} must be nonempty text")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(value: object, name: str) -> str:
|
||||||
|
text = _text(value, name)
|
||||||
|
if len(text) != 64 or any(character not in "0123456789abcdef" for character in text):
|
||||||
|
raise LegacyInventoryError(f"{name} must be a lowercase SHA-256 digest")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _relative_path(value: object, name: str) -> str:
|
||||||
|
text = _text(value, name)
|
||||||
|
path = Path(text)
|
||||||
|
if path.is_absolute() or ".." in path.parts or path.as_posix() != text:
|
||||||
|
raise LegacyInventoryError(f"{name} must be a normalized repository-relative path")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_artifact(value: object, index: int) -> HistoricalArtifactRecord:
|
||||||
|
item = _object(value, _ARTIFACT_FIELDS, f"historical_artifacts[{index}]")
|
||||||
|
length = item["byte_length"]
|
||||||
|
if isinstance(length, bool) or not isinstance(length, int) or length <= 0:
|
||||||
|
raise LegacyInventoryError(f"historical_artifacts[{index}].byte_length must be positive")
|
||||||
|
return HistoricalArtifactRecord(
|
||||||
|
path=_relative_path(item["path"], f"historical_artifacts[{index}].path"),
|
||||||
|
schema_version=_text(item["schema_version"], f"historical_artifacts[{index}].schema_version"),
|
||||||
|
sha256=_sha256(item["sha256"], f"historical_artifacts[{index}].sha256"),
|
||||||
|
byte_length=length,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _enum(enum_type: type[Enum], value: object, name: str) -> Enum:
|
||||||
|
text = _text(value, name)
|
||||||
|
try:
|
||||||
|
return enum_type(text)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise LegacyInventoryError(f"{name} has unknown value {text!r}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_entry(value: object, index: int) -> LegacyInventoryEntry:
|
||||||
|
item = _object(value, _ENTRY_FIELDS, f"entries[{index}]")
|
||||||
|
owner = _text(item["owner_boundary"], f"entries[{index}].owner_boundary")
|
||||||
|
if owner not in _OWNER_BOUNDARIES:
|
||||||
|
raise LegacyInventoryError(f"entries[{index}].owner_boundary is unknown")
|
||||||
|
raw_anchors = item["capability_anchors"]
|
||||||
|
if not isinstance(raw_anchors, list) or not raw_anchors:
|
||||||
|
raise LegacyInventoryError(f"entries[{index}].capability_anchors must be a nonempty list")
|
||||||
|
anchors = tuple(_text(anchor, f"entries[{index}].capability_anchors") for anchor in raw_anchors)
|
||||||
|
if len(set(anchors)) != len(anchors):
|
||||||
|
raise LegacyInventoryError(f"entries[{index}].capability_anchors contains duplicates")
|
||||||
|
entry = LegacyInventoryEntry(
|
||||||
|
entry_id=_sha256(item["entry_id"], f"entries[{index}].entry_id"),
|
||||||
|
path=_relative_path(item["path"], f"entries[{index}].path"),
|
||||||
|
symbol=_text(item["symbol"], f"entries[{index}].symbol"),
|
||||||
|
dependency_kind=_enum(DependencyKind, item["dependency_kind"], f"entries[{index}].dependency_kind"), # type: ignore[arg-type]
|
||||||
|
artifact_family=_text(item["artifact_family"], f"entries[{index}].artifact_family"),
|
||||||
|
capability_anchors=anchors,
|
||||||
|
owner_boundary=owner,
|
||||||
|
disposition=_enum(MigrationDisposition, item["disposition"], f"entries[{index}].disposition"), # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
if entry.entry_id != entry_identity(entry):
|
||||||
|
raise LegacyInventoryError(f"entries[{index}].entry_id does not match immutable identity fields")
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def entry_identity(entry: LegacyInventoryEntry) -> str:
|
||||||
|
"""Derive the stable identity without the mutable migration disposition."""
|
||||||
|
value = {
|
||||||
|
"artifact_family": entry.artifact_family,
|
||||||
|
"capability_anchors": entry.capability_anchors,
|
||||||
|
"dependency_kind": entry.dependency_kind.value,
|
||||||
|
"owner_boundary": entry.owner_boundary,
|
||||||
|
"path": entry.path,
|
||||||
|
"symbol": entry.symbol,
|
||||||
|
}
|
||||||
|
canonical = json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
||||||
|
return hashlib.sha256(canonical).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_inventory(content: bytes) -> LegacyMigrationInventory:
|
||||||
|
try:
|
||||||
|
value = json.loads(content)
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||||
|
raise LegacyInventoryError("migration inventory must be valid UTF-8 JSON") from exc
|
||||||
|
root = _object(value, _INVENTORY_FIELDS, "migration inventory")
|
||||||
|
if root["schema_version"] != INVENTORY_SCHEMA:
|
||||||
|
raise LegacyInventoryError("migration inventory schema_version is unknown")
|
||||||
|
raw_artifacts = root["historical_artifacts"]
|
||||||
|
raw_entries = root["entries"]
|
||||||
|
if not isinstance(raw_artifacts, list) or not isinstance(raw_entries, list) or not raw_entries:
|
||||||
|
raise LegacyInventoryError("migration inventory requires artifact and nonempty entry lists")
|
||||||
|
artifacts = tuple(_parse_artifact(item, index) for index, item in enumerate(raw_artifacts))
|
||||||
|
entries = tuple(_parse_entry(item, index) for index, item in enumerate(raw_entries))
|
||||||
|
if len({entry.entry_id for entry in entries}) != len(entries):
|
||||||
|
raise LegacyInventoryError("migration inventory contains duplicate identities")
|
||||||
|
return LegacyMigrationInventory(INVENTORY_SCHEMA, artifacts, entries)
|
||||||
|
|
||||||
|
|
||||||
|
def load_migration_inventory(path: str | Path) -> LegacyMigrationInventory:
|
||||||
|
"""Read the nonzero reviewed worklist without discovering new entries."""
|
||||||
|
try:
|
||||||
|
content = Path(path).read_bytes()
|
||||||
|
except OSError as exc:
|
||||||
|
raise LegacyInventoryError(f"migration inventory is not readable: {path}") from exc
|
||||||
|
if not content:
|
||||||
|
raise LegacyInventoryError("migration inventory must contain nonzero bytes")
|
||||||
|
return _decode_inventory(content)
|
||||||
|
|
||||||
|
|
||||||
|
def _defined_symbols(tree: ast.Module) -> set[str]:
|
||||||
|
symbols = {"<module>"}
|
||||||
|
|
||||||
|
def visit(nodes: list[ast.stmt], parents: tuple[str, ...] = ()) -> None:
|
||||||
|
for node in nodes:
|
||||||
|
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
|
current = (*parents, node.name)
|
||||||
|
symbols.add(".".join(current))
|
||||||
|
visit(node.body, current)
|
||||||
|
|
||||||
|
visit(tree.body)
|
||||||
|
return symbols
|
||||||
|
|
||||||
|
|
||||||
|
def _call_leaf(node: ast.Call) -> str:
|
||||||
|
if isinstance(node.func, ast.Name):
|
||||||
|
return node.func.id
|
||||||
|
if isinstance(node.func, ast.Attribute):
|
||||||
|
return node.func.attr
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _dotted(node: ast.AST) -> str:
|
||||||
|
if isinstance(node, ast.Name):
|
||||||
|
return node.id
|
||||||
|
if isinstance(node, ast.Attribute):
|
||||||
|
prefix = _dotted(node.value)
|
||||||
|
return f"{prefix}.{node.attr}" if prefix else ""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _top_level_reads(tree: ast.Module) -> set[str]:
|
||||||
|
aliases = set()
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
aliases.update(alias.asname for alias in node.names if alias.name == "openultrasast.core" and alias.asname)
|
||||||
|
elif isinstance(node, ast.ImportFrom) and node.module == "openultrasast":
|
||||||
|
aliases.update(alias.asname or alias.name for alias in node.names if alias.name == "core")
|
||||||
|
return {
|
||||||
|
node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute) and _dotted(node.value) in aliases | {"openultrasast.core"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _direct_core_module_bindings(tree: ast.Module) -> dict[str, str]:
|
||||||
|
bindings = {}
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
if alias.name.startswith("openultrasast.core."):
|
||||||
|
bindings[alias.asname or alias.name] = alias.name
|
||||||
|
elif isinstance(node, ast.ImportFrom) and node.module == "openultrasast.core":
|
||||||
|
for alias in node.names:
|
||||||
|
bindings[alias.asname or alias.name] = f"openultrasast.core.{alias.name}"
|
||||||
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
|
def _direct_module_attributes(tree: ast.Module) -> set[str]:
|
||||||
|
bindings = _direct_core_module_bindings(tree)
|
||||||
|
return {
|
||||||
|
f"{module}:{node.attr}"
|
||||||
|
for node in ast.walk(tree)
|
||||||
|
if isinstance(node, ast.Attribute) and (module := bindings.get(_dotted(node.value))) is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _capability_index(tree: ast.Module) -> dict[str, set[str]]:
|
||||||
|
imported = {
|
||||||
|
f"{node.module}:{alias.name}"
|
||||||
|
for node in ast.walk(tree)
|
||||||
|
if isinstance(node, ast.ImportFrom) and node.module is not None
|
||||||
|
for alias in node.names
|
||||||
|
}
|
||||||
|
modules = {alias.name for node in ast.walk(tree) if isinstance(node, ast.Import) for alias in node.names}
|
||||||
|
constants = {node.value for node in ast.walk(tree) if isinstance(node, ast.Constant) and isinstance(node.value, str)}
|
||||||
|
names = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
|
||||||
|
names.update(node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute))
|
||||||
|
return {
|
||||||
|
"call": {_call_leaf(node) for node in ast.walk(tree) if isinstance(node, ast.Call)},
|
||||||
|
"constant": constants,
|
||||||
|
"definition": _defined_symbols(tree),
|
||||||
|
"import": modules,
|
||||||
|
"import_from": imported,
|
||||||
|
"module_attr": _direct_module_attributes(tree),
|
||||||
|
"name": names,
|
||||||
|
"top_level": _top_level_reads(tree),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _missing_anchors(entry: LegacyInventoryEntry, tree: ast.Module) -> tuple[str, ...]:
|
||||||
|
index = _capability_index(tree)
|
||||||
|
missing = []
|
||||||
|
for anchor in entry.capability_anchors:
|
||||||
|
kind, separator, value = anchor.partition(":")
|
||||||
|
if not separator or kind not in index or value not in index[kind]:
|
||||||
|
missing.append(anchor)
|
||||||
|
return tuple(missing)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_inventory_sources(inventory: LegacyMigrationInventory, *, root: Path) -> None:
|
||||||
|
"""Prove every registered migration target is readable, nonzero, and present."""
|
||||||
|
for entry in inventory.entries:
|
||||||
|
path = root / entry.path
|
||||||
|
try:
|
||||||
|
content = path.read_bytes()
|
||||||
|
except OSError as exc:
|
||||||
|
raise LegacyInventoryError(f"inventory source is not readable: {entry.path}") from exc
|
||||||
|
if not content:
|
||||||
|
raise LegacyInventoryError(f"inventory source must contain nonzero bytes: {entry.path}")
|
||||||
|
try:
|
||||||
|
tree = ast.parse(content, filename=entry.path)
|
||||||
|
except (SyntaxError, UnicodeDecodeError) as exc:
|
||||||
|
raise LegacyInventoryError(f"inventory source is not parseable: {entry.path}") from exc
|
||||||
|
if entry.symbol not in _defined_symbols(tree):
|
||||||
|
raise LegacyInventoryError(f"inventory target is absent: {entry.path}:{entry.symbol}")
|
||||||
|
missing = _missing_anchors(entry, tree)
|
||||||
|
if missing:
|
||||||
|
raise LegacyInventoryError(f"inventory capability is absent: {entry.path}:{missing}")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_historical_artifacts(inventory: LegacyMigrationInventory, *, root: Path) -> tuple[LegacyArtifact, ...]:
|
||||||
|
"""Re-open and validate every byte-frozen v1 artifact from its exact manifest."""
|
||||||
|
artifacts = []
|
||||||
|
for record in inventory.historical_artifacts:
|
||||||
|
try:
|
||||||
|
content = (root / record.path).read_bytes()
|
||||||
|
except OSError as exc:
|
||||||
|
raise LegacyInventoryError(f"historical artifact is not readable: {record.path}") from exc
|
||||||
|
if len(content) != record.byte_length:
|
||||||
|
raise LegacyInventoryError(f"historical artifact length changed: {record.path}")
|
||||||
|
if hashlib.sha256(content).hexdigest() != record.sha256:
|
||||||
|
raise LegacyInventoryError(f"historical artifact digest changed: {record.path}")
|
||||||
|
artifact = decode_legacy_artifact(content, schema_version=record.schema_version)
|
||||||
|
artifacts.append(artifact)
|
||||||
|
return tuple(artifacts)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_inventory_transition(frozen: LegacyMigrationInventory, candidate: LegacyMigrationInventory) -> None:
|
||||||
|
"""Allow only disposition closure while retaining the frozen worklist identity."""
|
||||||
|
frozen_ids = tuple(entry.entry_id for entry in frozen.entries)
|
||||||
|
candidate_ids = tuple(entry.entry_id for entry in candidate.entries)
|
||||||
|
if frozen.schema_version != candidate.schema_version or frozen_ids != candidate_ids:
|
||||||
|
raise LegacyInventoryError("migration inventory identities changed")
|
||||||
|
if frozen.historical_artifacts != candidate.historical_artifacts:
|
||||||
|
raise LegacyInventoryError("historical artifact identities changed")
|
||||||
|
for before, after in zip(frozen.entries, candidate.entries, strict=True):
|
||||||
|
if not isinstance(before.disposition, MigrationDisposition) or not isinstance(after.disposition, MigrationDisposition):
|
||||||
|
raise LegacyInventoryError("migration inventory disposition is outside the closed domain")
|
||||||
|
if not isinstance(before.dependency_kind, DependencyKind) or not isinstance(after.dependency_kind, DependencyKind):
|
||||||
|
raise LegacyInventoryError("migration inventory dependency kind is outside the closed domain")
|
||||||
|
if entry_identity(after) != after.entry_id or entry_identity(before) != entry_identity(after):
|
||||||
|
raise LegacyInventoryError("migration inventory identities changed")
|
||||||
|
if before.disposition is MigrationDisposition.OPEN:
|
||||||
|
continue
|
||||||
|
if after.disposition is not before.disposition:
|
||||||
|
raise LegacyInventoryError(f"terminal migration entry reopened or changed: {before.entry_id}")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"REPO",
|
||||||
|
"DependencyKind",
|
||||||
|
"HistoricalArtifactRecord",
|
||||||
|
"LegacyInventoryEntry",
|
||||||
|
"LegacyInventoryError",
|
||||||
|
"LegacyMigrationInventory",
|
||||||
|
"MigrationDisposition",
|
||||||
|
"entry_identity",
|
||||||
|
"load_migration_inventory",
|
||||||
|
"validate_historical_artifacts",
|
||||||
|
"validate_inventory_sources",
|
||||||
|
"validate_inventory_transition",
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
+91
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"coverage": 0.3333333333333333,
|
||||||
|
"criteria_pre_registered": {
|
||||||
|
"max_flagged_ordinary_per_100": 10.0,
|
||||||
|
"max_indeterminate_rate": 0.5,
|
||||||
|
"min_fix_recall": 0.5,
|
||||||
|
"min_pair_correctness": 0.5,
|
||||||
|
"min_precision": 0.25,
|
||||||
|
"min_verified_location_recall": 0.5
|
||||||
|
},
|
||||||
|
"decision": "NO-GO",
|
||||||
|
"failed_gates": [
|
||||||
|
"min_fix_recall: 0.0 < 0.5",
|
||||||
|
"min_pair_correctness: 0.0 < 0.5",
|
||||||
|
"min_precision: 0.0 < 0.25",
|
||||||
|
"min_verified_location_recall: not measured (n=0) vs 0.5"
|
||||||
|
],
|
||||||
|
"families": [],
|
||||||
|
"fix_recall": {
|
||||||
|
"count": 0,
|
||||||
|
"n": 12,
|
||||||
|
"rate": 0.0,
|
||||||
|
"wilson95": [
|
||||||
|
0,
|
||||||
|
0.2425007574992425
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"flagged_ordinary_per_100": {
|
||||||
|
"count": 2,
|
||||||
|
"n": 240,
|
||||||
|
"rate": 0.8333333333333334,
|
||||||
|
"wilson95": [
|
||||||
|
0.2288235363161974,
|
||||||
|
2.9870346273441455
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indeterminate_rate": {
|
||||||
|
"count": 3,
|
||||||
|
"n": 264,
|
||||||
|
"rate": 0.011363636363636364,
|
||||||
|
"wilson95": [
|
||||||
|
0.003871976570054633,
|
||||||
|
0.03287212890123884
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"instrument_failures": 0,
|
||||||
|
"manifest": {
|
||||||
|
"acquisition_record_digest": "none",
|
||||||
|
"analyzer_revision": "d063167b28ed7db2335737fdb0dae0555225ea85",
|
||||||
|
"configuration_digest": "095b5a4905ad",
|
||||||
|
"fact_set_digest": "3eb5bf699434",
|
||||||
|
"population_digest": "c866c0e812c0b54476c7168fdb0e4a20652de0d30485270afecbe51e5c35e424",
|
||||||
|
"population_role": "admission"
|
||||||
|
},
|
||||||
|
"pair_correctness": {
|
||||||
|
"count": 0,
|
||||||
|
"n": 12,
|
||||||
|
"rate": 0.0,
|
||||||
|
"wilson95": [
|
||||||
|
0,
|
||||||
|
0.2425007574992425
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pairs": {
|
||||||
|
"correct": 0,
|
||||||
|
"n": 12
|
||||||
|
},
|
||||||
|
"precision": {
|
||||||
|
"count": 0,
|
||||||
|
"n": 14,
|
||||||
|
"rate": 0.0,
|
||||||
|
"wilson95": [
|
||||||
|
0,
|
||||||
|
0.21531701192718142
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"max_seconds": 7.112766742007807,
|
||||||
|
"mean_seconds": 2.8931797610857757,
|
||||||
|
"n": 12
|
||||||
|
},
|
||||||
|
"verified_location_recall": {
|
||||||
|
"count": 0,
|
||||||
|
"n": 0,
|
||||||
|
"rate": null,
|
||||||
|
"wilson95": [
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{"authority":"admin","completeness_reasons":[],"digests":{"input":"abc123"},"entry":"src/app.py::handler","guards":[{"effect":"branches","kind":"permission_check"}],"hops":[{"call_line":10,"complete":true,"function":"handler"}],"judgment":"unguarded","operation_kind":"protected_read","resource":"credentials","witnesses":[{"kind":"guard","provenance":"fact:checkPermission","relation":"all_paths"}]}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"""Read-only historical validation for the detection-core v1 migration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from openultrasast.core.legacy import (
|
||||||
|
LEGACY_REPORT_SCHEMA,
|
||||||
|
LEGACY_TRACE_SCHEMA,
|
||||||
|
LegacyArtifactError,
|
||||||
|
decode_legacy_artifact,
|
||||||
|
read_legacy_artifact,
|
||||||
|
)
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
FIXTURES = REPO / "tests/fixtures/detection_core/v1"
|
||||||
|
TRACE_SHA256 = "93c58ba7c51f11a3173c939e9dcc1eac33334c05a1e066bcc1e4879c4c8de8f0"
|
||||||
|
REPORT_SHA256 = "e07f10c838b3161040edf05965d1cd1e861023dce36f74f7367dc030b883bcca"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("name", "digest", "length"),
|
||||||
|
(("trace.json", TRACE_SHA256, 401), ("report.json", REPORT_SHA256, 1902)),
|
||||||
|
)
|
||||||
|
def test_v1_fixtures_are_nonzero_and_byte_exact(name: str, digest: str, length: int) -> None:
|
||||||
|
content = (FIXTURES / name).read_bytes()
|
||||||
|
|
||||||
|
assert len(content) == length, "the historical instrument must read the expected nonzero bytes"
|
||||||
|
assert hashlib.sha256(content).hexdigest() == digest
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_reader_routes_explicit_trace_and_report_schemas() -> None:
|
||||||
|
trace = read_legacy_artifact(FIXTURES / "trace.json", schema_version=LEGACY_TRACE_SCHEMA)
|
||||||
|
report = read_legacy_artifact(FIXTURES / "report.json", schema_version=LEGACY_REPORT_SCHEMA)
|
||||||
|
|
||||||
|
assert trace.schema_version == "trace-v1" and trace.byte_length == 401
|
||||||
|
assert trace.payload["judgment"] == "unguarded"
|
||||||
|
assert report.schema_version == "admission-report-v1" and report.byte_length == 1902
|
||||||
|
assert report.payload["decision"] == "NO-GO"
|
||||||
|
assert trace.content == (FIXTURES / "trace.json").read_bytes()
|
||||||
|
assert report.content == (FIXTURES / "report.json").read_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_reader_exposes_no_writer_converter_or_baseline_comparison() -> None:
|
||||||
|
from openultrasast.core.evidence import EvidenceRecord
|
||||||
|
|
||||||
|
artifact = read_legacy_artifact(FIXTURES / "trace.json", schema_version=LEGACY_TRACE_SCHEMA)
|
||||||
|
|
||||||
|
assert not isinstance(artifact, EvidenceRecord)
|
||||||
|
for prohibited in ("write", "to_v2", "decision", "baseline_identity", "compare_identity"):
|
||||||
|
assert not hasattr(artifact, prohibited)
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
artifact.content = b"replacement" # type: ignore[misc]
|
||||||
|
changed = artifact.payload
|
||||||
|
changed["judgment"] = "discharged"
|
||||||
|
assert artifact.payload["judgment"] == "unguarded"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("schema", ("", "trace-v2", "admission-report-v2", "unknown"))
|
||||||
|
def test_legacy_reader_refuses_unknown_or_v2_schema_routes(schema: str) -> None:
|
||||||
|
content = (FIXTURES / "trace.json").read_bytes()
|
||||||
|
|
||||||
|
with pytest.raises(LegacyArtifactError, match="schema"):
|
||||||
|
decode_legacy_artifact(content, schema_version=schema)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_reader_refuses_cross_routed_or_embedded_schema_identity() -> None:
|
||||||
|
with pytest.raises(LegacyArtifactError):
|
||||||
|
decode_legacy_artifact((FIXTURES / "trace.json").read_bytes(), schema_version=LEGACY_REPORT_SCHEMA)
|
||||||
|
with pytest.raises(LegacyArtifactError):
|
||||||
|
decode_legacy_artifact((FIXTURES / "report.json").read_bytes(), schema_version=LEGACY_TRACE_SCHEMA)
|
||||||
|
embedded = {**json.loads((FIXTURES / "trace.json").read_bytes()), "schema_version": LEGACY_TRACE_SCHEMA}
|
||||||
|
with pytest.raises(LegacyArtifactError, match="ambiguous"):
|
||||||
|
decode_legacy_artifact(json.dumps(embedded).encode(), schema_version=LEGACY_TRACE_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_reader_refuses_duplicate_json_fields_and_bytes_subclasses() -> None:
|
||||||
|
duplicated = b'{"entry":"shadow",' + (FIXTURES / "trace.json").read_bytes()[1:]
|
||||||
|
with pytest.raises(LegacyArtifactError, match="duplicate"):
|
||||||
|
decode_legacy_artifact(duplicated, schema_version=LEGACY_TRACE_SCHEMA)
|
||||||
|
|
||||||
|
class HostileBytes(bytes):
|
||||||
|
def __len__(self) -> int:
|
||||||
|
raise AssertionError("subclass methods must not be observed")
|
||||||
|
|
||||||
|
with pytest.raises(LegacyArtifactError, match="exact bytes"):
|
||||||
|
decode_legacy_artifact(HostileBytes((FIXTURES / "trace.json").read_bytes()), schema_version=LEGACY_TRACE_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("fixture", "schema", "mutation"),
|
||||||
|
(
|
||||||
|
("trace.json", "trace-v1", lambda value: {**value, "unknown": True}),
|
||||||
|
("trace.json", "trace-v1", lambda value: {**value, "judgment": "clean"}),
|
||||||
|
("report.json", "admission-report-v1", lambda value: {**value, "decision": "MAYBE"}),
|
||||||
|
("report.json", "admission-report-v1", lambda value: {key: item for key, item in value.items() if key != "manifest"}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_legacy_reader_refuses_open_or_invalid_historical_shapes(
|
||||||
|
fixture: str,
|
||||||
|
schema: str,
|
||||||
|
mutation: Callable[[dict[str, object]], dict[str, object]],
|
||||||
|
) -> None:
|
||||||
|
value = json.loads((FIXTURES / fixture).read_bytes())
|
||||||
|
|
||||||
|
with pytest.raises(LegacyArtifactError):
|
||||||
|
decode_legacy_artifact(json.dumps(mutation(value)).encode(), schema_version=schema)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_reader_refuses_empty_or_unreadable_input(tmp_path: Path) -> None:
|
||||||
|
empty = tmp_path / "empty.json"
|
||||||
|
empty.write_bytes(b"")
|
||||||
|
|
||||||
|
with pytest.raises(LegacyArtifactError, match="nonempty"):
|
||||||
|
read_legacy_artifact(empty, schema_version=LEGACY_TRACE_SCHEMA)
|
||||||
|
with pytest.raises(LegacyArtifactError, match="readable"):
|
||||||
|
read_legacy_artifact(tmp_path / "missing.json", schema_version=LEGACY_TRACE_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_decoder_has_no_compatibility_or_v2_domain_dependency() -> None:
|
||||||
|
path = REPO / "src/openultrasast/core/legacy.py"
|
||||||
|
content = path.read_bytes()
|
||||||
|
assert content, "the import-boundary instrument must read nonzero decoder bytes"
|
||||||
|
tree = ast.parse(content, filename=str(path.relative_to(REPO)))
|
||||||
|
modules = {node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) and node.module is not None} | {
|
||||||
|
alias.name for node in ast.walk(tree) if isinstance(node, ast.Import) for alias in node.names
|
||||||
|
}
|
||||||
|
|
||||||
|
assert "openultrasast.core.legacy_v1" not in modules
|
||||||
|
assert "openultrasast.core.evidence" not in modules
|
||||||
|
assert modules & {"openultrasast.core.canonical"} == {"openultrasast.core.canonical"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_reader_is_not_reexported_from_the_public_core_api() -> None:
|
||||||
|
import openultrasast.core as core
|
||||||
|
|
||||||
|
assert not ({"LegacyArtifact", "decode_legacy_artifact", "read_legacy_artifact"} & set(core.__all__))
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
"""Immutable worklist tests for the detection-core v1 migration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import replace
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import core_migration_audit
|
||||||
|
import pytest
|
||||||
|
from core_migration_audit import audit_legacy_readers, tracked_python
|
||||||
|
from core_migration_inventory import (
|
||||||
|
REPO,
|
||||||
|
LegacyInventoryError,
|
||||||
|
MigrationDisposition,
|
||||||
|
entry_identity,
|
||||||
|
load_migration_inventory,
|
||||||
|
validate_historical_artifacts,
|
||||||
|
validate_inventory_sources,
|
||||||
|
validate_inventory_transition,
|
||||||
|
)
|
||||||
|
|
||||||
|
INVENTORY = REPO / "tests/fixtures/detection_core/v1/migration_inventory.json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("source", "expected"),
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"import openultrasast.core.probes as legacy_probes\nresult = legacy_probes.LegacyProbeResult\n",
|
||||||
|
"module_attr:openultrasast.core.probes:LegacyProbeResult",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"from openultrasast.core import budget as legacy_budget\nresult = legacy_budget.Budget\n",
|
||||||
|
"module_attr:openultrasast.core.budget:Budget",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_reader_audit_covers_direct_legacy_submodule_bindings(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, source: str, expected: str
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(core_migration_audit, "REPO", tmp_path)
|
||||||
|
path = tmp_path / "reader.py"
|
||||||
|
path.write_text(source, encoding="utf-8")
|
||||||
|
|
||||||
|
report = audit_legacy_readers((path,))
|
||||||
|
|
||||||
|
assert report.files_read == 1 and report.bytes_read == len(source.encode())
|
||||||
|
assert report.capabilities == (("reader.py", expected),)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reader_audit_covers_real_direct_legacy_submodule_reads() -> None:
|
||||||
|
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__"),
|
||||||
|
}
|
||||||
|
paths = tuple(REPO / path for path in sorted({path for path, _ in expected}))
|
||||||
|
|
||||||
|
report = audit_legacy_readers(paths)
|
||||||
|
|
||||||
|
assert report.files_read == 3 and report.bytes_read > 0
|
||||||
|
assert expected <= set(report.capabilities)
|
||||||
|
|
||||||
|
|
||||||
|
def test_frozen_inventory_resolves_nonzero_sources_and_targets() -> None:
|
||||||
|
content = INVENTORY.read_bytes()
|
||||||
|
assert content, "the inventory instrument must read nonzero bytes"
|
||||||
|
|
||||||
|
inventory = load_migration_inventory(INVENTORY)
|
||||||
|
|
||||||
|
assert inventory.entries
|
||||||
|
assert all(entry.disposition is MigrationDisposition.OPEN for entry in inventory.entries)
|
||||||
|
assert all(entry.entry_id == entry_identity(entry) for entry in inventory.entries)
|
||||||
|
validate_inventory_sources(inventory, root=REPO)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_covers_known_live_legacy_boundaries() -> None:
|
||||||
|
inventory = load_migration_inventory(INVENTORY)
|
||||||
|
registered_readers = {entry.path for entry in inventory.entries if entry.dependency_kind.value == "reader"}
|
||||||
|
registered_writers = {entry.path for entry in inventory.entries if entry.dependency_kind.value == "writer"}
|
||||||
|
|
||||||
|
assert registered_readers == {
|
||||||
|
"benchmarks/valid/arm_regression.py",
|
||||||
|
"benchmarks/valid/prover_admission.py",
|
||||||
|
"src/openultrasast/core/__init__.py",
|
||||||
|
"src/openultrasast/core/consumer.py",
|
||||||
|
"src/openultrasast/core/factpack.py",
|
||||||
|
"src/openultrasast/core/trace.py",
|
||||||
|
"src/openultrasast/hunt/prover.py",
|
||||||
|
"src/openultrasast/semantic/obligations/core_adapter.py",
|
||||||
|
"tests/test_core_admission.py",
|
||||||
|
"tests/test_core_budget.py",
|
||||||
|
"tests/test_core_budget_v2.py",
|
||||||
|
"tests/test_core_contract.py",
|
||||||
|
"tests/test_core_coverage_v2.py",
|
||||||
|
"tests/test_core_factpack_consumer.py",
|
||||||
|
"tests/test_core_factpack_file.py",
|
||||||
|
"tests/test_core_native.py",
|
||||||
|
"tests/test_core_probes.py",
|
||||||
|
"tests/test_core_validation.py",
|
||||||
|
}
|
||||||
|
assert registered_writers == {
|
||||||
|
"benchmarks/learn/engine_task_entry.py",
|
||||||
|
"benchmarks/measurements/2026-10-14-access-control-hunt/analyse.py",
|
||||||
|
"benchmarks/unseen/calibrate.py",
|
||||||
|
"benchmarks/unseen/replay.py",
|
||||||
|
"benchmarks/unseen/score.py",
|
||||||
|
"benchmarks/valid/admission_run.py",
|
||||||
|
"benchmarks/valid/arm_regression.py",
|
||||||
|
"benchmarks/valid/development_runner.py",
|
||||||
|
"benchmarks/valid/hunter_arm.py",
|
||||||
|
"benchmarks/valid/judge_pass.py",
|
||||||
|
"benchmarks/valid/metrics.py",
|
||||||
|
"benchmarks/valid/prover_arm.py",
|
||||||
|
"benchmarks/valid/semgrep_arm.py",
|
||||||
|
"ops/smoke_admission.py",
|
||||||
|
"ops/smoke_report.py",
|
||||||
|
"src/openultrasast/core/_legacy_factpack.py",
|
||||||
|
"src/openultrasast/core/consumer.py",
|
||||||
|
"src/openultrasast/core/probes.py",
|
||||||
|
"src/openultrasast/core/trace.py",
|
||||||
|
"src/openultrasast/hunt/prover.py",
|
||||||
|
"src/openultrasast/hunt/report.py",
|
||||||
|
"src/openultrasast/hunt/runner.py",
|
||||||
|
"src/openultrasast/push/runner.py",
|
||||||
|
"src/openultrasast/semantic/obligations/core_adapter.py",
|
||||||
|
"tests/test_admission_scoring.py",
|
||||||
|
"tests/test_arm_regression.py",
|
||||||
|
"tests/test_core_contract.py",
|
||||||
|
"tests/test_core_factpack_consumer.py",
|
||||||
|
"tests/test_development_runner.py",
|
||||||
|
"tests/test_four_arm.py",
|
||||||
|
"tests/test_hunt_ablation.py",
|
||||||
|
"tests/test_hunt_artifact.py",
|
||||||
|
"tests/test_hunt_contamination.py",
|
||||||
|
"tests/test_hunter_arm.py",
|
||||||
|
"tests/test_obligation_judgment.py",
|
||||||
|
"tests/test_obligation_trace.py",
|
||||||
|
"tests/test_push_admission.py",
|
||||||
|
"tests/test_push_report.py",
|
||||||
|
"tests/test_runner_gates.py",
|
||||||
|
"tests/test_semgrep_arm.py",
|
||||||
|
"tests/test_unseen_calibrate.py",
|
||||||
|
"tests/test_unseen_proof.py",
|
||||||
|
"tests/test_unseen_replay.py",
|
||||||
|
"tests/test_unseen_score.py",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_registers_the_active_legacy_probe_writer() -> None:
|
||||||
|
inventory = load_migration_inventory(INVENTORY)
|
||||||
|
|
||||||
|
assert any(
|
||||||
|
entry.path == "src/openultrasast/core/probes.py"
|
||||||
|
and entry.symbol == "reversed_pair_probe"
|
||||||
|
and entry.dependency_kind.value == "writer"
|
||||||
|
for entry in inventory.entries
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_independent_reader_audit_matches_split_inventory_capabilities() -> None:
|
||||||
|
report = audit_legacy_readers(tracked_python())
|
||||||
|
inventory = load_migration_inventory(INVENTORY)
|
||||||
|
reader_entries = tuple(entry for entry in inventory.entries if entry.dependency_kind.value == "reader")
|
||||||
|
registered = {(entry.path, anchor) for entry in reader_entries for anchor in entry.capability_anchors}
|
||||||
|
|
||||||
|
assert report.files_read > 0 and report.bytes_read > 0
|
||||||
|
assert ("tests/test_core_probes.py", "top_level:ProbeResult") in report.capabilities
|
||||||
|
assert all(len(entry.capability_anchors) == 1 for entry in reader_entries)
|
||||||
|
assert registered == set(report.capabilities)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_targets_require_exact_capability_anchors(tmp_path: Path) -> None:
|
||||||
|
frozen = load_migration_inventory(INVENTORY)
|
||||||
|
entry = frozen.entries[0]
|
||||||
|
assert entry.capability_anchors
|
||||||
|
substituted = tmp_path / entry.path
|
||||||
|
substituted.parent.mkdir(parents=True)
|
||||||
|
substituted.write_text("def unrelated():\n return True\n", encoding="utf-8")
|
||||||
|
|
||||||
|
with pytest.raises(LegacyInventoryError, match="capability"):
|
||||||
|
validate_inventory_sources(replace(frozen, entries=(entry,)), root=tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_historical_artifact_manifest_pins_exact_nonzero_bytes() -> None:
|
||||||
|
inventory = load_migration_inventory(INVENTORY)
|
||||||
|
|
||||||
|
artifacts = validate_historical_artifacts(inventory, root=REPO)
|
||||||
|
|
||||||
|
assert {artifact.schema_version for artifact in artifacts} == {"trace-v1", "admission-report-v1"}
|
||||||
|
assert all(artifact.byte_length > 0 for artifact in artifacts)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("terminal", (MigrationDisposition.V2_MIGRATED, MigrationDisposition.RETIRED))
|
||||||
|
def test_inventory_entries_close_monotonically_without_identity_changes(terminal: MigrationDisposition) -> None:
|
||||||
|
frozen = load_migration_inventory(INVENTORY)
|
||||||
|
changed_entry = replace(frozen.entries[0], disposition=terminal)
|
||||||
|
candidate = replace(frozen, entries=(changed_entry, *frozen.entries[1:]))
|
||||||
|
|
||||||
|
validate_inventory_transition(frozen, candidate)
|
||||||
|
|
||||||
|
with pytest.raises(LegacyInventoryError, match="terminal"):
|
||||||
|
validate_inventory_transition(candidate, frozen)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_transition_refuses_runtime_invalid_disposition() -> None:
|
||||||
|
frozen = load_migration_inventory(INVENTORY)
|
||||||
|
invalid = replace(frozen.entries[0], disposition="closed") # type: ignore[arg-type]
|
||||||
|
candidate = replace(frozen, entries=(invalid, *frozen.entries[1:]))
|
||||||
|
|
||||||
|
with pytest.raises(LegacyInventoryError, match="disposition"):
|
||||||
|
validate_inventory_transition(frozen, candidate)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mutation", ("add", "delete", "rekey"))
|
||||||
|
def test_inventory_refuses_additions_deletions_and_rekeying(mutation: str) -> None:
|
||||||
|
frozen = load_migration_inventory(INVENTORY)
|
||||||
|
if mutation == "add":
|
||||||
|
added = replace(
|
||||||
|
frozen.entries[0],
|
||||||
|
entry_id="f" * 64,
|
||||||
|
path="src/openultrasast/core/canonical.py",
|
||||||
|
symbol="<module>",
|
||||||
|
)
|
||||||
|
candidate = replace(frozen, entries=(*frozen.entries, added))
|
||||||
|
elif mutation == "delete":
|
||||||
|
candidate = replace(frozen, entries=frozen.entries[1:])
|
||||||
|
else:
|
||||||
|
changed = replace(frozen.entries[0], symbol="different_target")
|
||||||
|
changed = replace(changed, entry_id=entry_identity(changed))
|
||||||
|
candidate = replace(frozen, entries=(changed, *frozen.entries[1:]))
|
||||||
|
|
||||||
|
with pytest.raises(LegacyInventoryError, match="identities"):
|
||||||
|
validate_inventory_transition(frozen, candidate)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("field", "value", "message"),
|
||||||
|
(("schema_version", "v2", "schema"), ("disposition", "closed", "disposition")),
|
||||||
|
)
|
||||||
|
def test_inventory_refuses_schema_and_status_mutations(tmp_path: Path, field: str, value: str, message: str) -> None:
|
||||||
|
document = json.loads(INVENTORY.read_bytes())
|
||||||
|
if field == "schema_version":
|
||||||
|
document[field] = value
|
||||||
|
else:
|
||||||
|
document["entries"][0][field] = value
|
||||||
|
changed = tmp_path / "migration_inventory.json"
|
||||||
|
changed.write_text(json.dumps(document), encoding="utf-8")
|
||||||
|
|
||||||
|
with pytest.raises(LegacyInventoryError, match=message):
|
||||||
|
load_migration_inventory(changed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_refuses_unreadable_zero_and_unknown_targets(tmp_path: Path) -> None:
|
||||||
|
frozen = load_migration_inventory(INVENTORY)
|
||||||
|
original = frozen.entries[0]
|
||||||
|
|
||||||
|
missing = replace(original, path="missing.py", symbol="<module>")
|
||||||
|
missing = replace(missing, entry_id=entry_identity(missing))
|
||||||
|
with pytest.raises(LegacyInventoryError, match="readable"):
|
||||||
|
validate_inventory_sources(replace(frozen, entries=(missing,)), root=tmp_path)
|
||||||
|
|
||||||
|
empty_path = tmp_path / "empty.py"
|
||||||
|
empty_path.write_bytes(b"")
|
||||||
|
empty = replace(original, path="empty.py", symbol="<module>")
|
||||||
|
empty = replace(empty, entry_id=entry_identity(empty))
|
||||||
|
with pytest.raises(LegacyInventoryError, match="nonzero"):
|
||||||
|
validate_inventory_sources(replace(frozen, entries=(empty,)), root=tmp_path)
|
||||||
|
|
||||||
|
source_path = tmp_path / "source.py"
|
||||||
|
source_path.write_text("def present():\n return True\n", encoding="utf-8")
|
||||||
|
unknown = replace(original, path="source.py", symbol="absent")
|
||||||
|
unknown = replace(unknown, entry_id=entry_identity(unknown))
|
||||||
|
with pytest.raises(LegacyInventoryError, match="target"):
|
||||||
|
validate_inventory_sources(replace(frozen, entries=(unknown,)), root=tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_refuses_changed_artifact_bytes(tmp_path: Path) -> None:
|
||||||
|
frozen = load_migration_inventory(INVENTORY)
|
||||||
|
artifact = frozen.historical_artifacts[0]
|
||||||
|
target = tmp_path / artifact.path
|
||||||
|
target.parent.mkdir(parents=True)
|
||||||
|
target.write_bytes(b"{}\n")
|
||||||
|
|
||||||
|
with pytest.raises(LegacyInventoryError, match="digest|length"):
|
||||||
|
validate_historical_artifacts(replace(frozen, historical_artifacts=(artifact,)), root=tmp_path)
|
||||||
Reference in New Issue
Block a user