File size: 23,300 Bytes
6342806
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
"""Control-plane registry: versioned entities for the whole evaluation system.

Entities (all persisted as JSON under runs/studio2/registry/<kind>/):

    models        ModelVersion       name/version/checkpoint + provenance
    datasets      DatasetVersion     role (TRAINING/VALIDATION/TEST/REGRESSION/
                                     LAUNCH/MONITORING), immutable lineage,
                                     role-transition rules with the raremine
                                     contamination-guard pattern
    scenarios     ScenarioVersion    generator + seed + recipe hash
    policies      PolicyVersion      content-hashed policy documents
    experiments   Experiment         candidate-vs-baseline grouping
    runs          EvaluationRun      full reproducibility tuple; runs missing
                                     any component are NON_REPRODUCIBLE
    safety_cases  SafetyCase         pointers to safety evidence packages /
                                     agentic scorecards
    decisions     ReleaseDecision    written by release_gate.py
    approvals     HumanApproval      the separate recorded human action

Auto-ingest adapters scan the existing runs/ stores (megaeval, seqeval,
safety evidence, agentic scorecards, bevfusion) and register entities
retroactively with provenance where derivable. Ingest is idempotent: entity
ids are content-derived from the source references.
"""

from __future__ import annotations

import hashlib
import json
import os
import threading
from typing import Dict, List, Optional

from sensorflow.studio2 import store

# ------------------------------------------------------------------ constants

KINDS = ("models", "datasets", "scenarios", "policies", "experiments",
         "runs", "safety_cases", "decisions", "approvals")

DATASET_ROLES = ("TRAINING", "VALIDATION", "TEST", "REGRESSION",
                 "LAUNCH", "MONITORING")

# Same semantics as raremine.models.PROTECTED_EVAL_DESTINATIONS: examples in
# these roles decide launches, so silently moving them across the
# training/evaluation boundary would corrupt every future measurement.
PROTECTED_EVAL_ROLES = frozenset({"TEST", "REGRESSION", "LAUNCH"})

# The reproducibility tuple. A run missing any component cannot be replayed
# bit-for-bit and is marked NON_REPRODUCIBLE.
REPRO_COMPONENTS = ("model_version_id", "dataset_version_id",
                    "scenario_version_id", "config_hash",
                    "calibration_version", "seed", "policy_version_id")

REPRODUCIBLE = "REPRODUCIBLE"
NON_REPRODUCIBLE = "NON_REPRODUCIBLE"


class RoleTransitionError(RuntimeError):
    """Raised on any attempt to move a dataset across the training/evaluation
    contamination boundary without an explicit governance override."""


def new_id(prefix: str, *seed_parts: str) -> str:
    """Deterministic when seed_parts are given (used by ingest for
    idempotency), random-ish otherwise."""
    if seed_parts:
        h = hashlib.sha256("|".join(seed_parts).encode()).hexdigest()[:10]
    else:
        h = hashlib.sha256(os.urandom(16)).hexdigest()[:10]
    return f"{prefix}-{h}"


def content_hash(doc: Dict, exclude: tuple = ("policy_version", "created_at")) -> str:
    blob = json.dumps({k: v for k, v in doc.items() if k not in exclude},
                      sort_keys=True, default=str)
    return hashlib.sha256(blob.encode()).hexdigest()[:16]


# ------------------------------------------------------------------ registry


class Registry:
    """Thin persisted entity store with the governance rules baked in."""

    def __init__(self):
        self._lock = threading.RLock()

    # -------------------------------------------------------------- storage

    def _path_parts(self, kind: str, entity_id: str):
        return ("registry", kind, f"{entity_id}.json")

    def put(self, kind: str, entity: Dict) -> Dict:
        if kind not in KINDS:
            raise KeyError(f"unknown entity kind {kind!r}; known: {KINDS}")
        entity = dict(entity)
        entity.setdefault("kind", kind)
        entity.setdefault("created_at", store.now_iso())
        store.write_json(entity, *self._path_parts(kind, entity["entity_id"]))
        return entity

    def get(self, kind: str, entity_id: str) -> Optional[Dict]:
        return store.read_json(*self._path_parts(kind, entity_id))

    def list(self, kind: str) -> List[Dict]:
        if kind not in KINDS:
            raise KeyError(f"unknown entity kind {kind!r}; known: {KINDS}")
        out = []
        for name in store.list_dir("registry", kind):
            if not name.endswith(".json"):
                continue
            doc = store.read_json("registry", kind, name)
            if doc:
                out.append(doc)
        out.sort(key=lambda e: e.get("created_at") or "", reverse=True)
        return out

    def counts(self) -> Dict[str, int]:
        return {kind: len(store.list_dir("registry", kind)) for kind in KINDS}

    # -------------------------------------------------------------- models

    def register_model(self, name: str, version: str, checkpoint: str = "",
                       provenance: Optional[Dict] = None,
                       meta: Optional[Dict] = None) -> Dict:
        entity_id = new_id("mv", name, version, checkpoint)
        existing = self.get("models", entity_id)
        if existing:
            return existing
        return self.put("models", {
            "entity_id": entity_id, "name": name, "version": version,
            "checkpoint": checkpoint, "provenance": provenance or {},
            "meta": meta or {}})

    # -------------------------------------------------------------- datasets

    def register_dataset(self, name: str, role: str,
                         lineage_parents: Optional[List[str]] = None,
                         provenance: Optional[Dict] = None,
                         meta: Optional[Dict] = None,
                         actor: str = "studio2") -> Dict:
        if role not in DATASET_ROLES:
            raise ValueError(f"unknown dataset role {role!r}; known: {DATASET_ROLES}")
        entity_id = new_id("dv", name, role_seed := role)
        existing = self.get("datasets", entity_id)
        if existing:
            return existing
        entity = self.put("datasets", {
            "entity_id": entity_id, "name": name, "role": role,
            "protected_evaluation": role in PROTECTED_EVAL_ROLES,
            # lineage is immutable: parents are recorded once, at creation
            "lineage": {"parents": list(lineage_parents or []),
                        "recorded_at": store.now_iso()},
            "role_history": [{"from": None, "to": role, "actor": actor,
                              "override": None, "timestamp": store.now_iso()}],
            "governance_overrides": [],
            "provenance": provenance or {}, "meta": meta or {}})
        store.audit("dataset_registered", "datasets", entity_id, actor=actor,
                    detail=f"{name} role={role}")
        return entity

    def transition_role(self, dataset_id: str, new_role: str, actor: str,
                        override_reason: Optional[str] = None) -> Dict:
        """The ONLY path that changes a dataset's role.

        Contamination guard (raremine.lineage pattern):
          - protected eval role -> TRAINING requires an explicit override
            (who + why), otherwise RoleTransitionError;
          - TRAINING -> protected eval role is guarded the same way (training
            examples silently becoming launch evidence is the same corruption
            in the other direction).
        Every transition and every override is audited.
        """
        if new_role not in DATASET_ROLES:
            raise ValueError(f"unknown dataset role {new_role!r}")
        with self._lock:
            entity = self.get("datasets", dataset_id)
            if entity is None:
                raise KeyError(f"unknown dataset {dataset_id}")
            old_role = entity["role"]
            if old_role == new_role:
                return entity

            crossing = ((old_role in PROTECTED_EVAL_ROLES and new_role == "TRAINING")
                        or (old_role == "TRAINING" and new_role in PROTECTED_EVAL_ROLES))
            override = None
            if crossing:
                if not (override_reason and override_reason.strip() and actor.strip()):
                    raise RoleTransitionError(
                        f"dataset {dataset_id} ({entity['name']}): {old_role} -> "
                        f"{new_role} crosses the training/evaluation contamination "
                        "boundary; it requires an explicit governance override "
                        "recording who and why")
                override = {"actor": actor, "reason": override_reason,
                            "timestamp": store.now_iso(),
                            "transition": f"{old_role}->{new_role}"}
                entity["governance_overrides"].append(override)
                store.audit("governance_override", "datasets", dataset_id,
                            actor=actor, detail=override_reason)

            entity["role"] = new_role
            entity["protected_evaluation"] = new_role in PROTECTED_EVAL_ROLES
            entity["role_history"].append(
                {"from": old_role, "to": new_role, "actor": actor,
                 "override": override, "timestamp": store.now_iso()})
            self.put("datasets", entity)
            store.audit("role_transition", "datasets", dataset_id, actor=actor,
                        detail=f"{old_role} -> {new_role}")
            return entity

    # -------------------------------------------------------------- scenarios

    def register_scenario(self, name: str, generator: str, seed: Optional[int],
                          recipe: Optional[Dict] = None,
                          provenance: Optional[Dict] = None) -> Dict:
        recipe_hash = content_hash(recipe or {}, exclude=())
        entity_id = new_id("sv", name, generator, str(seed), recipe_hash)
        existing = self.get("scenarios", entity_id)
        if existing:
            return existing
        return self.put("scenarios", {
            "entity_id": entity_id, "name": name, "generator": generator,
            "seed": seed, "recipe": recipe or {}, "recipe_hash": recipe_hash,
            "provenance": provenance or {}})

    # -------------------------------------------------------------- policies

    def register_policy(self, name: str, doc: Dict,
                        provenance: Optional[Dict] = None) -> Dict:
        version = content_hash(doc)
        entity_id = f"pol-{version}"
        existing = self.get("policies", entity_id)
        if existing:
            return existing
        return self.put("policies", {
            "entity_id": entity_id, "name": name, "policy_version": version,
            "doc": doc, "provenance": provenance or {}})

    # -------------------------------------------------------------- experiments

    def register_experiment(self, name: str, candidate_model_id: str,
                            baseline_model_id: str,
                            meta: Optional[Dict] = None) -> Dict:
        entity_id = new_id("exp", name, candidate_model_id, baseline_model_id)
        existing = self.get("experiments", entity_id)
        if existing:
            return existing
        return self.put("experiments", {
            "entity_id": entity_id, "name": name,
            "candidate_model_id": candidate_model_id,
            "baseline_model_id": baseline_model_id, "meta": meta or {}})

    # -------------------------------------------------------------- runs

    def register_run(self, name: str, engine: str,
                     tuple_components: Dict, results: Optional[Dict] = None,
                     experiment_id: Optional[str] = None,
                     provenance: Optional[Dict] = None) -> Dict:
        """tuple_components: values for REPRO_COMPONENTS (None/'' = missing).
        The reproducibility verdict is computed here and cannot be supplied."""
        missing = [c for c in REPRO_COMPONENTS
                   if tuple_components.get(c) in (None, "")]
        entity_id = new_id("run", name, engine,
                           content_hash(tuple_components, exclude=()))
        existing = self.get("runs", entity_id)
        if existing:
            return existing
        return self.put("runs", {
            "entity_id": entity_id, "name": name, "engine": engine,
            "experiment_id": experiment_id,
            "reproducibility_tuple": {c: tuple_components.get(c)
                                      for c in REPRO_COMPONENTS},
            "missing_components": missing,
            "reproducibility": NON_REPRODUCIBLE if missing else REPRODUCIBLE,
            "results": results or {}, "provenance": provenance or {}})

    # -------------------------------------------------------------- safety cases

    def register_safety_case(self, name: str, case_kind: str, reference: Dict,
                             provenance: Optional[Dict] = None) -> Dict:
        entity_id = new_id("sc", name, case_kind,
                           content_hash(reference, exclude=()))
        existing = self.get("safety_cases", entity_id)
        if existing:
            return existing
        return self.put("safety_cases", {
            "entity_id": entity_id, "name": name, "case_kind": case_kind,
            "reference": reference, "provenance": provenance or {}})


# ------------------------------------------------------------------ ingest


def _safe_json(path: str) -> Optional[Dict]:
    try:
        with open(path) as f:
            return json.load(f)
    except Exception:
        return None


def ingest_existing_stores(registry: Registry, repo_root: str = ".") -> Dict:
    """Scan the landed packages' stores and register entities retroactively.

    Best-effort and idempotent. Nothing is fabricated: components that cannot
    be derived from the source records stay missing, so most ingested runs are
    honestly NON_REPRODUCIBLE (megaeval lineage carries the most complete
    tuple; it still lacks scenario + calibration components).
    """
    counts = {"models": 0, "datasets": 0, "runs": 0, "safety_cases": 0,
              "scenarios": 0, "policies": 0}
    sources: Dict[str, str] = {}

    # ---- megaeval runs (runs/megaeval/runs/*/run.json -> {"state": {...}})
    mega_dir = os.path.join(repo_root, "runs", "megaeval", "runs")
    if os.path.isdir(mega_dir):
        sources["megaeval"] = mega_dir
        for rid in sorted(os.listdir(mega_dir)):
            state = (_safe_json(os.path.join(mega_dir, rid, "run.json")) or {}).get("state")
            if not state or state.get("status") != "published":
                continue
            lin = state.get("lineage") or {}
            model = registry.register_model(
                name=state.get("model_version") or "unknown",
                version=state.get("model_version") or "unknown",
                checkpoint=lin.get("model_checkpoint") or "",
                provenance={"source_package": "megaeval", "run_id": rid})
            counts["models"] += 1
            dataset = registry.register_dataset(
                name=lin.get("dataset_version") or state.get("population_id") or rid,
                role="TEST",
                provenance={"source_package": "megaeval",
                            "population_id": state.get("population_id")},
                meta={"label_version": lin.get("label_version")})
            counts["datasets"] += 1
            registry.register_run(
                name=rid, engine="megaeval",
                tuple_components={
                    "model_version_id": model["entity_id"],
                    "dataset_version_id": dataset["entity_id"],
                    "scenario_version_id": None,  # megaeval has no scenario axis
                    "config_hash": content_hash(
                        {"threshold": lin.get("threshold_config"),
                         "sampling": lin.get("sampling_config"),
                         "metric_version": lin.get("metric_version"),
                         "evaluator": lin.get("evaluator_code_version")},
                        exclude=()),
                    "calibration_version": None,  # not recorded in megaeval lineage
                    "seed": state.get("seed"),
                    "policy_version_id": None,
                },
                results={"headline": state.get("headline")},
                provenance={"source_package": "megaeval", "run_id": rid,
                            "lineage": lin})
            counts["runs"] += 1

    # ---- seqeval runs (runs/seqeval/runs/*/run.json)
    seq_dir = os.path.join(repo_root, "runs", "seqeval", "runs")
    if os.path.isdir(seq_dir):
        sources["seqeval"] = seq_dir
        for rid in sorted(os.listdir(seq_dir)):
            st = _safe_json(os.path.join(seq_dir, rid, "run.json"))
            if not st:
                continue
            policy = registry.register_policy(
                name="seqeval-policy", doc=st.get("policy") or {},
                provenance={"source_package": "seqeval", "run_id": rid})
            counts["policies"] += 1
            cand = (st.get("candidate") or {}).get("model_version") or "candidate"
            base = (st.get("baseline") or {}).get("model_version") or "baseline"
            model = registry.register_model(
                name=cand, version=cand,
                provenance={"source_package": "seqeval", "run_id": rid})
            counts["models"] += 1
            registry.register_run(
                name=rid, engine="seqeval",
                tuple_components={
                    "model_version_id": model["entity_id"],
                    "dataset_version_id": st.get("population_id"),
                    "scenario_version_id": None,
                    "config_hash": content_hash(st.get("policy") or {}, exclude=()),
                    "calibration_version": None,
                    "seed": st.get("seed"),
                    "policy_version_id": policy["entity_id"],
                },
                results={"decision": st.get("decision"), "gate": st.get("gate"),
                         "stopping_reason": st.get("stopping_reason"),
                         "baseline": base, "candidate": cand},
                provenance={"source_package": "seqeval", "run_id": rid})
            counts["runs"] += 1

    # ---- safety evidence packages (runs/safety/evidence/*.json)
    ev_dir = os.path.join(repo_root, "runs", "safety", "evidence")
    if os.path.isdir(ev_dir):
        sources["safety"] = ev_dir
        for name in sorted(os.listdir(ev_dir)):
            if not name.endswith(".json"):
                continue
            pkg = _safe_json(os.path.join(ev_dir, name))
            if not pkg:
                continue
            registry.register_safety_case(
                name=pkg.get("package_id") or name,
                case_kind="safety_evidence_package",
                reference={"package_id": pkg.get("package_id"),
                           "candidate_run_id": (pkg.get("candidate") or {}).get("run_id"),
                           "baseline_run_id": (pkg.get("baseline") or {}).get("run_id"),
                           "release_ready": (pkg.get("decision") or {}).get("release_ready"),
                           "path": os.path.join("runs", "safety", "evidence", name)},
                provenance={"source_package": "safety"})
            counts["safety_cases"] += 1

    # ---- agentic scorecards (runs/agentic/scorecards/*.json) — in-flight pkg,
    # but ingest reads plain JSON so it works regardless of importability
    sc_dir = os.path.join(repo_root, "runs", "agentic", "scorecards")
    if os.path.isdir(sc_dir):
        sources["agentic"] = sc_dir
        for name in sorted(os.listdir(sc_dir)):
            if not name.endswith(".json"):
                continue
            card = _safe_json(os.path.join(sc_dir, name))
            if not card:
                continue
            registry.register_safety_case(
                name=card.get("scorecard_id") or name,
                case_kind="agentic_scorecard",
                reference={"scorecard_id": card.get("scorecard_id"),
                           "failure_id": card.get("failure_id"),
                           "policy_version": card.get("policy_version"),
                           "recommended_option": card.get("recommended_option"),
                           "path": os.path.join("runs", "agentic", "scorecards", name)},
                provenance={"source_package": "agentic"})
            counts["safety_cases"] += 1

    # ---- bevfusion comparison runs (runs/bevfusion/bevrun-*.json)
    bev_dir = os.path.join(repo_root, "runs", "bevfusion")
    if os.path.isdir(bev_dir):
        sources["bevfusion"] = bev_dir
        for name in sorted(os.listdir(bev_dir)):
            if not (name.startswith("bevrun-") and name.endswith(".json")):
                continue
            rep = _safe_json(os.path.join(bev_dir, name))
            if not rep:
                continue
            params = rep.get("params") or {}
            scenario = registry.register_scenario(
                name=f"bevfusion-suite-{params.get('seed')}",
                generator="bevfusion.scenes.generate_sequences",
                seed=params.get("seed"), recipe=params,
                provenance={"source_package": "bevfusion", "run_id": rep.get("run_id")})
            counts["scenarios"] += 1
            engines = rep.get("engines") or {}
            model = registry.register_model(
                name=engines.get("candidate") or "bev-fused",
                version=engines.get("candidate") or "bev-fused",
                provenance={"source_package": "bevfusion", "run_id": rep.get("run_id")})
            counts["models"] += 1
            registry.register_run(
                name=rep.get("run_id") or name, engine="bevfusion",
                tuple_components={
                    "model_version_id": model["entity_id"],
                    "dataset_version_id": None,  # scenes are generated, not versioned data
                    "scenario_version_id": scenario["entity_id"],
                    "config_hash": content_hash(params, exclude=()),
                    "calibration_version": None,
                    "seed": params.get("seed"),
                    "policy_version_id": None,
                },
                results={"recommendation": rep.get("recommendation"),
                         "headline_deltas": rep.get("headline_deltas")},
                provenance={"source_package": "bevfusion", "run_id": rep.get("run_id")})
            counts["runs"] += 1

    store.audit("ingest", None, None, detail=f"scanned {sorted(sources)}",
                payload={"counts": counts})
    return {"sources": sources, "registered": counts,
            "totals": registry.counts()}


# ------------------------------------------------------------------ singleton

_REGISTRY: Optional[Registry] = None
_REG_LOCK = threading.RLock()


def get_registry() -> Registry:
    global _REGISTRY
    with _REG_LOCK:
        if _REGISTRY is None:
            _REGISTRY = Registry()
        return _REGISTRY