File size: 8,535 Bytes
d6c8a4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbe7355
d6c8a4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbe7355
 
 
 
 
d6c8a4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from __future__ import annotations

import json
from pathlib import Path
from typing import Iterable

from src.hackathon.data import list_dummy_models, list_dummy_stimuli, stimulus_key

MODEL_REGISTRY_ENV = "HACKATHON_MODEL_REGISTRY"
BLUE_MODEL_REGISTRY_ENV = "HACKATHON_BLUE_MODEL_REGISTRY"
RED_MODEL_REGISTRY_ENV = "HACKATHON_RED_MODEL_REGISTRY"
STIMULI_CATALOG_ENV = "HACKATHON_STIMULI_CATALOG"
BLUE_STIMULI_CATALOG_ENV = "HACKATHON_BLUE_STIMULI_CATALOG"

BLUE_TEAM_REQUIRED_MODELS = 20
RED_TEAM_REQUIRED_STIMULI = 1000


def _ensure_unique(values: list[str], label: str) -> None:
    if len(values) != len(set(values)):
        raise ValueError(f"{label} must be unique.")


def _load_json(path: Path) -> object:
    try:
        return json.loads(path.read_text())
    except FileNotFoundError as exc:
        raise ValueError(f"File not found: {path}") from exc
    except json.JSONDecodeError as exc:
        raise ValueError(f"Invalid JSON in {path}: {exc}") from exc


def _parse_registry_entries(path: str | None) -> list[dict | str]:
    """Parse registry file into raw entries (strings or dicts)."""
    if not path:
        return []

    data = _load_json(Path(path))
    if isinstance(data, dict):
        entries = data.get("models")
        if entries is None:
            raise ValueError("Model registry JSON must be a list or contain a 'models' list.")
    elif isinstance(data, list):
        entries = data
    else:
        raise ValueError("Model registry JSON must be a list or object.")

    return entries


def load_model_registry(path: str | None) -> set[str]:
    if not path:
        return set(list_dummy_models())

    entries = _parse_registry_entries(path)
    names: list[str] = []
    for idx, entry in enumerate(entries, start=1):
        if isinstance(entry, str):
            name = entry.strip()
        elif isinstance(entry, dict):
            name = str(entry.get("model_name", "")).strip()
        else:
            raise ValueError(f"Model registry entry {idx} must be a string or object.")

        if not name:
            raise ValueError(f"Model registry entry {idx} is missing model_name.")
        names.append(name)

    _ensure_unique(names, "Model registry entries")
    return set(names)


def load_model_registry_specs(path: str | None) -> dict[str, dict]:
    """Load full model specs keyed by model_name.

    Returns a dict mapping model_name -> {"layer": ..., "embedding": ..., "preprocess": ...}.
    """
    if not path:
        return {}

    entries = _parse_registry_entries(path)
    specs: dict[str, dict] = {}
    for idx, entry in enumerate(entries, start=1):
        if not isinstance(entry, dict):
            continue
        name = str(entry.get("model_name", "")).strip()
        if not name:
            continue
        specs[name] = {
            "layer": str(entry.get("layer", "")).strip(),
            "embedding": str(entry.get("embedding", "flatten")).strip(),
            "preprocess": entry.get("preprocess", {}),
        }
    return specs


def load_stimuli_catalog(path: str | None) -> list[dict[str, str]]:
    if not path:
        return list_dummy_stimuli()

    path_obj = Path(path)
    if path_obj.suffix == ".jsonl":
        lines = path_obj.read_text().splitlines()
        entries = [json.loads(line) for line in lines if line.strip()]
    else:
        data = _load_json(path_obj)
        if isinstance(data, dict):
            entries = data.get("stimuli")
            if entries is None:
                raise ValueError("Stimuli catalog JSON must be a list or contain a 'stimuli' list.")
        elif isinstance(data, list):
            entries = data
        else:
            raise ValueError("Stimuli catalog JSON must be a list or object.")

    stimuli: list[dict[str, str]] = []
    for idx, entry in enumerate(entries, start=1):
        if not isinstance(entry, dict):
            raise ValueError(f"Stimulus entry {idx} must be an object.")
        dataset_name = str(entry.get("dataset_name", "")).strip()
        image_identifier = str(entry.get("image_identifier", "")).strip()
        if not dataset_name or not image_identifier:
            raise ValueError(f"Stimulus entry {idx} must include dataset_name and image_identifier.")
        stimuli.append({"dataset_name": dataset_name, "image_identifier": image_identifier})

    keys = [stimulus_key(stimulus) for stimulus in stimuli]
    _ensure_unique(keys, "Stimuli catalog entries")
    return stimuli


def validate_blue_submission(
    payload: dict,
    *,
    model_registry: Iterable[str] | None = None,
    registry_specs: dict[str, dict] | None = None,
) -> list[str]:
    if model_registry is None:
        model_registry = list_dummy_models()
    registry_set = set(model_registry)

    models = payload.get("models")
    if not isinstance(models, list):
        raise ValueError("Blue submission must include a list of models.")

    names: list[str] = []
    layer_mismatches: list[str] = []
    missing_layers: list[str] = []
    for idx, item in enumerate(models, start=1):
        if isinstance(item, str):
            name = item.strip()
            layer_name = None
        elif isinstance(item, dict):
            name = str(item.get("model_name", "")).strip()
            layer_name = str(item.get("layer_name", "")).strip() or None
        else:
            raise ValueError(f"Model entry {idx} must be a string or object with model_name.")
        if not name:
            raise ValueError(f"Model entry {idx} is missing model_name.")
        names.append(name)

        # layer_name is required when registry_specs are available
        if registry_specs:
            if not layer_name:
                missing_layers.append(f"Model entry {idx} ({name}) is missing layer_name.")
            elif name in registry_specs:
                expected_layer = registry_specs[name].get("layer", "")
                if layer_name != expected_layer:
                    layer_mismatches.append(
                        f"{name}: submitted layer_name '{layer_name}' "
                        f"does not match registry layer '{expected_layer}'"
                    )

    _ensure_unique(names, "Model selections")

    if len(names) != BLUE_TEAM_REQUIRED_MODELS:
        raise ValueError(
            f"Blue team submission must contain exactly {BLUE_TEAM_REQUIRED_MODELS} "
            f"unique models, but got {len(names)}."
        )

    missing = [name for name in names if name not in registry_set]
    if missing:
        missing_str = ", ".join(missing)
        raise ValueError(f"Unknown models requested: {missing_str}")

    if missing_layers:
        raise ValueError(
            f"Missing layer_name for {len(missing_layers)} model(s):\n"
            + "\n".join(f"  - {m}" for m in missing_layers)
        )

    if layer_mismatches:
        raise ValueError(
            f"Layer name mismatch for {len(layer_mismatches)} model(s):\n"
            + "\n".join(f"  - {m}" for m in layer_mismatches)
        )

    return names


def validate_red_submission(
    payload: dict,
    *,
    stimuli_catalog: Iterable[dict[str, str]] | None = None,
) -> list[str]:
    if stimuli_catalog is None:
        stimuli_catalog = list_dummy_stimuli()

    images = payload.get("differentiating_images")
    if not isinstance(images, list):
        raise ValueError("Red submission must include differentiating_images.")
    if len(images) != RED_TEAM_REQUIRED_STIMULI:
        raise ValueError(
            f"Red team submission must contain exactly {RED_TEAM_REQUIRED_STIMULI} "
            f"unique stimuli, but got {len(images)}."
        )

    keys: list[str] = []
    for idx, item in enumerate(images, start=1):
        if not isinstance(item, dict):
            raise ValueError(f"Stimulus entry {idx} must be an object.")
        dataset_name = str(item.get("dataset_name", "")).strip()
        image_identifier = str(item.get("image_identifier", "")).strip()
        if not dataset_name or not image_identifier:
            raise ValueError(f"Stimulus entry {idx} must include dataset_name and image_identifier.")
        keys.append(stimulus_key({"dataset_name": dataset_name, "image_identifier": image_identifier}))

    _ensure_unique(keys, "Stimulus selections")

    available = {stimulus_key(stimulus) for stimulus in stimuli_catalog}
    missing = [key for key in keys if key not in available]
    if missing:
        missing_str = ", ".join(missing)
        raise ValueError(f"Unknown stimuli requested: {missing_str}")

    return keys