github-actions[bot] commited on
Commit
30a9dc2
·
1 Parent(s): 62335fa

Deploy fab7842

Browse files

The consequence pass now calls Evo 2, instead of claiming it did

Source: https://github.com/WINTER4000/turingDNA/commit/fab7842627a8e635689634387736e7f1fe750879

dee/core/compiler.py CHANGED
@@ -354,12 +354,17 @@ def compile_correction(wt_allele: str, patient_allele: str, *,
354
  # intergenic space, so I did not clear this guide".
355
  #
356
  # Every pass reports one of:
357
- # ok ran, nothing blocking
358
- # warn ran, with a caveat the designer must read
359
  # error ran, and refused
 
360
  # unavailable did NOT run, because this deployment cannot — with the reason
361
  # skipped did not run because an earlier pass already refused
362
  #
 
 
 
 
363
  # "unavailable" is deliberately distinct from "ok". Conflating them is how a
364
  # tool ends up implying it checked something it never looked at.
365
 
@@ -398,7 +403,10 @@ class CompileReport:
398
 
399
  @property
400
  def incomplete_because(self) -> List[str]:
401
- return [p.title for p in self.passes if p.status == "unavailable"]
 
 
 
402
 
403
 
404
  # What this deployment can and cannot do, stated once so the passes and the
@@ -435,16 +443,18 @@ def compile_report(wt_allele: str, patient_allele: str, *,
435
  window: str = "", offset: int = -1,
436
  germline: bool = False,
437
  can_enumerate: bool = True,
438
- can_score_consequence: bool = False,
439
  can_check_specificity: bool = False) -> CompileReport:
440
  """Run the passes and report every one, including those that could not run.
441
 
442
- The three `can_*` flags are supplied by the caller from LIVE capability
443
- checks (is the DNA model reachable, is an off-target index loaded), never
444
- hardcoded the same "availability must mean reachable" rule the rest of
445
- this codebase learned the hard way. Defaulting the two model-backed passes
446
- to False means a caller that forgets to check gets an honestly incomplete
447
- record rather than a falsely complete one.
 
 
448
  """
449
  if germline:
450
  raise GermlineRefused(
@@ -509,8 +519,30 @@ def compile_report(wt_allele: str, patient_allele: str, *,
509
  add("enumerate", "unavailable", CAPABILITY_NOTES["enumerate"])
510
 
511
  # ── consequence ─────────────────────────────────────────────────────
512
- add("consequence", "ok" if can_score_consequence else "unavailable",
513
- CAPABILITY_NOTES["consequence"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
  # ── specificity ─────────────────────────────────────────────���───────
516
  # Always carries its caveat, even when it runs: a pass that reports "ok"
 
354
  # intergenic space, so I did not clear this guide".
355
  #
356
  # Every pass reports one of:
357
+ # ok ran, produced a result, nothing blocking
358
+ # warn ran, produced a result, with a caveat the designer must read
359
  # error ran, and refused
360
+ # failed was ATTEMPTED and produced no result
361
  # unavailable did NOT run, because this deployment cannot — with the reason
362
  # skipped did not run because an earlier pass already refused
363
  #
364
+ # `failed` is separate from `warn` on purpose. Reporting a model call that
365
+ # errored as "passed with caveat" is a soft version of the same lie as
366
+ # reporting an unrun pass as ok: in both cases nothing was assessed.
367
+ #
368
  # "unavailable" is deliberately distinct from "ok". Conflating them is how a
369
  # tool ends up implying it checked something it never looked at.
370
 
 
403
 
404
  @property
405
  def incomplete_because(self) -> List[str]:
406
+ """Passes that produced no assessment whether they were never run
407
+ or were attempted and failed. Both leave the same hole in the record."""
408
+ return [p.title for p in self.passes
409
+ if p.status in ("unavailable", "failed")]
410
 
411
 
412
  # What this deployment can and cannot do, stated once so the passes and the
 
443
  window: str = "", offset: int = -1,
444
  germline: bool = False,
445
  can_enumerate: bool = True,
446
+ consequence: Optional[Dict[str, object]] = None,
447
  can_check_specificity: bool = False) -> CompileReport:
448
  """Run the passes and report every one, including those that could not run.
449
 
450
+ `consequence` is the ACTUAL RESULT of scoring the variant, or None. It is
451
+ deliberately not a capability flag: the first version of this function
452
+ took `can_score_consequence: bool` and reported the pass as "ok" whenever
453
+ the model was merely *reachable*, so the UI printed "Assess edit
454
+ consequence passed" while nothing had been assessed. That is the same
455
+ "configured means done" lie that dee/core/modal_client.reachable exists to
456
+ prevent, rebuilt one layer up. A pass reports success only when it holds
457
+ the output of work that happened.
458
  """
459
  if germline:
460
  raise GermlineRefused(
 
519
  add("enumerate", "unavailable", CAPABILITY_NOTES["enumerate"])
520
 
521
  # ── consequence ─────────────────────────────────────────────────────
522
+ # Reports on the RESULT, never on the ability to have produced one.
523
+ if consequence is None:
524
+ add("consequence", "unavailable", CAPABILITY_NOTES["consequence"])
525
+ elif not consequence.get("ok"):
526
+ add("consequence", "failed",
527
+ "Scoring was attempted and did not return a result: "
528
+ f"{consequence.get('error') or 'no detail'}. Nothing is reported "
529
+ "for this pass rather than an assumed-benign default. "
530
+ + CAPABILITY_NOTES["consequence"])
531
+ else:
532
+ dl = consequence.get("delta_ll")
533
+ label = consequence.get("label") or "the variant"
534
+ # State the direction in words. A bare signed number invites the
535
+ # reader to supply their own convention, and half of them will
536
+ # supply the wrong one.
537
+ if isinstance(dl, (int, float)):
538
+ direction = ("less likely than wild-type" if dl < 0
539
+ else "more likely than wild-type" if dl > 0
540
+ else "indistinguishable from wild-type")
541
+ detail = (f"{label}: delta log-likelihood {dl:+.4f} — the model "
542
+ f"finds the patient sequence {direction}. ")
543
+ else:
544
+ detail = f"{label}: scored, no delta returned. "
545
+ add("consequence", "ok", detail + CAPABILITY_NOTES["consequence"])
546
 
547
  # ── specificity ─────────────────────────────────────────────���───────
548
  # Always carries its caveat, even when it runs: a pass that reports "ok"
dee/server.py CHANGED
@@ -3416,6 +3416,47 @@ def create_app() -> Flask:
3416
  # Lowers a pathogenic variant to an editing strategy, or refuses with a
3417
  # diagnostic. See dee/core/compiler.py for the scope this operates in —
3418
  # somatic design and assessment only, never a clinical decision.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3419
  @app.post("/api/compiler/compile")
3420
  def compiler_compile() -> Response:
3421
  gate = _dna_signin_gate("compiler")
@@ -3434,23 +3475,21 @@ def create_app() -> Flask:
3434
  if not wt and not patient:
3435
  return jsonify({"error": "missing 'wt_allele' and 'patient_allele'"}), 400
3436
 
3437
- # Capability comes from a LIVE probe, never a constant. A pass that
3438
- # cannot run must report `unavailable`, and the only way to know is to
3439
- # ask. Specificity stays off in this version on purpose: the human
3440
- # off-target index is coding-sequence only, so this endpoint does not
3441
- # claim to have cleared a guide it never checked outside coding space.
3442
- try:
3443
- can_score = bool(_dna_scoring.runnable("achilles"))
3444
- except Exception: # noqa: BLE001 capability must never break a compile
3445
- app.logger.debug("DNA capability probe failed", exc_info=True)
3446
- can_score = False
3447
 
3448
  try:
3449
  report = _compiler.compile_report(
3450
  wt, patient, window=window, offset=offset,
3451
  germline=bool(body.get("germline")),
3452
  can_enumerate=True,
3453
- can_score_consequence=can_score,
3454
  can_check_specificity=False,
3455
  )
3456
  except _compiler.GermlineRefused as exc:
 
3416
  # Lowers a pathogenic variant to an editing strategy, or refuses with a
3417
  # diagnostic. See dee/core/compiler.py for the scope this operates in —
3418
  # somatic design and assessment only, never a clinical decision.
3419
+ def _score_variant_consequence(wt: str, patient: str, window: str, offset: int):
3420
+ """Ask Evo 2 what the PATIENT allele does, in its real sequence context.
3421
+
3422
+ The variant scored is the pathogenic one against the wild-type window
3423
+ — `<wt><1-based pos><patient>` — so a negative delta log-likelihood
3424
+ means the model finds the patient's sequence less likely than
3425
+ wild-type. That is supporting evidence about the lesion, not proof:
3426
+ zero-shot, no training on this locus, no validated relationship to
3427
+ clinical consequence.
3428
+
3429
+ Returns None when there is nothing to score (no window, or a lesion
3430
+ with no single-base correction). Never raises — a scoring failure must
3431
+ degrade the record's completeness, not the compile.
3432
+ """
3433
+ if not window or offset < 0 or offset >= len(window):
3434
+ return None
3435
+ if len(wt) != 1 or len(patient) != 1:
3436
+ return None # indels are not point substitutions
3437
+ if window[offset].upper() != wt.upper():
3438
+ return None # the verify pass will refuse this anyway
3439
+
3440
+ label = f"{wt.upper()}{offset + 1}{patient.upper()}"
3441
+ try:
3442
+ res = _dna_scoring.score_dna_variants(window, [label], tier="achilles")
3443
+ except Exception as exc: # noqa: BLE001
3444
+ app.logger.warning("consequence scoring failed: %s", exc)
3445
+ return {"ok": False, "label": label,
3446
+ "error": "the DNA model did not return a score"}
3447
+
3448
+ scores = (res or {}).get("scores") or []
3449
+ hit = next((s for s in scores if s.get("label") == label), None)
3450
+ if hit is None:
3451
+ skipped = (res or {}).get("skipped") or []
3452
+ return {"ok": False, "label": label,
3453
+ "error": (f"the model returned no score for {label}"
3454
+ + (f" (skipped: {skipped})" if skipped else ""))}
3455
+ return {"ok": True, "label": label, "delta_ll": hit.get("delta_ll"),
3456
+ "reference_ll": (res or {}).get("reference_ll"),
3457
+ "checkpoint": (res or {}).get("checkpoint"),
3458
+ "tier": (res or {}).get("tier")}
3459
+
3460
  @app.post("/api/compiler/compile")
3461
  def compiler_compile() -> Response:
3462
  gate = _dna_signin_gate("compiler")
 
3475
  if not wt and not patient:
3476
  return jsonify({"error": "missing 'wt_allele' and 'patient_allele'"}), 400
3477
 
3478
+ # Consequence scoring is OPT-IN. The compile itself is pure logic and
3479
+ # returns in milliseconds; an Evo 2 call is a rented 7B GPU and ~80s
3480
+ # cold, so running it on every compile would make the fast, honest
3481
+ # part of the tool feel broken. Off by default means the pass reports
3482
+ # `unavailable` which is true rather than quietly costing money.
3483
+ consequence = None
3484
+ if body.get("score_consequence"):
3485
+ consequence = _score_variant_consequence(wt, patient, window, offset)
 
 
3486
 
3487
  try:
3488
  report = _compiler.compile_report(
3489
  wt, patient, window=window, offset=offset,
3490
  germline=bool(body.get("germline")),
3491
  can_enumerate=True,
3492
+ consequence=consequence,
3493
  can_check_specificity=False,
3494
  )
3495
  except _compiler.GermlineRefused as exc:
dee/static/app.js CHANGED
@@ -12310,10 +12310,14 @@ if (document.readyState === 'loading') {
12310
 
12311
  const ICON = {
12312
  ok: '&#10003;', warn: '!', error: '&#10005;',
12313
- unavailable: '&#8212;', skipped: '&#183;',
12314
  };
 
 
 
12315
  const LABEL = {
12316
  ok: 'passed', warn: 'passed with caveat', error: 'refused',
 
12317
  unavailable: 'not run', skipped: 'skipped',
12318
  };
12319
 
@@ -12432,7 +12436,13 @@ if (document.readyState === 'loading') {
12432
  const win = ($('tcWindow').value || '').replace(/\s+/g, '').toUpperCase();
12433
  const offset = parseInt($('tcOffset').value, 10);
12434
  const btn = $('tcRun');
 
 
12435
  btn.disabled = true;
 
 
 
 
12436
  try {
12437
  const res = await fetch('/api/compiler/compile', {
12438
  method: 'POST',
@@ -12440,6 +12450,7 @@ if (document.readyState === 'loading') {
12440
  body: JSON.stringify({
12441
  wt_allele: wt, patient_allele: patient,
12442
  window: win, offset: isNaN(offset) ? -1 : offset,
 
12443
  }),
12444
  });
12445
  const data = await res.json();
@@ -12459,6 +12470,7 @@ if (document.readyState === 'loading') {
12459
  setError((err && err.message) || String(err));
12460
  } finally {
12461
  btn.disabled = false;
 
12462
  }
12463
  }
12464
 
 
12310
 
12311
  const ICON = {
12312
  ok: '&#10003;', warn: '!', error: '&#10005;',
12313
+ failed: '&#10005;', unavailable: '&#8212;', skipped: '&#183;',
12314
  };
12315
+ // 'failed' is NOT 'passed with caveat'. A model call that errored
12316
+ // assessed nothing, and saying otherwise is the soft version of
12317
+ // reporting an unrun pass as ok.
12318
  const LABEL = {
12319
  ok: 'passed', warn: 'passed with caveat', error: 'refused',
12320
+ failed: 'attempted, no result',
12321
  unavailable: 'not run', skipped: 'skipped',
12322
  };
12323
 
 
12436
  const win = ($('tcWindow').value || '').replace(/\s+/g, '').toUpperCase();
12437
  const offset = parseInt($('tcOffset').value, 10);
12438
  const btn = $('tcRun');
12439
+ const scoring = !!($('tcScore') || {}).checked;
12440
+ const origLabel = btn.textContent;
12441
  btn.disabled = true;
12442
+ // Only claim a wait when there IS one. The compile alone returns in
12443
+ // milliseconds; saying "scoring on the GPU" for a pure-logic pass
12444
+ // would be theatre.
12445
+ if (scoring) btn.textContent = 'Compiling + scoring on the GPU…';
12446
  try {
12447
  const res = await fetch('/api/compiler/compile', {
12448
  method: 'POST',
 
12450
  body: JSON.stringify({
12451
  wt_allele: wt, patient_allele: patient,
12452
  window: win, offset: isNaN(offset) ? -1 : offset,
12453
+ score_consequence: !!($('tcScore') || {}).checked,
12454
  }),
12455
  });
12456
  const data = await res.json();
 
12470
  setError((err && err.message) || String(err));
12471
  } finally {
12472
  btn.disabled = false;
12473
+ btn.textContent = origLabel;
12474
  }
12475
  }
12476
 
dee/static/index.html CHANGED
@@ -112,7 +112,7 @@
112
  <!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
113
  app.js change. Bump these numbers whenever you ship a frontend update —
114
  without them, users keep getting the stale file for up to a week. -->
115
- <link rel="stylesheet" href="/static/app.css?v=20260812-tc" />
116
  <!-- The work catalog + the draggable rail. Kept out of app.css so two new
117
  self-contained surfaces stay reviewable; every colour is an app.css
118
  token, so both themes work with nothing added. -->
@@ -1326,6 +1326,19 @@
1326
  <span class="field-opt">· 0-based</span></label>
1327
  <input id="tcOffset" class="tc-num mono" type="number" min="0" value="0" />
1328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1329
  <div class="tc-actions">
1330
  <button class="ghost" type="button" id="tcExampleOk"
1331
  title="A transition — base-editable, compiles">Example that compiles</button>
@@ -2830,7 +2843,7 @@
2830
  <!-- Cloning reference data must load before app.js so the Designer
2831
  can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
2832
  <script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
2833
- <script src="/static/app.js?v=20260812-tc" defer></script>
2834
  <!-- The decision trace, BEFORE cockpit.js: applyEvent calls TDTrace.push
2835
  on the very first event, and both are `defer`, so document order is
2836
  load order. Loading it after would drop the opening events of a
 
112
  <!-- ?v= query bumps invalidate browser + iframe asset caches when app.css /
113
  app.js change. Bump these numbers whenever you ship a frontend update —
114
  without them, users keep getting the stale file for up to a week. -->
115
+ <link rel="stylesheet" href="/static/app.css?v=20260812-tc2" />
116
  <!-- The work catalog + the draggable rail. Kept out of app.css so two new
117
  self-contained surfaces stay reviewable; every colour is an app.css
118
  token, so both themes work with nothing added. -->
 
1326
  <span class="field-opt">· 0-based</span></label>
1327
  <input id="tcOffset" class="tc-num mono" type="number" min="0" value="0" />
1328
 
1329
+ <label class="primer-toggle">
1330
+ <input type="checkbox" id="tcScore" />
1331
+ <span class="primer-toggle-text">
1332
+ <strong>Score the variant with Evo 2</strong>
1333
+ <span class="field-hint">Asks the genome model what the
1334
+ <em>patient's</em> allele does in this exact sequence context. Off by
1335
+ default: the compile itself is instant logic, while this is a rented
1336
+ 7B GPU (~25s warm, ~80s cold). Leaving it off makes the consequence
1337
+ pass report <strong>not run</strong> — which is the truth — rather
1338
+ than quietly spending money.</span>
1339
+ </span>
1340
+ </label>
1341
+
1342
  <div class="tc-actions">
1343
  <button class="ghost" type="button" id="tcExampleOk"
1344
  title="A transition — base-editable, compiles">Example that compiles</button>
 
2843
  <!-- Cloning reference data must load before app.js so the Designer
2844
  can read VECTORS / ENZYMES / CLONING_METHODS / TAGS / LINKERS. -->
2845
  <script src="/static/cloning_db.js?v=20260530-ui-polish" defer></script>
2846
+ <script src="/static/app.js?v=20260812-tc2" defer></script>
2847
  <!-- The decision trace, BEFORE cockpit.js: applyEvent calls TDTrace.push
2848
  on the very first event, and both are `defer`, so document order is
2849
  load order. Loading it after would drop the opening events of a
tests/test_compiler.py CHANGED
@@ -225,12 +225,56 @@ def test_specificity_still_carries_its_caveat_when_it_runs():
225
  assert "GUIDE-seq" in spec.detail
226
 
227
 
228
- def test_consequence_pass_states_it_is_zero_shot_even_when_available():
229
  r = C.compile_report("G", "A", window=WINDOW, offset=0,
230
- can_score_consequence=True)
231
- d = _by_name(r)["consequence"].detail
232
- assert "zero-shot" in d
233
- assert "no validated relationship to clinical outcome" in d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
 
236
  def test_a_refused_lesion_skips_the_rest_rather_than_reporting_ok():
 
225
  assert "GUIDE-seq" in spec.detail
226
 
227
 
228
+ def test_consequence_reports_the_number_when_scoring_actually_ran():
229
  r = C.compile_report("G", "A", window=WINDOW, offset=0,
230
+ consequence={"ok": True, "label": "G1A", "delta_ll": -4.2})
231
+ p = _by_name(r)["consequence"]
232
+ assert p.status == "ok"
233
+ assert "-4.2000" in p.detail
234
+ assert "less likely than wild-type" in p.detail, "direction stated in words"
235
+ assert "zero-shot" in p.detail
236
+ assert "no validated relationship to clinical outcome" in p.detail
237
+
238
+
239
+ def test_a_positive_delta_is_described_in_the_other_direction():
240
+ r = C.compile_report("G", "A", window=WINDOW, offset=0,
241
+ consequence={"ok": True, "label": "G1A", "delta_ll": 1.5})
242
+ assert "more likely than wild-type" in _by_name(r)["consequence"].detail
243
+
244
+
245
+ def test_reachability_alone_never_makes_the_consequence_pass_succeed():
246
+ """The bug this replaced: the pass took a capability flag and reported
247
+ 'passed' whenever the model was merely reachable, so the UI said the edit
248
+ had been assessed when nothing had been scored."""
249
+ r = C.compile_report("G", "A", window=WINDOW, offset=0)
250
+ assert _by_name(r)["consequence"].status == "unavailable"
251
+
252
+
253
+ def test_a_failed_scoring_attempt_is_failed_not_passed_with_caveat():
254
+ """'passed with caveat' on a model call that errored is the soft version
255
+ of reporting an unrun pass as ok — in both cases nothing was assessed."""
256
+ r = C.compile_report("G", "A", window=WINDOW, offset=0,
257
+ consequence={"ok": False, "label": "G1A",
258
+ "error": "backend refused"})
259
+ p = _by_name(r)["consequence"]
260
+ assert p.status == "failed"
261
+ assert p.status != "warn", "must not read as a pass"
262
+ assert "backend refused" in p.detail
263
+ assert "assumed-benign" in p.detail
264
+
265
+
266
+ def test_a_failed_pass_leaves_the_record_incomplete():
267
+ """Never run and attempted-but-failed leave the same hole."""
268
+ r = C.compile_report("G", "A", window=WINDOW, offset=0,
269
+ consequence={"ok": False, "error": "boom"})
270
+ assert "Assess edit consequence" in r.incomplete_because
271
+
272
+
273
+ def test_a_failed_consequence_does_not_block_the_compile():
274
+ """The lesion still routes; only the record is incomplete."""
275
+ r = C.compile_report("G", "A", window=WINDOW, offset=0,
276
+ consequence={"ok": False, "error": "boom"})
277
+ assert r.compiled
278
 
279
 
280
  def test_a_refused_lesion_skips_the_rest_rather_than_reporting_ok():