feat(lab): measure RAVNOVES00 R1 quality baseline

This commit is contained in:
DCCONSTRUCTIONS
2026-07-28 03:42:32 +03:00
parent e35f81ac34
commit c018f2b074
20 changed files with 2401 additions and 11 deletions
@@ -0,0 +1,830 @@
"""Development-trained RAVNOVES00 R1 perception-quality baseline.
E38 consumes the frozen E37 contract without changing its denominator or
validation split. A small deterministic decision tree is fitted only on the
development partition and is then evaluated once on the sealed validation
partition. The result remains source-scoped and diagnostic.
"""
from __future__ import annotations
import hashlib
import json
import math
import os
import shutil
import uuid
from collections import Counter, defaultdict
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final
from k1link.compute.e37_acceptance_contract import (
E37_ITEMS_NAME,
E37AcceptanceContractError,
read_e37_acceptance_contract,
)
E38_PROFILE_SCHEMA: Final = "missioncore.e38-perception-baseline-profile/v1"
E38_PACKAGE_SCHEMA: Final = "missioncore.e38-worker-package/v1"
E38_RESULT_SCHEMA: Final = "missioncore.e38-perception-baseline/v1"
E38_PREDICTION_SCHEMA: Final = "missioncore.e38-perception-prediction/v1"
E38_MODEL_SCHEMA: Final = "missioncore.e38-development-cart-model/v1"
E38_REPORT_SCHEMA: Final = "missioncore.e38-perception-baseline-report/v1"
E38_PREDICTIONS_NAME: Final = "predictions.jsonl"
E38_MODEL_NAME: Final = "development-model.json"
E38_REPORT_NAME: Final = "run-report.json"
E38_MANIFEST_NAME: Final = "manifest.json"
_MATERIALIZATION_SCHEMA: Final = "missioncore.e30-evidence-materialization/v2"
_SOURCE_SESSION_ID: Final = "20260720T065719Z_viewer_live"
_SOURCE_DISPLAY_NAME: Final = "RAVNOVES00"
_DIMENSIONS: Final = ("presence", "geometry_association", "freshness")
_CATEGORICAL_FEATURES: Final = {
"stratum": ("agree", "camera-only", "conflict", "geometry-only", "unknown"),
"range": ("near", "middle", "far", "unavailable"),
"geometry": (
"agree",
"conflict",
"single-source-camera",
"single-source-geometry",
"unknown",
"unavailable",
),
}
_AUTHORITY: Final = {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
class E38PerceptionBaselineError(RuntimeError):
"""The E38 profile, immutable inputs, or result is invalid."""
@dataclass(frozen=True, slots=True)
class E38PerceptionBaseline:
result_id: str
result_root: Path
manifest: dict[str, Any]
report: dict[str, Any]
model: dict[str, Any]
@property
def quality_gate_passed(self) -> bool:
return self.report.get("quality_gate", {}).get("passed") is True
def build_e38_perception_baseline(
*,
acceptance_root: Path,
materialization_root: Path,
profile_path: Path,
output_root: Path,
worker_node: str | None = None,
) -> E38PerceptionBaseline:
"""Fit on E37 development rows and evaluate the sealed validation rows."""
profile_file = profile_path.resolve(strict=True)
profile = _read_json(profile_file)
_validate_profile(profile)
acceptance_path = acceptance_root.resolve(strict=True)
materialization_path = materialization_root.resolve(strict=True)
try:
acceptance = read_e37_acceptance_contract(acceptance_path)
except E37AcceptanceContractError as exc:
raise E38PerceptionBaselineError("E38 E37 contract is invalid") from exc
if acceptance.result_id != profile["source"]["acceptance_result_id"]:
raise E38PerceptionBaselineError("E38 acceptance identity changed")
acceptance_items_path = acceptance_path / E37_ITEMS_NAME
acceptance_rows = _read_jsonl(acceptance_items_path)
materialization_rows, materialization_binding = _load_materialization(
materialization_path,
acceptance.manifest,
profile,
)
if (
len(acceptance_rows) != 486
or len(materialization_rows) != 486
or {row.get("item_id") for row in acceptance_rows}
!= {row.get("item_id") for row in materialization_rows}
):
raise E38PerceptionBaselineError("E38 denominator accounting differs")
identity = {
"schema_version": E38_RESULT_SCHEMA,
"source": {
"session_id": _SOURCE_SESSION_ID,
"display_name": _SOURCE_DISPLAY_NAME,
"acceptance_result_id": acceptance.result_id,
"acceptance_identity_sha256": acceptance.manifest["identity_sha256"],
"acceptance_items_sha256": _sha256(acceptance_items_path),
**materialization_binding,
},
"profile": {
"profile_id": profile["profile_id"],
"sha256": _sha256(profile_file),
},
"execution": {
"class": "deterministic-development-trained-validation-evaluation",
"worker_node": worker_node or "unbound-local",
},
"authority": _AUTHORITY,
}
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
result_id = f"e38-perception-baseline-{identity_sha256}"
destination = output_root.expanduser().absolute() / result_id
if destination.exists():
return read_e38_perception_baseline(destination)
materialization_by_id = {
str(row["item_id"]): row for row in materialization_rows
}
joined: list[dict[str, Any]] = []
for acceptance_row in acceptance_rows:
item_id = str(acceptance_row.get("item_id"))
reference = acceptance_row.get("reference")
if (
acceptance_row.get("split") not in {"development", "validation"}
or acceptance_row.get("severity") not in {"standard", "medium", "high"}
or not isinstance(reference, dict)
or set(reference) != set(_DIMENSIONS)
):
raise E38PerceptionBaselineError("E38 acceptance row is invalid")
joined.append(
{
"acceptance": acceptance_row,
"features": _feature_vector(
acceptance_row,
materialization_by_id[item_id],
),
}
)
model_dimensions: dict[str, Any] = {}
for dimension in _DIMENSIONS:
dimension_profile = profile["model"]["dimensions"][dimension]
feature_names = _dimension_features(
joined,
include_source_time=dimension_profile["include_source_time"],
)
development = [
(
_selected_features(row["features"], feature_names),
str(row["acceptance"]["reference"][dimension]),
)
for row in joined
if row["acceptance"]["split"] == "development"
]
tree = _train_tree(
development,
feature_names=feature_names,
max_depth=int(dimension_profile["max_depth"]),
min_leaf=int(dimension_profile["min_leaf"]),
)
model_dimensions[dimension] = {
"feature_names": feature_names,
"max_depth": dimension_profile["max_depth"],
"min_leaf": dimension_profile["min_leaf"],
"tree": tree,
}
predictions: list[dict[str, Any]] = []
for row in joined:
acceptance_row = row["acceptance"]
predicted = {
dimension: _predict_tree(
model_dimensions[dimension]["tree"],
row["features"],
)
for dimension in _DIMENSIONS
}
predictions.append(
{
"schema_version": E38_PREDICTION_SCHEMA,
"sequence": acceptance_row["sequence"],
"item_id": acceptance_row["item_id"],
"review_key": acceptance_row["review_key"],
"source_frame_index": acceptance_row["source_frame_index"],
"source_stratum": acceptance_row["source_stratum"],
"severity": acceptance_row["severity"],
"split": acceptance_row["split"],
"prediction": predicted,
"reference": acceptance_row["reference"],
"scored": acceptance_row["split"] == "validation",
"authority": _AUTHORITY,
}
)
validation_predictions = [
row for row in predictions if row["split"] == "validation"
]
development_count = sum(row["split"] == "development" for row in predictions)
validation_count = len(validation_predictions)
if development_count != 340 or validation_count != 146:
raise E38PerceptionBaselineError("E38 frozen split changed")
dimension_metrics = {
dimension: _dimension_metrics(
validation_predictions,
dimension=dimension,
target=float(profile["targets"][f"{dimension}_target"]),
)
for dimension in _DIMENSIONS
}
high_severity_failures = sum(
row["severity"] == "high"
and any(
row["prediction"][dimension] != row["reference"][dimension]
for dimension in _DIMENSIONS
)
for row in validation_predictions
)
accounting_fraction = (
len(validation_predictions) / validation_count if validation_count else 0.0
)
false_free_claims = sum(
value == "free"
for row in predictions
for value in row["prediction"].values()
)
gate_checks = {
"presence_target_reached": dimension_metrics["presence"]["passed"],
"geometry_association_target_reached": dimension_metrics[
"geometry_association"
]["passed"],
"freshness_target_reached": dimension_metrics["freshness"]["passed"],
"validation_accounting_complete": math.isclose(accounting_fraction, 1.0),
"false_free_claims_zero": false_free_claims == 0,
"high_severity_failures_zero": high_severity_failures == 0,
"authority_remains_diagnostic": True,
}
quality_gate_passed = all(gate_checks.values())
model_document = {
"schema_version": E38_MODEL_SCHEMA,
"profile_id": profile["profile_id"],
"training_split": "development",
"training_items": development_count,
"validation_labels_used_for_training": False,
"dimensions": model_dimensions,
"authority": _AUTHORITY,
}
report = {
"schema_version": E38_REPORT_SCHEMA,
"result_id": result_id,
"identity_sha256": identity_sha256,
"status": "measured-r1-source-scoped-baseline",
"source_session_id": _SOURCE_SESSION_ID,
"source_display_name": _SOURCE_DISPLAY_NAME,
"profile_id": profile["profile_id"],
"execution": identity["execution"],
"metrics": {
"development_items": development_count,
"validation_items": validation_count,
"terminal_outcomes": validation_count,
"accounting_fraction": round(accounting_fraction, 6),
"false_free_claims": false_free_claims,
"high_severity_failures": high_severity_failures,
"dimensions": dimension_metrics,
},
"quality_gate": {
"passed": quality_gate_passed,
"checks": gate_checks,
"blocking_checks": [
name for name, passed in gate_checks.items() if not passed
],
},
"decision": {
"r1_baseline_measured": True,
"accepted_for_release": quality_gate_passed,
"next_gate": (
"R2 source-scoped temporal product state"
if quality_gate_passed
else "R1 detector presence and geometry-association improvement"
),
},
"limitations": [
(
"the model is trained and evaluated only on the source-scoped "
"RAVNOVES00 engineering-reviewed contract"
),
"validation labels are evaluation-only and never used to fit a tree",
(
"the result is not independent ground truth and does not prove "
"another route, camera, rig or mount"
),
"navigation, command and safety authority remain false",
],
"authority": _AUTHORITY,
}
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
staging.mkdir(mode=0o700, exist_ok=False)
try:
_write_jsonl(staging / E38_PREDICTIONS_NAME, predictions)
_write_json(staging / E38_MODEL_NAME, model_document)
_write_json(staging / E38_REPORT_NAME, report)
artifacts = [
_artifact(staging / E38_PREDICTIONS_NAME, "sealed-evaluation"),
_artifact(staging / E38_MODEL_NAME, "development-trained-model"),
_artifact(staging / E38_REPORT_NAME, "quality-report"),
]
manifest = {
"schema_version": E38_RESULT_SCHEMA,
"result_id": result_id,
"identity_sha256": identity_sha256,
"identity": identity,
"created_at_utc": _utc_now(),
"acceptance_state": "completed-r1-source-scoped-baseline",
"quality_gate_passed": quality_gate_passed,
"artifacts": artifacts,
}
_write_json(staging / E38_MANIFEST_NAME, manifest)
os.replace(staging, destination)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
return read_e38_perception_baseline(destination)
def read_e38_perception_baseline(root: Path) -> E38PerceptionBaseline:
"""Validate and read one immutable E38 result."""
resolved = root.resolve(strict=True)
manifest = _read_json(resolved / E38_MANIFEST_NAME)
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
if (
manifest.get("schema_version") != E38_RESULT_SCHEMA
or not isinstance(identity, dict)
or not isinstance(identity_sha256, str)
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or manifest.get("result_id") != f"e38-perception-baseline-{identity_sha256}"
or resolved.name != manifest.get("result_id")
or manifest.get("acceptance_state")
!= "completed-r1-source-scoped-baseline"
or identity.get("authority") != _AUTHORITY
):
raise E38PerceptionBaselineError("E38 result identity is invalid")
expected = {
E38_PREDICTIONS_NAME: "sealed-evaluation",
E38_MODEL_NAME: "development-trained-model",
E38_REPORT_NAME: "quality-report",
}
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list) or len(artifacts) != len(expected):
raise E38PerceptionBaselineError("E38 artifact catalog is invalid")
for row in artifacts:
if not isinstance(row, dict):
raise E38PerceptionBaselineError("E38 artifact descriptor is invalid")
name = row.get("path")
path = resolved / str(name)
if (
name not in expected
or row.get("role") != expected[name]
or not path.is_file()
or path.is_symlink()
or row.get("byte_length") != path.stat().st_size
or row.get("sha256") != _sha256(path)
):
raise E38PerceptionBaselineError("E38 artifact content changed")
report = _read_json(resolved / E38_REPORT_NAME)
model = _read_json(resolved / E38_MODEL_NAME)
if (
report.get("schema_version") != E38_REPORT_SCHEMA
or report.get("result_id") != resolved.name
or report.get("identity_sha256") != identity_sha256
or report.get("status") != "measured-r1-source-scoped-baseline"
or model.get("schema_version") != E38_MODEL_SCHEMA
or model.get("validation_labels_used_for_training") is not False
or manifest.get("quality_gate_passed")
is not report.get("quality_gate", {}).get("passed")
):
raise E38PerceptionBaselineError("E38 report or model is invalid")
return E38PerceptionBaseline(
result_id=resolved.name,
result_root=resolved,
manifest=manifest,
report=report,
model=model,
)
def _load_materialization(
root: Path,
acceptance_manifest: dict[str, Any],
profile: dict[str, Any],
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
manifest_path = root / "manifest.json"
index_path = root / "materialized-items.jsonl"
manifest = _read_json(manifest_path)
binding = (
acceptance_manifest.get("identity", {})
.get("reviewed_substrate", {})
)
if (
manifest.get("schema_version") != _MATERIALIZATION_SCHEMA
or root.name != profile["source"]["materialization_id"]
or manifest.get("result_id") != root.name
or root.name != binding.get("materialization_id")
or manifest.get("identity_sha256")
!= binding.get("materialization_identity_sha256")
or _sha256(manifest_path)
!= binding.get("materialization_manifest_sha256")
or not index_path.is_file()
or index_path.is_symlink()
or _sha256(index_path) != binding.get("materialization_index_sha256")
):
raise E38PerceptionBaselineError("E38 materialization identity changed")
rows = _read_jsonl(index_path)
return rows, {
"materialization_id": root.name,
"materialization_identity_sha256": manifest["identity_sha256"],
"materialization_index_sha256": _sha256(index_path),
}
def _feature_vector(
acceptance: dict[str, Any],
materialization: dict[str, Any],
) -> dict[str, float]:
snapshot = _object(materialization.get("e29_snapshot"), "E38 snapshot")
evidence = _object(
materialization.get("materialization"),
"E38 materialization evidence",
)
bounds = snapshot.get("bounds_map_xyz_m")
height = snapshot.get("height_range_m")
features = {
"detector_score": _number_or(evidence.get("detector_score"), -1.0),
"selected_points": _number_or(evidence.get("selected_point_count"), 0.0),
"rejected_points": _number_or(
evidence.get("rejected_candidate_point_count"),
0.0,
),
"candidate_points": _number_or(evidence.get("candidate_point_count"), 0.0),
"camera_front_points": _number_or(
evidence.get("camera_front_point_count"),
0.0,
),
"projected_points": _number_or(
evidence.get("projected_point_count"),
0.0,
),
"frame_points": _number_or(evidence.get("frame_point_count"), 0.0),
"point_count": _number_or(snapshot.get("point_count"), 0.0),
"voxel_count": _number_or(snapshot.get("voxel_count"), 0.0),
"nearest_range_m": _number_or(snapshot.get("nearest_range_m"), -1.0),
"height_span_m": _span(height, 0),
"bounds_span_x_m": _span(bounds, 0),
"bounds_span_y_m": _span(bounds, 1),
"bounds_span_z_m": _span(bounds, 2),
"session_seconds": _number_or(acceptance.get("session_seconds"), -1.0),
"source_frame_index": _number_or(
acceptance.get("source_frame_index"),
-1.0,
),
}
categorical = {
"stratum": materialization.get("stratum"),
"range": materialization.get("range_bucket"),
"geometry": snapshot.get("geometry_status"),
}
for prefix, values in _CATEGORICAL_FEATURES.items():
observed = str(categorical[prefix])
for value in values:
features[f"{prefix}={value}"] = float(observed == value)
return features
def _dimension_features(
rows: list[dict[str, Any]],
*,
include_source_time: bool,
) -> list[str]:
features = sorted(
{
name
for row in rows
for name in row["features"]
if include_source_time
or name not in {"session_seconds", "source_frame_index"}
}
)
if not features:
raise E38PerceptionBaselineError("E38 feature set is empty")
return features
def _selected_features(
values: dict[str, float],
names: list[str],
) -> dict[str, float]:
return {name: values.get(name, 0.0) for name in names}
def _train_tree(
rows: list[tuple[dict[str, float], str]],
*,
feature_names: list[str],
max_depth: int,
min_leaf: int,
depth: int = 0,
) -> dict[str, Any]:
if not rows:
raise E38PerceptionBaselineError("E38 tree has no training rows")
labels = [label for _, label in rows]
prediction = sorted(Counter(labels).items(), key=lambda item: (-item[1], item[0]))[
0
][0]
node: dict[str, Any] = {
"prediction": prediction,
"samples": len(rows),
"distribution": dict(sorted(Counter(labels).items())),
}
if (
depth >= max_depth
or len(set(labels)) == 1
or len(rows) < 2 * min_leaf
):
return node
base_impurity = _gini(labels)
best: tuple[
float,
str,
float,
list[tuple[dict[str, float], str]],
list[tuple[dict[str, float], str]],
] | None = None
for feature in feature_names:
values = sorted({float(features.get(feature, 0.0)) for features, _ in rows})
for left_value, right_value in zip(values, values[1:], strict=False):
threshold = (left_value + right_value) / 2.0
left_rows = [
row for row in rows if float(row[0].get(feature, 0.0)) <= threshold
]
right_rows = [
row for row in rows if float(row[0].get(feature, 0.0)) > threshold
]
if len(left_rows) < min_leaf or len(right_rows) < min_leaf:
continue
impurity = (
len(left_rows) * _gini([label for _, label in left_rows])
+ len(right_rows) * _gini([label for _, label in right_rows])
) / len(rows)
gain = base_impurity - impurity
candidate = (gain, feature, threshold, left_rows, right_rows)
if best is None or _better_split(candidate, best):
best = candidate
if best is None or best[0] <= 1e-12:
return node
node.update(
{
"feature": best[1],
"threshold": round(best[2], 12),
"left": _train_tree(
best[3],
feature_names=feature_names,
max_depth=max_depth,
min_leaf=min_leaf,
depth=depth + 1,
),
"right": _train_tree(
best[4],
feature_names=feature_names,
max_depth=max_depth,
min_leaf=min_leaf,
depth=depth + 1,
),
}
)
return node
def _better_split(
candidate: tuple[float, str, float, Any, Any],
current: tuple[float, str, float, Any, Any],
) -> bool:
if candidate[0] > current[0] + 1e-12:
return True
if abs(candidate[0] - current[0]) <= 1e-12:
return (candidate[1], candidate[2]) < (current[1], current[2])
return False
def _predict_tree(tree: dict[str, Any], features: dict[str, float]) -> str:
node = tree
while "feature" in node:
feature = str(node["feature"])
threshold = float(node["threshold"])
node = (
_object(node.get("left"), "E38 tree left")
if features.get(feature, 0.0) <= threshold
else _object(node.get("right"), "E38 tree right")
)
prediction = node.get("prediction")
if not isinstance(prediction, str) or not prediction:
raise E38PerceptionBaselineError("E38 tree prediction is invalid")
return prediction
def _dimension_metrics(
rows: list[dict[str, Any]],
*,
dimension: str,
target: float,
) -> dict[str, Any]:
confusion: Counter[tuple[str, str]] = Counter()
stratum_counts: dict[str, Counter[str]] = defaultdict(Counter)
correct = 0
for row in rows:
reference = str(row["reference"][dimension])
prediction = str(row["prediction"][dimension])
confusion[(reference, prediction)] += 1
matched = reference == prediction
correct += matched
stratum_counts[str(row["source_stratum"])][
"correct" if matched else "incorrect"
] += 1
total = len(rows)
accuracy = correct / total if total else 0.0
return {
"correct": correct,
"incorrect": total - correct,
"total": total,
"accuracy": round(accuracy, 6),
"target": target,
"passed": accuracy >= target,
"confusion": [
{
"reference": reference,
"prediction": prediction,
"count": count,
}
for (reference, prediction), count in sorted(
confusion.items(),
key=lambda item: (-item[1], item[0]),
)
],
"by_stratum": {
stratum: {
"correct": counts["correct"],
"incorrect": counts["incorrect"],
"total": sum(counts.values()),
"accuracy": round(
counts["correct"] / sum(counts.values()),
6,
),
}
for stratum, counts in sorted(stratum_counts.items())
},
}
def _gini(labels: list[str]) -> float:
counts = Counter(labels)
total = len(labels)
return 1.0 - sum((count / total) ** 2 for count in counts.values())
def _span(value: object, axis: int) -> float:
if not isinstance(value, list) or len(value) != 2:
return -1.0
if axis == 0 and all(isinstance(item, (int, float)) for item in value):
return float(value[1]) - float(value[0])
if not all(
isinstance(item, list)
and len(item) > axis
and isinstance(item[axis], (int, float))
for item in value
):
return -1.0
return float(value[1][axis]) - float(value[0][axis])
def _number_or(value: object, fallback: float) -> float:
if isinstance(value, (int, float)) and not isinstance(value, bool):
parsed = float(value)
if math.isfinite(parsed):
return parsed
return fallback
def _validate_profile(profile: dict[str, Any]) -> None:
source = _object(profile.get("source"), "E38 source")
model = _object(profile.get("model"), "E38 model")
dimensions = _object(model.get("dimensions"), "E38 model dimensions")
targets = _object(profile.get("targets"), "E38 targets")
if (
profile.get("schema_version") != E38_PROFILE_SCHEMA
or profile.get("profile_id")
!= "e38-ravnoves00-r1-development-cart/v1"
or source.get("session_id") != _SOURCE_SESSION_ID
or source.get("display_name") != _SOURCE_DISPLAY_NAME
or not isinstance(source.get("acceptance_result_id"), str)
or not str(source["acceptance_result_id"]).startswith(
"e37-ravnoves-acceptance-"
)
or not isinstance(source.get("materialization_id"), str)
or not str(source["materialization_id"]).startswith("e30-materialization-")
or model.get("type") != "deterministic-shallow-cart"
or set(dimensions) != set(_DIMENSIONS)
or any(
not isinstance(dimensions[name], dict)
or not isinstance(dimensions[name].get("include_source_time"), bool)
or not isinstance(dimensions[name].get("max_depth"), int)
or not 1 <= dimensions[name]["max_depth"] <= 8
or not isinstance(dimensions[name].get("min_leaf"), int)
or not 2 <= dimensions[name]["min_leaf"] <= 32
for name in _DIMENSIONS
)
or any(targets.get(f"{name}_target") != 0.9 for name in _DIMENSIONS)
or targets.get("accounting_target") != 1.0
or targets.get("maximum_false_free_claims") != 0
or profile.get("authority") != _AUTHORITY
):
raise E38PerceptionBaselineError("E38 profile contract changed")
def _artifact(path: Path, role: str) -> dict[str, Any]:
return {
"role": role,
"path": path.name,
"byte_length": path.stat().st_size,
"sha256": _sha256(path),
}
def _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise E38PerceptionBaselineError(f"{label} must be an object")
return value
def _read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8-sig"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise E38PerceptionBaselineError(f"invalid JSON: {path.name}") from exc
return _object(value, path.name)
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
try:
with path.open("r", encoding="utf-8-sig") as stream:
for line in stream:
rows.append(_object(json.loads(line), path.name))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise E38PerceptionBaselineError(f"invalid JSONL: {path.name}") from exc
return rows
def _write_json(path: Path, value: object) -> None:
with path.open("x", encoding="utf-8", newline="\n") as stream:
json.dump(value, stream, ensure_ascii=False, indent=2, sort_keys=True)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
def _write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None:
with path.open("x", encoding="utf-8", newline="\n") as stream:
for row in rows:
stream.write(
json.dumps(
row,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
)
)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _utc_now() -> str:
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")