@trillboards/edge-sdk 0.2.111 → 0.2.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/detector/bench/regrade_control_selection.py +291 -28
- package/deploy/detector/bench/venue_adjudication.py +13 -2
- package/deploy/detector/tests/test_regrade_control_selection.py +108 -0
- package/dist/AudienceAnalyzeClient.d.ts +11 -0
- package/dist/AudienceAnalyzeClient.d.ts.map +1 -1
- package/dist/AudienceAnalyzeClient.js +7 -0
- package/dist/AudienceAnalyzeClient.js.map +1 -1
- package/dist/ChannelProber.d.ts.map +1 -1
- package/dist/ChannelProber.js +15 -1
- package/dist/ChannelProber.js.map +1 -1
- package/dist/CloudSensingManager.d.ts +20 -0
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +30 -0
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/SensingProfileClient.d.ts +10 -0
- package/dist/SensingProfileClient.d.ts.map +1 -1
- package/dist/SensingProfileClient.js +16 -0
- package/dist/SensingProfileClient.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/discovery.d.ts +1 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +15 -4
- package/dist/discovery.js.map +1 -1
- package/dist/localSensing.d.ts +70 -0
- package/dist/localSensing.d.ts.map +1 -1
- package/dist/localSensing.js +182 -1
- package/dist/localSensing.js.map +1 -1
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ from __future__ import annotations
|
|
|
37
37
|
import argparse
|
|
38
38
|
import json
|
|
39
39
|
import os
|
|
40
|
+
import time
|
|
40
41
|
|
|
41
42
|
#: Grades whose own note says the set cannot rate a class. Kept in step with
|
|
42
43
|
#: `venue_adjudication.py::LIVENESS_ONLY_GRADES`; the test pins them equal.
|
|
@@ -67,7 +68,124 @@ def rateable_controls(per_control: dict, ctlq: dict) -> dict:
|
|
|
67
68
|
return out
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
#: Schemas this tool will read a NEW control rate out of. A rescore artifact is
|
|
72
|
+
#: the ONLY way a control the original screen never scored can reach a verdict
|
|
73
|
+
#: here, and it is admissible for exactly one reason: it was produced by
|
|
74
|
+
#: `venue_adjudication.py`'s OWN rule -- same GRID, same QUIET_MAX_FIRE, same
|
|
75
|
+
#: worst-valid-bucket quiet_t, same bar -- against a control that carries a
|
|
76
|
+
#: contact-sheet grade. Anything else would be an assertion wearing a filename.
|
|
77
|
+
RESCORE_SCHEMAS = ("trillboards.fire-smoke-rescore/1",)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_rescores(paths) -> dict:
|
|
81
|
+
"""Committed rescore artifacts, keyed by (model, class).
|
|
82
|
+
|
|
83
|
+
Returns {(model, class): row}. A file whose schema is not in
|
|
84
|
+
`RESCORE_SCHEMAS` is REFUSED rather than skipped: a rescore this tool cannot
|
|
85
|
+
read is a number somebody expected to count, and silently ignoring it is how
|
|
86
|
+
a verdict stays stale while looking re-derived.
|
|
87
|
+
"""
|
|
88
|
+
out = {}
|
|
89
|
+
for path in paths or []:
|
|
90
|
+
doc = json.load(open(path))
|
|
91
|
+
if doc.get("schema") not in RESCORE_SCHEMAS:
|
|
92
|
+
raise SystemExit(
|
|
93
|
+
"%s has schema %r, which this tool cannot read (known: %s). Refusing rather "
|
|
94
|
+
"than skipping -- a rescore that is silently ignored leaves the verdict stale "
|
|
95
|
+
"while the run reports success."
|
|
96
|
+
% (path, doc.get("schema"), ", ".join(RESCORE_SCHEMAS)))
|
|
97
|
+
model = (doc.get("artifact") or {}).get("model_id")
|
|
98
|
+
if not model:
|
|
99
|
+
raise SystemExit("%s names no artifact.model_id, so its rates belong to nobody" % path)
|
|
100
|
+
# A rescore may carry caveats that survive its own verdict -- "this clears
|
|
101
|
+
# the bar AND no live route reaches it" is one fact, not two, and the
|
|
102
|
+
# record of record is where a reader decides whether to SELL. Carried
|
|
103
|
+
# rather than restated so there is one home for the measurement.
|
|
104
|
+
caveats = doc.get("sellability_caveats") or []
|
|
105
|
+
for cls, e in (doc.get("per_class") or {}).items():
|
|
106
|
+
out[(model, cls)] = {
|
|
107
|
+
"control": e["control_set"],
|
|
108
|
+
"n": e["control_frames"],
|
|
109
|
+
"rate_025": e["control_rate_025"],
|
|
110
|
+
"rate_at_quiet_t": e["control_rate_at_quiet_t"],
|
|
111
|
+
"quiet_t": e["quiet_t"],
|
|
112
|
+
"source_file": os.path.basename(path),
|
|
113
|
+
"sellability_caveats": caveats,
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def apply_rescore(cand: dict, rescores: dict, ctlq: dict):
|
|
119
|
+
"""Merge a rescored control into one candidate's `per_control`.
|
|
120
|
+
|
|
121
|
+
Returns (candidate, note). The candidate is a COPY when anything changed.
|
|
122
|
+
|
|
123
|
+
THE OFF-TARGET BOUND IS THE WHOLE CARE HERE. `decide()`'s third gate is
|
|
124
|
+
`off_target_mean <= bar`, and a rescore does not recompute it -- the
|
|
125
|
+
off-target control sets the original screen fired against are gone. What it
|
|
126
|
+
can do is INHERIT the committed value, and only in the direction where that
|
|
127
|
+
is sound: `off_target` is `(scores >= quiet_t).mean()`, which is monotone
|
|
128
|
+
NON-INCREASING in the threshold, so a value measured at a LOWER quiet_t is a
|
|
129
|
+
valid UPPER BOUND at a higher one. If the rescore's threshold is BELOW the
|
|
130
|
+
committed one the bound runs the wrong way, and the row is refused rather
|
|
131
|
+
than passed on a number nobody measured at that operating point.
|
|
132
|
+
"""
|
|
133
|
+
key = (cand.get("model"), cand.get("class"))
|
|
134
|
+
r = (rescores or {}).get(key)
|
|
135
|
+
if not r:
|
|
136
|
+
return cand, None
|
|
137
|
+
grade = (ctlq.get(r["control"]) or {}).get("grade")
|
|
138
|
+
if grade is None:
|
|
139
|
+
raise SystemExit(
|
|
140
|
+
"rescore %s supplies control %r for %s/%s, which has no contact-sheet grade. "
|
|
141
|
+
"An ungraded set is neither `unusable` nor `liveness_only`, so it would be "
|
|
142
|
+
"eligible to carry a verdict on nobody's inspection."
|
|
143
|
+
% (r["source_file"], r["control"], key[0], key[1]))
|
|
144
|
+
committed_t = cand.get("quiet_t")
|
|
145
|
+
if committed_t is not None and r["quiet_t"] < committed_t:
|
|
146
|
+
return cand, {
|
|
147
|
+
"model": key[0], "class": key[1], "control": r["control"],
|
|
148
|
+
"refused": ("rescore quiet_t %s is BELOW the committed %s, so the committed "
|
|
149
|
+
"off_target_mean is not an upper bound at the rescored operating "
|
|
150
|
+
"point and no off-target number exists for it"
|
|
151
|
+
% (r["quiet_t"], committed_t)),
|
|
152
|
+
"source_file": r["source_file"],
|
|
153
|
+
}
|
|
154
|
+
merged = dict(cand)
|
|
155
|
+
# A RATE BELONGS TO THE THRESHOLD IT WAS MEASURED AT. Raising the candidate's
|
|
156
|
+
# `quiet_t` does not re-measure the controls already in `per_control` -- their
|
|
157
|
+
# `rate_at_quiet_t` is still the value at the COMMITTED threshold. Because
|
|
158
|
+
# `(scores >= t).mean()` is monotone non-increasing in t, that number is an
|
|
159
|
+
# UPPER BOUND at the higher threshold, so reading it as the recall at the new
|
|
160
|
+
# operating point OVERSTATES recall and can manufacture a PUBLISH. Every entry
|
|
161
|
+
# is therefore stamped with the threshold that produced it, and selection
|
|
162
|
+
# admits only the ones measured at the threshold actually applied.
|
|
163
|
+
merged["per_control"] = {
|
|
164
|
+
name: dict(row, measured_at_t=row.get("measured_at_t", committed_t))
|
|
165
|
+
for name, row in (cand.get("per_control") or {}).items()
|
|
166
|
+
}
|
|
167
|
+
merged["per_control"][r["control"]] = {
|
|
168
|
+
"n": r["n"], "rate_025": r["rate_025"], "rate_at_quiet_t": r["rate_at_quiet_t"],
|
|
169
|
+
"measured_at_t": r["quiet_t"],
|
|
170
|
+
}
|
|
171
|
+
merged["quiet_t"] = r["quiet_t"]
|
|
172
|
+
note = {
|
|
173
|
+
"model": key[0], "class": key[1], "control": r["control"], "grade": grade,
|
|
174
|
+
"n": r["n"], "rate_at_quiet_t": r["rate_at_quiet_t"],
|
|
175
|
+
"quiet_t_committed": committed_t, "quiet_t_rescored": r["quiet_t"],
|
|
176
|
+
"off_target_mean_inherited": cand.get("off_target_mean"),
|
|
177
|
+
"off_target_bound_holds_because": (
|
|
178
|
+
"off_target is (scores >= t).mean(), monotone non-increasing in t, and the "
|
|
179
|
+
"rescored t (%s) is >= the committed t (%s), so the committed value is an "
|
|
180
|
+
"upper bound at the rescored operating point"
|
|
181
|
+
% (r["quiet_t"], committed_t)),
|
|
182
|
+
"source_file": r["source_file"],
|
|
183
|
+
"sellability_caveats": r.get("sellability_caveats") or [],
|
|
184
|
+
}
|
|
185
|
+
return merged, note
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def regrade_capability(cap: dict, ctlq: dict, bar: dict, rescores: dict = None) -> dict:
|
|
71
189
|
"""Re-apply the bar to one capability using only rateable controls."""
|
|
72
190
|
recall_bar = bar["recall_retention"]
|
|
73
191
|
off_bar = bar["off_target_mean"]
|
|
@@ -88,8 +206,11 @@ def regrade_capability(cap: dict, ctlq: dict, bar: dict) -> dict:
|
|
|
88
206
|
"why": "resolved before any control is read: %s" % (prior,),
|
|
89
207
|
}
|
|
90
208
|
|
|
91
|
-
rows, unknown = [], []
|
|
209
|
+
rows, unknown, rescored, stale_t = [], [], [], []
|
|
92
210
|
for cand in cap["candidates"]:
|
|
211
|
+
cand, note = apply_rescore(cand, rescores, ctlq)
|
|
212
|
+
if note:
|
|
213
|
+
rescored.append(note)
|
|
93
214
|
elided = "_elided" in (cand.get("by_bucket") or {})
|
|
94
215
|
rate = rateable_controls(cand.get("per_control"), ctlq)
|
|
95
216
|
if not rate:
|
|
@@ -120,9 +241,39 @@ def regrade_capability(cap: dict, ctlq: dict, bar: dict) -> dict:
|
|
|
120
241
|
"elided": elided,
|
|
121
242
|
})
|
|
122
243
|
continue
|
|
244
|
+
# Only a control measured at the threshold this candidate is DECIDED at may
|
|
245
|
+
# carry the verdict. Absent a rescore nothing is stamped and every control
|
|
246
|
+
# is at the committed threshold, so this is a no-op on the untouched path.
|
|
247
|
+
applied_t = cand.get("quiet_t")
|
|
248
|
+
fresh = {k: v for k, v in rate.items()
|
|
249
|
+
if v.get("measured_at_t") is None or v["measured_at_t"] == applied_t}
|
|
250
|
+
stale = {k: v for k, v in rate.items() if k not in fresh}
|
|
251
|
+
for k, v in sorted(stale.items()):
|
|
252
|
+
stale_t.append({
|
|
253
|
+
"model": cand["model"], "class": cand["class"], "control": k,
|
|
254
|
+
"measured_at_t": v.get("measured_at_t"), "applied_t": applied_t,
|
|
255
|
+
"rate_at_measured_t": v.get("rate_at_quiet_t"),
|
|
256
|
+
"why_excluded": ("its rate is an upper bound at the applied threshold, "
|
|
257
|
+
"not a measurement of it, so it may not carry a verdict"),
|
|
258
|
+
})
|
|
259
|
+
if not fresh:
|
|
260
|
+
unknown.append({
|
|
261
|
+
"model": cand["model"], "class": cand["class"],
|
|
262
|
+
"decided_on": cand.get("best_control"),
|
|
263
|
+
"grade": (ctlq.get(cand.get("best_control")) or {}).get("grade"),
|
|
264
|
+
"why_unknown": ("a rescore moved quiet_t to %s and every rateable control "
|
|
265
|
+
"here was measured at a lower threshold (%s), where its rate "
|
|
266
|
+
"is only an upper bound -- no control has a rate AT the "
|
|
267
|
+
"applied operating point"
|
|
268
|
+
% (applied_t,
|
|
269
|
+
", ".join(sorted(str(v.get("measured_at_t"))
|
|
270
|
+
for v in stale.values())))),
|
|
271
|
+
"elided": elided,
|
|
272
|
+
})
|
|
273
|
+
continue
|
|
123
274
|
# argmax(rate_025) among the rateable, exactly as the fixed tool does.
|
|
124
|
-
name = max(
|
|
125
|
-
row =
|
|
275
|
+
name = max(fresh, key=lambda k: (fresh[k]["rate_025"] or 0))
|
|
276
|
+
row = fresh[name]
|
|
126
277
|
rows.append({
|
|
127
278
|
"model": cand["model"], "class": cand["class"],
|
|
128
279
|
"control": name, "control_n": row["n"], "grade": row["grade"],
|
|
@@ -130,8 +281,13 @@ def regrade_capability(cap: dict, ctlq: dict, bar: dict) -> dict:
|
|
|
130
281
|
"off_target_mean": cand.get("off_target_mean"),
|
|
131
282
|
"quiet_t": cand.get("quiet_t"),
|
|
132
283
|
"n_valid_negatives": cand.get("n_valid_negatives"),
|
|
133
|
-
"also_scored": {k:
|
|
134
|
-
"
|
|
284
|
+
"also_scored": {k: dict(
|
|
285
|
+
{"n": v["n"], "grade": (ctlq.get(k) or {}).get("grade"),
|
|
286
|
+
"rate_at_quiet_t": v["rate_at_quiet_t"]},
|
|
287
|
+
**({} if v.get("measured_at_t") in (None, applied_t) else {
|
|
288
|
+
"measured_at_t": v["measured_at_t"],
|
|
289
|
+
"upper_bound_only": ("measured at %s, not the applied %s"
|
|
290
|
+
% (v["measured_at_t"], applied_t))}))
|
|
135
291
|
for k, v in (cand.get("per_control") or {}).items() if k != name},
|
|
136
292
|
"source": "recomputed from committed per_control",
|
|
137
293
|
})
|
|
@@ -208,6 +364,10 @@ def regrade_capability(cap: dict, ctlq: dict, bar: dict) -> dict:
|
|
|
208
364
|
"winner": win,
|
|
209
365
|
"candidates_regraded": len(rows),
|
|
210
366
|
"candidates_unknown": blocking,
|
|
367
|
+
"rescored_controls": rescored,
|
|
368
|
+
# Never silent: a control dropped for being measured at another
|
|
369
|
+
# threshold is a stated exclusion, exactly like a withheld model.
|
|
370
|
+
"stale_threshold_controls": stale_t,
|
|
211
371
|
}
|
|
212
372
|
|
|
213
373
|
|
|
@@ -260,7 +420,35 @@ def _selftest() -> None:
|
|
|
260
420
|
measured = regrade_capability(cap("NO MODEL CLEARS BAR"), ctlq, bar)
|
|
261
421
|
assert measured["regraded_decision"] == "NO MODEL CLEARS BAR", measured["regraded_decision"]
|
|
262
422
|
|
|
263
|
-
|
|
423
|
+
# 3. A RESCORE SETTLES AN UNRESOLVED. This is the whole point of --rescore: a
|
|
424
|
+
# control added after the screen ran reaches the ladder, and a candidate
|
|
425
|
+
# that clears the bar on it turns "not known" into PUBLISH. The elided row
|
|
426
|
+
# is still unknown and still reported -- it just cannot block any more,
|
|
427
|
+
# which is this tool's own stated monotonicity ("an unknown can only ever
|
|
428
|
+
# ADD a passing candidate").
|
|
429
|
+
ctlq3 = dict(ctlq, fresh={"grade": "good"})
|
|
430
|
+
rescore = {("m1", "fire"): {"control": "fresh", "n": 72, "rate_025": 0.82,
|
|
431
|
+
"rate_at_quiet_t": 0.86, "quiet_t": 0.30,
|
|
432
|
+
"source_file": "rescore.json"}}
|
|
433
|
+
settled = regrade_capability(cap(UNRESOLVED), ctlq3, bar, rescore)
|
|
434
|
+
assert settled["regraded_decision"] == "PUBLISH", settled["regraded_decision"]
|
|
435
|
+
assert settled["winner"]["control"] == "fresh", settled["winner"]
|
|
436
|
+
assert settled["rescored_controls"], "the rescore must be recorded, not silently used"
|
|
437
|
+
assert settled["changed"] is True
|
|
438
|
+
|
|
439
|
+
# 4. A RESCORE AT A LOWER THRESHOLD IS REFUSED. `off_target_mean` is inherited
|
|
440
|
+
# from the committed row and is only an UPPER BOUND when the rescored
|
|
441
|
+
# threshold is >= the committed one. Below it, no off-target number exists
|
|
442
|
+
# at that operating point and passing the row would assert one.
|
|
443
|
+
low = {("m1", "fire"): {"control": "fresh", "n": 72, "rate_025": 0.82,
|
|
444
|
+
"rate_at_quiet_t": 0.86, "quiet_t": 0.10,
|
|
445
|
+
"source_file": "rescore.json"}}
|
|
446
|
+
refused = regrade_capability(cap(UNRESOLVED), ctlq3, bar, low)
|
|
447
|
+
assert refused["regraded_decision"] == UNRESOLVED, refused["regraded_decision"]
|
|
448
|
+
assert any("refused" in r for r in refused["rescored_controls"]), refused["rescored_controls"]
|
|
449
|
+
|
|
450
|
+
print("selftest OK: idempotent on UNRESOLVED; a measured failure is left alone; "
|
|
451
|
+
"a rescore settles an unknown; a rescore below the committed threshold is refused")
|
|
264
452
|
|
|
265
453
|
|
|
266
454
|
def main() -> None:
|
|
@@ -269,7 +457,24 @@ def main() -> None:
|
|
|
269
457
|
_selftest()
|
|
270
458
|
return
|
|
271
459
|
ap.add_argument("--adjudication", required=True)
|
|
272
|
-
ap.add_argument("--control-quality", required=True
|
|
460
|
+
ap.add_argument("--control-quality", required=True, nargs="+",
|
|
461
|
+
help="positive-control-quality-*.json. TAKES A LIST and merges them, later "
|
|
462
|
+
"files winning, exactly as venue_adjudication.py does. It read ONE "
|
|
463
|
+
"path until 2026-08-27, which is unsafe for the reason that file "
|
|
464
|
+
"states about itself: `liveness_only` and `unusable` are both built "
|
|
465
|
+
"by MEMBERSHIP, so a set whose grade lives in a file this run did not "
|
|
466
|
+
"read falls through both and is eligible to carry a verdict on "
|
|
467
|
+
"nobody's inspection. The grades live in DATED files because the "
|
|
468
|
+
"2026-08-25 record is pinned by the score cards that cite it, so a "
|
|
469
|
+
"set graded later lands in a new file rather than editing that one -- "
|
|
470
|
+
"which means a single-path reader was guaranteed to go stale.")
|
|
471
|
+
ap.add_argument("--rescore", nargs="+", default=None,
|
|
472
|
+
help="committed rescore artifact(s) supplying a control the ORIGINAL "
|
|
473
|
+
"screen never scored. Admissible only because they are produced by "
|
|
474
|
+
"venue_adjudication.py's own rule -- same GRID, same QUIET_MAX_FIRE, "
|
|
475
|
+
"same worst-valid-bucket quiet_t, same bar. Without this there is no "
|
|
476
|
+
"path for a control added after the screen ran, and a capability "
|
|
477
|
+
"whose verdict a later measurement settles stays UNRESOLVED for ever.")
|
|
273
478
|
ap.add_argument("--out", required=True)
|
|
274
479
|
ap.add_argument("--apply", action="store_true",
|
|
275
480
|
help="also write the corrected verdict back into --adjudication. The "
|
|
@@ -278,10 +483,15 @@ def main() -> None:
|
|
|
278
483
|
args = ap.parse_args()
|
|
279
484
|
|
|
280
485
|
adj = json.load(open(args.adjudication))
|
|
281
|
-
ctlq =
|
|
486
|
+
ctlq = {}
|
|
487
|
+
for _q in args.control_quality:
|
|
488
|
+
for _k, _v in json.load(open(_q)).items():
|
|
489
|
+
if isinstance(_v, dict) and isinstance(_v.get("grade"), str):
|
|
490
|
+
ctlq[_k] = _v
|
|
282
491
|
bar = adj["bar"]
|
|
283
492
|
|
|
284
|
-
|
|
493
|
+
rescores = load_rescores(args.rescore)
|
|
494
|
+
rows = [regrade_capability(c, ctlq, bar, rescores) for c in adj["capabilities"]]
|
|
285
495
|
changed = [r for r in rows if r["changed"]]
|
|
286
496
|
|
|
287
497
|
# The regrade artifact is a record OF A TRANSITION. Once `--apply` has run, the
|
|
@@ -313,6 +523,8 @@ def main() -> None:
|
|
|
313
523
|
"This re-reads the numbers the adjudication already committed. A "
|
|
314
524
|
"capability marked `needs_remeasure` has a candidate whose "
|
|
315
525
|
"rateable-control rate was elided and must come from a re-run.",
|
|
526
|
+
"control_quality_inputs": sorted(os.path.basename(x) for x in args.control_quality),
|
|
527
|
+
"rescore_inputs": sorted({os.path.basename(x) for x in (args.rescore or [])}),
|
|
316
528
|
"liveness_only_grades": list(LIVENESS_ONLY_GRADES),
|
|
317
529
|
"unusable_grades": list(UNUSABLE_GRADES),
|
|
318
530
|
"bar": bar,
|
|
@@ -327,34 +539,85 @@ def main() -> None:
|
|
|
327
539
|
}, open(args.out, "w"), indent=1)
|
|
328
540
|
|
|
329
541
|
if args.apply and changed:
|
|
542
|
+
# THE WRITER MUST HANDLE BOTH DIRECTIONS. It was built for the WITHDRAWAL
|
|
543
|
+
# only -- it hardcoded the date `2026-08-26`, always wrote `ships: False`,
|
|
544
|
+
# and never recorded a winner, because a withdrawal has none. All three
|
|
545
|
+
# are correct for taking a PUBLISH away and wrong for giving one back,
|
|
546
|
+
# and a restoration written through the withdrawal's fields would land a
|
|
547
|
+
# PUBLISH in the record with no model, no control and `ships: False`
|
|
548
|
+
# beside it -- a verdict a reader cannot act on and a sales claim nobody
|
|
549
|
+
# can trace to an artifact.
|
|
550
|
+
today = time.strftime("%Y-%m-%d", time.gmtime())
|
|
330
551
|
by_cap = {r["capability"]: r for r in changed}
|
|
331
552
|
for cap in adj["capabilities"]:
|
|
332
553
|
r = by_cap.get(cap["capability"])
|
|
333
554
|
if not r:
|
|
334
555
|
continue
|
|
335
556
|
was = dict(cap["decision"])
|
|
336
|
-
|
|
557
|
+
win = r.get("winner") or {}
|
|
558
|
+
publishes = r["regraded_decision"] == "PUBLISH"
|
|
559
|
+
if publishes:
|
|
560
|
+
why_corrected = (
|
|
561
|
+
"a control that CAN rate this class was added after the screen ran "
|
|
562
|
+
"(`%s`, n=%s, grade %s) and is committed in %s, which computed its rate "
|
|
563
|
+
"with venue_adjudication.py's OWN rule -- same GRID, same QUIET_MAX_FIRE, "
|
|
564
|
+
"same worst-valid-bucket quiet_t, same bar. Re-applying that bar with the "
|
|
565
|
+
"new control in scope clears it. The elided rows stay unknown and cannot "
|
|
566
|
+
"block: an unknown can only ever ADD a passing candidate."
|
|
567
|
+
% (win.get("control"), win.get("control_n"), win.get("grade"),
|
|
568
|
+
", ".join(sorted({os.path.basename(x) for x in (args.rescore or [])}))
|
|
569
|
+
or "a rescore artifact"))
|
|
570
|
+
else:
|
|
571
|
+
why_corrected = (
|
|
572
|
+
"the verdict rested on `%s` (n=%s, grade %s), a set whose own "
|
|
573
|
+
"contact-sheet note says it can only show liveness. Re-applying "
|
|
574
|
+
"this run's OWN bar to the controls that can rate a class "
|
|
575
|
+
"overturns it." % (was.get("control"), was.get("control_n"),
|
|
576
|
+
r.get("prior_control_grade")))
|
|
577
|
+
decision = {
|
|
337
578
|
"decision": r["regraded_decision"],
|
|
338
579
|
"why": r["why"],
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
# the
|
|
580
|
+
# NOT re-derived here in either direction, and for the same
|
|
581
|
+
# reason: `events_lost` / `vlm_calls_saved` are computed by
|
|
582
|
+
# venue_adjudication.py from the FULL per-frame score matrices
|
|
583
|
+
# against the whole negative set, which this tool does not hold.
|
|
584
|
+
# A withdrawal must not keep a promise computed from a recall it
|
|
585
|
+
# no longer accepts; a restoration must not invent one.
|
|
342
586
|
"gate": None,
|
|
343
|
-
"
|
|
344
|
-
"
|
|
345
|
-
"
|
|
346
|
-
"
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
587
|
+
"gate_is_null_because": (
|
|
588
|
+
"the cascade-gate arithmetic is a function of the full score matrices, "
|
|
589
|
+
"which this tool does not read. A verdict is derivable from committed "
|
|
590
|
+
"rates; a savings promise is not."),
|
|
591
|
+
}
|
|
592
|
+
caveats = [c for n in (r.get("rescored_controls") or [])
|
|
593
|
+
for c in (n.get("sellability_caveats") or [])]
|
|
594
|
+
if caveats:
|
|
595
|
+
# TOP LEVEL, beside the verdict, not buried in `corrected`. This
|
|
596
|
+
# file is quoted in the sales claim sheet, and a PUBLISH whose
|
|
597
|
+
# caveat is one level down is a PUBLISH that gets quoted alone.
|
|
598
|
+
decision["sellability_caveats"] = caveats
|
|
599
|
+
if publishes:
|
|
600
|
+
decision.update({
|
|
601
|
+
"model": win.get("model"),
|
|
602
|
+
"class": win.get("class"),
|
|
603
|
+
"control": win.get("control"),
|
|
604
|
+
"control_n": win.get("control_n"),
|
|
605
|
+
"control_grade": win.get("grade"),
|
|
606
|
+
"operating_conf": win.get("quiet_t"),
|
|
607
|
+
"n_valid_negatives": win.get("n_valid_negatives"),
|
|
608
|
+
"also_scored": win.get("also_scored"),
|
|
609
|
+
})
|
|
610
|
+
decision["corrected"] = {
|
|
611
|
+
"on": today,
|
|
612
|
+
"by": "deploy/detector/bench/regrade_control_selection.py --apply",
|
|
613
|
+
"was": was,
|
|
614
|
+
"ships": publishes,
|
|
615
|
+
"why_corrected": why_corrected,
|
|
616
|
+
"needs_remeasure": r["candidates_unknown"],
|
|
617
|
+
"rescored_controls": r.get("rescored_controls") or [],
|
|
618
|
+
"regrade_artifact": args.out,
|
|
357
619
|
}
|
|
620
|
+
cap["decision"] = decision
|
|
358
621
|
cap["changed"] = bool(cap.get("prior_decision_taxi_negatives")
|
|
359
622
|
and cap["prior_decision_taxi_negatives"]
|
|
360
623
|
!= r["regraded_decision"])
|
|
@@ -151,8 +151,19 @@ CLASS_CONTROL = {
|
|
|
151
151
|
"firearm": ["gun_clean", "weapon_cctv"], "knife": ["knife_cctv", "weapon_cctv", "n_knife"],
|
|
152
152
|
"fall": ["fall_cctv"], "fallen": ["fall_cctv"], "fall-detected": ["fall_cctv"],
|
|
153
153
|
"Fall-Detected": ["fall_cctv"], "down": ["fall_cctv"],
|
|
154
|
-
"door": ["n_door"], "window": ["n_window"],
|
|
155
|
-
|
|
154
|
+
"door": ["n_door"], "window": ["n_window"],
|
|
155
|
+
# `phone_use_meva` (22 frames / 17 fixed cameras, `good`) leads because it is the only
|
|
156
|
+
# set that can rate this class at all TODAY: `n_phone` is graded `good` but its FRAMES
|
|
157
|
+
# ARE GONE -- s3://trillboards-media-prod/DEV/regression-corpus/ holds no `n_phone`
|
|
158
|
+
# prefix, measured 2026-08-27, the same as fire_cctv/fire_generic/wc_spill. `n_phone` is
|
|
159
|
+
# KEPT and stays second for the same reason `fire_generic` was kept after it was
|
|
160
|
+
# demoted: its number is in the committed adjudication and deleting the mapping would
|
|
161
|
+
# hide what the earlier screen scored. The two also answer different questions --
|
|
162
|
+
# `n_phone` is web stills of people holding smartphones, `phone_use_meva` is fixed CCTV
|
|
163
|
+
# at 79-690 px of person height, which is the deployment.
|
|
164
|
+
"phone": ["phone_use_meva", "n_phone"], "cell phone": ["phone_use_meva", "n_phone"],
|
|
165
|
+
"phone_call": ["phone_use_meva", "n_phone"],
|
|
166
|
+
"mouse": ["n_rodent"], "void": ["n_shelfgap"],
|
|
156
167
|
"cart": ["wc_cart", "n_trolley"], "trolley": ["wc_cart", "n_trolley"], "shopping_cart": ["wc_cart", "n_trolley"],
|
|
157
168
|
"spill": ["wc_spill", "n_spill"], "Spill": ["wc_spill", "n_spill"],
|
|
158
169
|
"cigarette": ["n_smoking"], "Cigarette": ["n_smoking"], "smoking": ["n_smoking"],
|
|
@@ -29,7 +29,10 @@ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "bench")
|
|
|
29
29
|
|
|
30
30
|
from regrade_control_selection import ( # noqa: E402
|
|
31
31
|
PRESERVE_ON_BLOCKING,
|
|
32
|
+
RESCORE_SCHEMAS,
|
|
32
33
|
UNRESOLVED,
|
|
34
|
+
apply_rescore,
|
|
35
|
+
load_rescores,
|
|
33
36
|
rateable_controls,
|
|
34
37
|
regrade_capability,
|
|
35
38
|
)
|
|
@@ -116,5 +119,110 @@ class TestRegradeControlSelection(unittest.TestCase):
|
|
|
116
119
|
self.assertEqual(out["status"], "not_affected")
|
|
117
120
|
|
|
118
121
|
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class RescoreReachesTheLadder(unittest.TestCase):
|
|
125
|
+
"""A control added AFTER the screen ran must be able to settle a verdict.
|
|
126
|
+
|
|
127
|
+
Without this the tool has no input for a set the original run never scored,
|
|
128
|
+
so a capability whose verdict a later measurement settles stays `UNRESOLVED`
|
|
129
|
+
for ever while the registry publishes a model — two stores disagreeing about
|
|
130
|
+
whether a capability is sellable, which is the state this input exists to
|
|
131
|
+
end. Proved empirically first: with `fire_firesense` graded `good` and
|
|
132
|
+
visible to it, the tool still returned UNRESOLVED, because the new control
|
|
133
|
+
appears in no candidate's `per_control`.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
RESCORE = {("m1", "fire"): {"control": "fresh", "n": 72, "rate_025": 0.82,
|
|
137
|
+
"rate_at_quiet_t": 0.86, "quiet_t": 0.30,
|
|
138
|
+
"source_file": "rescore.json"}}
|
|
139
|
+
CTLQ3 = dict(CTLQ, fresh={"grade": "good"})
|
|
140
|
+
|
|
141
|
+
def test_a_rescore_settles_an_unresolved(self):
|
|
142
|
+
r = regrade_capability(_cap(UNRESOLVED), self.CTLQ3, BAR, self.RESCORE)
|
|
143
|
+
self.assertEqual(r["regraded_decision"], "PUBLISH")
|
|
144
|
+
self.assertEqual(r["winner"]["control"], "fresh")
|
|
145
|
+
self.assertTrue(r["rescored_controls"], "the rescore must be recorded, not silently used")
|
|
146
|
+
|
|
147
|
+
def test_a_rescore_below_the_committed_threshold_is_REFUSED(self):
|
|
148
|
+
"""`off_target_mean` is INHERITED, and only bounds upward.
|
|
149
|
+
|
|
150
|
+
It is `(scores >= t).mean()`, monotone non-increasing in t, so a value
|
|
151
|
+
measured at a LOWER threshold is an upper bound at a higher one. Below
|
|
152
|
+
the committed threshold the bound runs the wrong way and there is no
|
|
153
|
+
off-target number at that operating point — passing the row would assert
|
|
154
|
+
one nobody measured.
|
|
155
|
+
"""
|
|
156
|
+
low = {("m1", "fire"): dict(self.RESCORE[("m1", "fire")], quiet_t=0.10)}
|
|
157
|
+
r = regrade_capability(_cap(UNRESOLVED), self.CTLQ3, BAR, low)
|
|
158
|
+
self.assertEqual(r["regraded_decision"], UNRESOLVED)
|
|
159
|
+
self.assertTrue(any("refused" in n for n in r["rescored_controls"]))
|
|
160
|
+
|
|
161
|
+
def test_an_ungraded_rescored_control_is_refused_not_trusted(self):
|
|
162
|
+
"""An ungraded set falls through both `liveness_only` and `unusable`."""
|
|
163
|
+
with self.assertRaises(SystemExit):
|
|
164
|
+
apply_rescore(_cap(UNRESOLVED)["candidates"][0], self.RESCORE, CTLQ)
|
|
165
|
+
|
|
166
|
+
def test_a_rescore_never_overturns_a_measured_failure_into_silence(self):
|
|
167
|
+
"""A passing rescore may raise a failure to PUBLISH — that is the point —
|
|
168
|
+
but it must do so through the ladder, with a winner and a recorded source."""
|
|
169
|
+
r = regrade_capability(_cap("NO MODEL CLEARS BAR"), self.CTLQ3, BAR, self.RESCORE)
|
|
170
|
+
self.assertEqual(r["regraded_decision"], "PUBLISH")
|
|
171
|
+
self.assertEqual(r["rescored_controls"][0]["source_file"], "rescore.json")
|
|
172
|
+
|
|
173
|
+
def test_an_unreadable_rescore_schema_is_refused_not_skipped(self):
|
|
174
|
+
import json as _json
|
|
175
|
+
import tempfile
|
|
176
|
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh:
|
|
177
|
+
_json.dump({"schema": "something.else/1"}, fh)
|
|
178
|
+
bad = fh.name
|
|
179
|
+
with self.assertRaises(SystemExit):
|
|
180
|
+
load_rescores([bad])
|
|
181
|
+
|
|
182
|
+
def test_a_stale_threshold_control_may_not_carry_the_verdict(self):
|
|
183
|
+
"""THE RATE BELONGS TO THE THRESHOLD IT WAS MEASURED AT.
|
|
184
|
+
|
|
185
|
+
A rescore raises the candidate's `quiet_t` but re-measures only its OWN
|
|
186
|
+
control. Every other rate in `per_control` is still the value at the
|
|
187
|
+
committed, LOWER threshold, where `(scores >= t).mean()` is an UPPER
|
|
188
|
+
BOUND — so reading it as the recall at the new operating point overstates
|
|
189
|
+
recall. Here `big` is stale at 0.90 and the rescored control is a genuine
|
|
190
|
+
0.30 failure: without per-control threshold tracking `argmax(rate_025)`
|
|
191
|
+
picks `big` and manufactures a PUBLISH out of a number measured somewhere
|
|
192
|
+
else.
|
|
193
|
+
"""
|
|
194
|
+
cap = _cap(UNRESOLVED, with_elided=False)
|
|
195
|
+
cap["candidates"][0]["per_control"]["big"] = {
|
|
196
|
+
"n": 9, "rate_025": 0.90, "rate_at_quiet_t": 0.90}
|
|
197
|
+
weak = {("m1", "fire"): dict(self.RESCORE[("m1", "fire")],
|
|
198
|
+
rate_025=0.30, rate_at_quiet_t=0.30)}
|
|
199
|
+
r = regrade_capability(cap, self.CTLQ3, BAR, weak)
|
|
200
|
+
self.assertNotEqual(
|
|
201
|
+
r["regraded_decision"], "PUBLISH",
|
|
202
|
+
"a rate measured at the committed threshold was used to clear the bar "
|
|
203
|
+
"at the higher rescored threshold")
|
|
204
|
+
stale = r["stale_threshold_controls"]
|
|
205
|
+
self.assertTrue(stale, "the stale-threshold refusal must be stated, not silent")
|
|
206
|
+
self.assertEqual(stale[0]["control"], "big")
|
|
207
|
+
self.assertEqual((stale[0]["measured_at_t"], stale[0]["applied_t"]), (0.25, 0.30))
|
|
208
|
+
|
|
209
|
+
def test_a_control_at_the_applied_threshold_still_carries_the_verdict(self):
|
|
210
|
+
"""The positive half: the rescored control IS at the applied threshold,
|
|
211
|
+
so it decides — this is the path fire and smoke actually take."""
|
|
212
|
+
r = regrade_capability(_cap(UNRESOLVED, with_elided=False),
|
|
213
|
+
self.CTLQ3, BAR, self.RESCORE)
|
|
214
|
+
self.assertEqual(r["regraded_decision"], "PUBLISH")
|
|
215
|
+
self.assertEqual(r["winner"]["control"], "fresh")
|
|
216
|
+
|
|
217
|
+
def test_without_a_rescore_no_control_is_stamped_stale(self):
|
|
218
|
+
"""The untouched path must be byte-identical — nothing is stamped, so
|
|
219
|
+
every control remains admissible."""
|
|
220
|
+
a = regrade_capability(_cap("NO MODEL CLEARS BAR", rateable_rate=0.8), CTLQ, BAR)
|
|
221
|
+
self.assertEqual(a["regraded_decision"], "PUBLISH")
|
|
222
|
+
|
|
223
|
+
def test_the_known_schema_list_is_not_empty(self):
|
|
224
|
+
self.assertTrue(RESCORE_SCHEMAS)
|
|
225
|
+
|
|
226
|
+
|
|
119
227
|
if __name__ == "__main__":
|
|
120
228
|
unittest.main()
|
|
@@ -210,6 +210,17 @@ export declare class AudienceAnalyzeClient {
|
|
|
210
210
|
* local-vlm rows while the cloud path is 100%.
|
|
211
211
|
*/
|
|
212
212
|
promptText?: string;
|
|
213
|
+
/**
|
|
214
|
+
* Which fields of `preAnalyzed` a GRANTED SPECIALIST answered, as
|
|
215
|
+
* `field -> modelId`. Everything not named here was answered by the
|
|
216
|
+
* on-device VLM.
|
|
217
|
+
*
|
|
218
|
+
* The two producers write byte-identical values into the same flat
|
|
219
|
+
* object, so this map is the only thing that can separate them — and the
|
|
220
|
+
* server needs it to stamp `local_detector` in `_provenance_map` rather
|
|
221
|
+
* than flattening the whole observation to `local_connector`.
|
|
222
|
+
*/
|
|
223
|
+
detectorProduced?: Record<string, string>;
|
|
213
224
|
}): Promise<AudienceAnalyzeResponse>;
|
|
214
225
|
/**
|
|
215
226
|
* POST a FRAMELESS camera-health beacon. This is used when the connector can
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudienceAnalyzeClient.d.ts","sourceRoot":"","sources":["../src/AudienceAnalyzeClient.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"AudienceAnalyzeClient.d.ts","sourceRoot":"","sources":["../src/AudienceAnalyzeClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -230,6 +230,13 @@ class AudienceAnalyzeClient {
|
|
|
230
230
|
...(typeof request.promptText === 'string' && request.promptText.trim() !== ''
|
|
231
231
|
? { localPromptText: request.promptText }
|
|
232
232
|
: {}),
|
|
233
|
+
// Per-field producer attribution. Sent only when a specialist actually
|
|
234
|
+
// answered something: an absent key means "no specialist answered on this
|
|
235
|
+
// frame", which is a different fact from an empty map and the server
|
|
236
|
+
// treats them the same way only because it never sees the second one.
|
|
237
|
+
...(request.detectorProduced && Object.keys(request.detectorProduced).length > 0
|
|
238
|
+
? { detectorProduced: request.detectorProduced }
|
|
239
|
+
: {}),
|
|
233
240
|
// Same last-spread override semantics as analyze(); see the note there.
|
|
234
241
|
...(request.extra || {}),
|
|
235
242
|
...(evidenceId ? { evidenceId } : {}),
|