botp
/

File size: 13,767 Bytes
1d2de8a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Serving identity: which readout is served, and proof that it is the one that was measured.

Split out of `solomon/service.py` unchanged. Three things live here:

  * the frozen selection file (readout mode, served design, where the binding is),
  * the two-letter prompt identity, and
  * `RuntimeBinding` -- the one safety property this release keeps. A binding carries the runtime identity
    of the model that was scored, and every key of it must equal the live engine's or the service refuses
    to load. The binding also carries the calibration, so a temperature cannot drift from the stack it was
    fitted against, and it refuses a calibration fitted on a different model.
"""
import copy
import hashlib
import importlib
import json
from pathlib import Path

from solomon.calibration import NoTemperature, ROOT, digest, load_temperature
from solomon.engine_contract import BOOLEAN_TASK, LABEL

SELECTION = ROOT / 'serving/selection.json'
DEFAULT_BINDING = ROOT / 'serving/serving-binding.json'
DESIGN_FREEZE = ROOT / 'serving/design-freeze.json'
CONTRACT = 'solomon-v1'
MODES = ('four_collapsed', 'two_letter')
BINDING_SCHEMA = 'solomon-serving-binding-v1'
# ------------------------------------------------------------------ selection and prompts
def load_selection(path=SELECTION):
    """The frozen selection file -> {'mode', 'design', 'prompts', 'binding', 'sha256'}.

    Accepted keys: 'readout', 'readout_mode' or 'mode' (four_collapsed | two_letter).
    Optional: 'design' {single_choice, ordered} override, 'two_letter_prompts' (inline headers dict or
    'module:ATTRIBUTE'), 'binding' (path to the Solomon serving-binding.json: absolute, or relative to the
    selection file's own directory, else to the project root).
    """
    path = Path(path)
    if not path.exists():
        raise ValueError(f'Solomon selection not found: {path} (pass mode= explicitly for development use)')
    raw = json.loads(path.read_text())
    mode = raw.get('readout') or raw.get('readout_mode') or raw.get('mode')
    if mode not in MODES:
        raise ValueError('selection.json names no readout mode (readout / readout_mode / mode)')
    return {'mode': mode, 'design': raw.get('design'), 'prompts': raw.get('two_letter_prompts'), 'binding': raw.get('binding'),
            'evidence_head': raw.get('evidence_head'),
            'sha256': hashlib.sha256(path.read_bytes()).hexdigest(), 'path': str(path), 'dir': str(path.resolve().parent)}


def default_design(mode):
    """four_collapsed serves the frozen design (single R, ordered S); two_letter was trained on S rows only."""
    if mode == 'two_letter':
        return {'single_choice': 'S', 'ordered': 'S'}
    frozen = json.loads(DESIGN_FREEZE.read_text()) if DESIGN_FREEZE.exists() else {'single_choice': 'R', 'ordered': 'S'}
    return {'single_choice': frozen['single_choice'], 'ordered': frozen['ordered']}


class TwoLetterPrompts:
    """Header swap four-state -> two-letter. headers: {'boolean': str, 'label': str, optional 'entity': str}.

    The four-state block is HEADER + rest (question / label / answer cue); only HEADER changes, so a two-letter block
    is byte-identical to what training used iff the training builder does the same swap (checked by `check_parity`).
    """
    FOUR = {'boolean': BOOLEAN_TASK, 'label': LABEL}

    def __init__(self, headers):
        if not isinstance(headers, dict) or not {'boolean', 'label'} <= set(headers) or set(headers) - {'boolean', 'label', 'entity'}:
            raise ValueError('two-letter prompts need headers for boolean and label (optional entity)')
        if any(not isinstance(v, str) or not v.strip() for v in headers.values()):
            raise ValueError('two-letter headers must be nonempty strings')
        if any(v.startswith(four) or four.startswith(v) for v in headers.values() for four in self.FOUR.values()):
            raise ValueError('two-letter header must differ from the four-state headers')
        self.headers = dict(headers)
        self.sha256 = digest(self.headers)

    def header(self, task, kind):
        return self.headers.get('entity', self.headers['boolean']) if (kind == 'boolean' and task == 'entity') else self.headers[kind]

    def rewrite(self, task, block):
        """Four-state block -> (two-letter block, 2), or None when the block is not a four-state Noul block."""
        for kind, four in self.FOUR.items():
            if block.startswith(four) and (kind == 'label') == (task == 'multilabel') and task in ('boolean', 'entity', 'multilabel'):
                return self.header(task, kind) + block[len(four):], 2
        return None

    def check_parity(self, rows):
        """rows: two-letter training/eval rows {'task', 'block'} (Noul tasks). Returns mismatching row indices."""
        bad = []
        for i, row in enumerate(rows):
            task, block = row['task'], row['block']
            kind = 'label' if task == 'multilabel' else 'boolean'
            head = self.header(task, kind)
            if not block.startswith(head) or self.rewrite(task, self.FOUR[kind] + block[len(head):]) != (block, 2):
                bad.append(i)
        return bad


def load_prompts(spec):
    """spec: None (solomon/prompts_two_letter.py, the headers the two-letter readout was trained on), inline headers dict, or 'module:ATTRIBUTE'."""
    if spec is None:
        from solomon import prompts_two_letter as p9
        loaded = TwoLetterPrompts({'boolean': p9.BOOLEAN_TASK2, 'label': p9.LABEL2})
        loaded.sha256 = hashlib.sha256(Path(p9.__file__).read_bytes()).hexdigest()  # = the prompts_sha256 a two_letter binding pins
        return loaded
    if isinstance(spec, str):
        module, _, attr = spec.partition(':')
        spec = getattr(importlib.import_module(module), attr or 'TWO_LETTER_HEADERS')
    return spec if isinstance(spec, TwoLetterPrompts) else TwoLetterPrompts(spec)


class UnboundServing:
    """Development / no-binding-file path. Still answers everything; it simply claims no bound identity."""
    status = 'unbound'
    schema = None
    sha256 = None

    def __init__(self, reason='unbound: no Solomon serving binding is loaded'):
        self.reason = reason
        self.bound_keys = []
        self.calibration = NoTemperature()
        self.head_routing = None

    def describe(self):
        return {'status': self.status, 'schema': None, 'sha256': None, 'bound_runtime_keys': [],
                'reason': self.reason, 'calibration': self.calibration.describe()}


class RuntimeBinding:
    """The one safety property Solomon v1.1 keeps: the stack being served is the stack that was measured.

    A `solomon-serving-binding-v1` artifact carries the runtime identity of the scored model (15 keys for this
    release: every ServiceEngine identity key except the recomputed `fingerprint`). Every one of them must equal the
    live engine identity or construction raises, so the service refuses to load on drift. It carries NO thresholds,
    NO per-task status, NO temperatures and NO fitted correctness head: there is nothing here that could gate an
    answer, because nothing gates an answer.
    """
    status = 'bound'
    schema = BINDING_SCHEMA
    # Without these two the binding would not pin the model at all; a binding that omits them is refused.
    REQUIRED = ('adapter_sha256', 'trained_heads_sha256')
    # Keys that must never appear in a serving binding: they are the machinery of the abstention path (v1.1).
    # `temperatures` is deliberately NOT here -- a per-task scalar is a calibration parameter, not a decision, and
    # carrying it inside the binding is what stops it drifting from the stack it was fitted against.
    REFUSED = ('tasks', 'thresholds', 'confidence', 'correctness', 'abstention', 'policy', 'status')
    # A temperature is fitted on ONE model's logits and is meaningless on another, even though the numbers would
    # look identical. So a non-trivial calibration must record the model it was fitted on, and that model must be
    # the one this binding serves.
    CALIBRATION_BOUND_KEYS = ('adapter_sha256', 'trained_heads_sha256')

    def __init__(self, binding, *, mode, runtime, design, prompts_sha256=None):
        if not isinstance(binding, dict) or binding.get('schema') != BINDING_SCHEMA:
            raise ValueError('Solomon serving binding schema required')
        if binding.get('sha256') != digest({k: v for k, v in binding.items() if k != 'sha256'}):
            raise ValueError('Solomon serving binding checksum mismatch')
        present = [k for k in self.REFUSED if k in binding]
        if present:
            raise ValueError('serving binding carries removed abstention machinery: ' + ', '.join(present))
        if binding.get('contract') not in (None, CONTRACT):
            raise ValueError('Solomon serving binding was issued for a different serving contract: ' + str(binding.get('contract')))
        if binding.get('readout') != mode:
            raise ValueError('Solomon serving binding was built for a different readout mode')
        bound = binding.get('runtime') or {}
        if not isinstance(bound, dict) or any(k not in bound for k in self.REQUIRED):
            raise ValueError('serving binding must bind at least ' + ', '.join(self.REQUIRED))
        drift = sorted(k for k, v in bound.items() if runtime.get(k) != v)
        if drift:
            raise ValueError('Solomon serving binding runtime identity mismatch: ' + ', '.join(drift))
        if binding.get('design') is not None and binding['design'] != design:
            raise ValueError('Solomon serving binding design mismatch')
        if mode == 'two_letter' and binding.get('prompts_sha256') != prompts_sha256:
            raise ValueError('Solomon serving binding prompt identity mismatch')
        routing = binding.get('head_routing')
        if routing is not None and (not isinstance(routing, dict) or any(not isinstance(k, str) or not isinstance(v, str)
                                                                          for k, v in routing.items())):
            raise ValueError('serving binding head_routing must map head_key -> head_key')
        self.head_routing = dict(routing) if routing is not None else None
        self.binding = copy.deepcopy(binding)
        self.sha256 = binding['sha256']
        self.bound_keys = sorted(bound)
        # Covered by this binding's checksum and by the identity check above, so the temperatures cannot drift
        # from the runtime they were measured on.
        self.calibration = load_temperature(binding.get('temperatures'))
        provenance = binding.get('provenance') or {}
        if self.calibration.file_sha256 is None:      # embedded copy: take the file hash from the provenance
            self.calibration.file_sha256 = provenance.get('calibration_file_sha256')
        self._check_calibration_model(bound, provenance)

    def _check_calibration_model(self, bound, provenance):
        """Refuse a calibration inherited from a different model.

        The 15-key identity check already proves the SERVING stack is the scored one. It cannot see this: a
        temperature fitted on model A and copied onto a binding for model B has the same numbers and passes every
        value check there is. The only thing that distinguishes them is which model's logits the scalars were
        fitted against, so that has to be recorded and matched.
        """
        if self.calibration.is_identity():
            return                                    # T = 1.0 everywhere is a no-op; safe on any model
        fitted = provenance.get('calibration_fitted_on') or {}
        missing = [k for k in self.CALIBRATION_BOUND_KEYS if not fitted.get(k)]
        if missing:
            raise ValueError(
                'serving binding carries temperatures but no record of the model they were fitted on (missing '
                + ', '.join('provenance.calibration_fitted_on.' + k for k in missing) + '). A temperature fitted '
                'on one model is meaningless on another, so an unattributed calibration is refused. Refit on this '
                'model, or serve at T = 1.0.')
        for key in self.CALIBRATION_BOUND_KEYS:
            if bound.get(key) != fitted[key]:
                raise ValueError(
                    f'calibration was fitted on {key} {fitted[key]}, binding carries {key} {bound.get(key)}. '
                    'Temperatures fitted on one model do not transfer to another even when the numbers match; '
                    'refit on this model against the error-rich fit panel, or serve it at T = 1.0.')

    def describe(self):
        return {'status': self.status, 'schema': self.schema, 'sha256': self.sha256,
                'bound_runtime_keys': list(self.bound_keys), 'calibration': self.calibration.describe(),
                **({'head_routing': dict(self.head_routing)} if self.head_routing is not None else {})}


def load_binding(path, *, mode, runtime, design, prompts_sha256=None):
    """None / missing file -> UnboundServing; a present but invalid or drifted file fails closed (raises)."""
    if isinstance(path, (UnboundServing, RuntimeBinding)) or hasattr(path, 'describe'):
        return path
    if isinstance(path, dict):
        return RuntimeBinding(path, mode=mode, runtime=runtime, design=design, prompts_sha256=prompts_sha256)
    path = Path(path) if path is not None else DEFAULT_BINDING
    if not path.is_absolute():
        path = ROOT / path
    if not path.exists():
        return UnboundServing(f'unbound: no Solomon serving binding at {path.relative_to(ROOT) if path.is_relative_to(ROOT) else path}')
    return RuntimeBinding(json.loads(path.read_text()), mode=mode, runtime=runtime, design=design, prompts_sha256=prompts_sha256)