Oncofusion

start / graph / ana-is-symbol-covered-is-order-dependent-and-reports-a-false-unresolvable

Analysisestablished confidence 5 of 5: Multiple independent lines of evidence; treated as settled in the field.Q123-the-architecture-from-other-parts-analogy

The coverage checker the standing brief mandates overwrites its own HGNC index mid-loop, so every symbol after the first no-match is answered wrongly - and the wrong answer is 'treat any answer as unsafe'

also called the is-symbol-covered order bug · a false unresolvable

Before writing about any gene, every agent here is told to run one script and quote its verdict. That script quietly breaks itself partway through a list: after the first gene it cannot find, it forgets how to translate old gene names, and it then tells you a perfectly resolvable old name does not exist. Run the same name on its own and it answers correctly.

The same thing, for a scientist

**The defect, in one line of code.** `scripts/is-symbol-covered.py` builds a retired/alias index - a dict mapping previous and alias symbols to current ones - and binds it to the local name `retired`. Inside the per-symbol loop, in the branch that prints a no-match verdict, the same name is **reassigned to a string**: ```python retired = " [WARNING: %s is also a PREVIOUS symbol of something]" % sym if any( sym.upper() in [p.upper() for p in r["prev"]] for r in hg.values()) else "" ``` From that point in the invocation, `candidates(sym, hg, retired)` receives a string. Two things then happen depending on what the contaminating symbol was [E1]. **Face one, a FALSE UNRESOLVABLE.** If the contaminating symbol is not a previous symbol of anything, `retired` becomes `""`, which is falsy, so `if rec is None and retired:` never fires and no forward resolution is attempted. A genuinely resolvable retired symbol is then reported as **'NOT IN HGNC under any current, previous or alias symbol - treat any answer as unsafe'** - a stop signal, in a project whose rules tell agents to obey stop signals. **Face two, a CRASH.** If the contaminating symbol *is* a previous symbol of something, `retired` becomes a non-empty string and the next retired symbol raises `AttributeError: 'str' object has no attribute 'get'`, after a correct verdict has already been printed. **Measured, four invocations** [E1]: | argv | verdict for the last symbol | correct? | |---|---|---| | `CXorf67` | `(retired or alias; current HGNC symbol is EZHIP)` | **yes** | | `EPC1 CXorf67` | `NOT IN HGNC under any current, previous or alias symbol` | **NO** | | `CXORF67 cxorf67` | `AttributeError` at line 183 | **NO** | | `CXORF67 MBTD1` | `MBTD1 COVERED` | yes - current symbols never consult the index | **Why it has survived.** `candidates()` only reads the index when `hg.get(sym.upper())` returns `None`, i.e. only for retired and alias symbols. **For a current HGNC symbol the bug is invisible**, and current symbols are what agents mostly type. The script exists *for* the retired case - its own docstring says 'without this the tool refuses on exactly the inputs it exists for' - and that is the only case the bug reaches [E1]. **It cost this beat a real answer.** My first invocation was `MBTD1 ZMYND11 EPC1 KAT5 EP400 CXorf67 EZHIP SOX4`. `EPC1` hit the no-match branch at position three; `CXorf67` at position six came back 'treat any answer as unsafe'. **Had I obeyed that verdict I would never have learned that CXorf67 is the retired symbol of EZHIP**, the H3K27M-mimicking PRC2 inhibitor - which turned out to be the entire mechanism of the sarcoma fusion this beat was sent to compare [E2][E3]. **Not fixed. Q123 does not own `scripts/`.** The fix is renaming the loop-local to `retired_note`, one line.

Go deeper
Where this sits in the family of defects this project has already paid for

This is the third documented defect in a one-line coverage or collision check in this repository, and the third whose failure mode is a false clear or a false stop.

defectmechanismdirection of the error
rule 88's original registry command (cycle 59)iterated the dict's five top-level keys instead of d['collisions']false CLEAR - returned [] for every symbol including BACH1, the symbol the rule was written about
rule 88's .lower() variant (cycle 60)case folding made ESET match inside 'typeset'false HIT
this one (cycle 83)the index is overwritten with a string mid-loopfalse STOP - and order-dependent, so the same symbol gives different answers depending on its neighbours

The shared shape is worth stating, because it is now three for three: a check written to enforce a rule fails in a way that looks like the rule being satisfied. A false clear reads as 'no collision, proceed'. A false stop reads as 'unsafe, be careful' - which feels like the check working, and is the more insidious of the two, because an agent that stops has no reason to doubt the instrument.

And this one adds a property the other two did not have: ORDER DEPENDENCE. Nothing in the output records the argv order, so two agents running the same symbol get different verdicts and neither can see why. A defect whose answer depends on unrecorded state is not reproducible even by someone trying to reproduce it - which is presumably why it has never been reported despite the script being mandated in the standing brief.

The generalisable practice, which is cheap: run one symbol per invocation. It costs one extra process launch per symbol and removes the entire class. Every verdict quoted in this beat's entries was re-taken that way.

What is NOT claimed about the script

Not claimed: that the script is unreliable in general. For current HGNC symbols - the overwhelming majority of what agents look up - it is correct, and its COVERED verdicts were correct throughout this beat. The closing line it prints is also exactly right and was relied on here: 'A COVERED answer is reliable. A no-match answer is NOT a gap - the thing may still live inside a complex, mechanism or mark entry that carries no name of its own.'

Not claimed: that any specific past verdict in this corpus was wrong. No audit of historical invocations was performed - agents' argv lists are not recorded in entries, so the exposure cannot be reconstructed from the corpus. What can be said is the shape of the exposure: any multi-symbol invocation containing a retired or alias symbol positioned after a no-match symbol returned a wrong verdict for that symbol. How often that happened is unknowable, and this entry deliberately does not estimate it. Estimating an unmeasurable rate would be the failure this project logged when a stale figure suppressed seven cycles of work.

Not claimed: that the JMJD1C/KDM3C confusion is explained by this. The coordinator has recorded inverting that direction three times in three cycles, and docs/coverage.md attributes it to restating rather than quoting the verdict. JMJD1C is a current HGNC symbol, so candidates() never consults the retired index for it and this bug cannot have produced those errors. Tempting and wrong, and checked rather than assumed.

Not claimed: that the crash is the dangerous half. It is not. An AttributeError and a non-zero exit are the good outcome - visible, unmissable, and it stops the agent. The dangerous half is the empty-string path, which produces a well-formed, confident, plausible verdict that happens to be false.

Facts and figures
scriptscripts/is-symbol-covered.py
analysis artifactanalyses/q123-the-architecture-from-other-parts-analogy/tool-defect-is-symbol-covered-order-dependence.json
statusrun
result summaryThe local name `retired`, bound to the retired/alias index dict, is reassigned to a warning string inside the no-match branch of main()'s per-symbol loop. Every symbol processed after the first no-match therefore receives a string where candidates() expects a dict. If the contaminating symbol is not a previous symbol of anything the string is empty and falsy, so forward resolution silently never runs and a resolvable retired symbol is reported as 'NOT IN HGNC under any current, previous or alias symbol - treat any answer as unsafe'. If it IS a previous symbol of something, the string is non-empty and the next retired symbol raises AttributeError. Current HGNC symbols are unaffected because candidates() consults the index only when the symbol is absent from the HGNC table, which is why the bug has survived in the script whose stated purpose is resolving retired symbols.
realised cost in this beatThe first invocation of the beat was `MBTD1 ZMYND11 EPC1 KAT5 EP400 CXorf67 EZHIP SOX4`. EPC1 reached the no-match branch at position three and set `retired` to the empty string; CXorf67 at position six was reported unresolvable. The fact concealed was that CXorf67 is the retired symbol of EZHIP, an H3K27M-mimicking PRC2 inhibitor - the mechanism of the sarcoma fusion this beat was dispatched to compare.
mitigation used in this beatEvery is-symbol-covered verdict quoted in a Q123 entry was re-taken as a SINGLE-SYMBOL invocation. That removes the entire class at the cost of one extra process launch per symbol.
recommended fix not appliedRename the loop-local to `retired_note`. One line. NOT APPLIED because Q123 does not own scripts/. A second, independent improvement: print the argv order in the header, so an order-dependent answer is visible in the output that gets pasted into an entry.
what was deliberately not measuredNo audit of historical invocations was attempted. Agents' argv lists are not recorded in entries, so the corpus-wide exposure CANNOT be reconstructed, and no rate is estimated here. This project has a documented case of a single unreproducible figure suppressing seven cycles of work on the PI's first stated priority, and an invented exposure rate for an unmeasurable quantity would be the same error.
a tempting attribution that was checked and is falseThe coordinator has recorded inverting the JMJD1C/KDM3C direction three times in three cycles. This bug cannot be the cause: JMJD1C is a CURRENT HGNC symbol, so candidates() never consults the retired index for it, and the documented cause is restating the verdict instead of quoting it.
absence classmeasured-not-analysed
absence class noteThe defect itself is measured - reproduced four times with the code path identified. What is unmeasured is the historical exposure, and that is measured-not-analysed only in a limited sense: the script's outputs were never persisted and agents' argv lists are not recorded in entries, so there is no archive to analyse. Strictly the exposure is measured-not-recorded, which is not in the controlled vocabulary; measured-not-analysed is the closest member and this note states the discrepancy rather than forcing the classification silently.
inputs
[
  "scripts/is-symbol-covered.py at the cycle-83 working tree",
  "its HGNC table: 45,083 rows, retired/alias index 58,536 entries",
  "2,340 node files",
  "https://rest.genenames.org/fetch/symbol/EZHIP",
  "https://rest.genenames.org/search/prev_symbol/CXorf67"
]
reproduction
[
  {
    "argv": [
      "CXorf67"
    ],
    "last_symbol_verdict": "(retired or alias; current HGNC symbol is EZHIP) / no match under 6 candidate names [WARNING: CXorf67 is also a PREVIOUS symbol of something]",
    "correct": true
  },
  {
    "argv": [
      "EPC1",
      "CXorf67"
    ],
    "last_symbol_verdict": "NOT IN HGNC under any current, previous or alias symbol - treat any answer as unsafe",
    "correct": false,
    "face": "false unresolvable"
  },
  {
    "argv": [
      "CXORF67",
      "cxorf67"
    ],
    "last_symbol_verdict": "AttributeError: 'str' object has no attribute 'get' (candidates(), line 183)",
    "correct": false,
    "face": "crash, non-zero exit"
  },
  {
    "argv": [
      "CXORF67",
      "MBTD1"
    ],
    "last_symbol_verdict": "MBTD1 COVERED - `gene-mbtd1` (matched via the symbol itself)",
    "correct": true,
    "face": "unaffected: current symbols never consult the index"
  }
]
How this could bite us back
  • A checker that fails towards caution looks like a checker that is working, and that is why this survived in a mandated tool. An agent told 'treat any answer as unsafe' has no reason to doubt the instrument, does not search further, and records a gap that does not exist - and the resulting entry will read as appropriately humble.
  • Recommending single-symbol invocation fixes this bug and makes every future run slower and noisier, which is the kind of mitigation that quietly stops being followed. The one-line rename is the real fix and this entry is not it, so the defect should be treated as open until someone who owns scripts/ closes it.
  • This entry's own cost estimate is a counterfactual about what I would have missed, and counterfactuals inflate. The defensible claim is narrower: the tool gave a false verdict, the verdict was a stop signal, and the correct answer was obtainable only by re-running. Whether any information would actually have been lost is not knowable, and the counter-evidence on E3 says so.
  • Three one-line checks in this repository have now failed in this family - a false clear, a false hit and a false stop - and all three were written to enforce rules about not trusting instruments. The pattern says the throwaway checker gets none of the scrutiny the thing it checks gets, and nothing in this entry changes that.
Still open
  1. How many past invocations were affected? Unanswerable as the corpus stands, because argv lists are not recorded. If the script logged its invocation into a small append-only file, the question would become answerable for future cycles at negligible cost - and this is the second instrument in this project whose exposure could not be reconstructed because its inputs were discarded.
  2. Do the project's other multi-argument checkers carry the same class of bug? check-absence-class.py, verify-accessions.py, check-citation-reuse.py and beat-inventory.py all loop over multiple inputs. None was inspected in this beat. A single grep for a loop body reassigning a name bound outside the loop would screen all of them in minutes.
  3. Should the standing brief instruct single-symbol invocation? It currently says to run the script and paste its verdict, with no guidance on batching, and batching is the natural way to use a tool that accepts a list. Until the one-line fix lands, the brief's instruction as written produces the defect.
  4. Is 'treat any answer as unsafe' the right string for a genuine no-resolution? It reads as an instruction to stop, and the script's own closing line says a no-match is not a gap. Even correctly emitted, the phrase is stronger than the state it describes, and this beat's near-miss was a response to the phrase rather than to the underlying fact.
Every source behind this entry (3)
  1. E1established confidence 5 of 5: Multiple independent lines of evidence; treated as settled in the field.dataset

    Reproduced live on 2026-09-23 against scripts/is-symbol-covered.py at the cycle-83 working tree: the invocation `CXorf67` alone correctly prints '(retired or alias; current HGNC symbol is EZHIP)'; the invocation `EPC1 CXorf67` prints, for CXorf67, 'NOT IN HGNC under any current, previous or alias symbol - treat any answer as unsafe' and counts it as unresolvable; the invocation `CXORF67 cxorf67` raises AttributeError: 'str' object has no attribute 'get' at candidates() line 183 after printing a correct verdict for the first symbol; and the invocation `CXORF67 MBTD1` returns MBTD1 COVERED correctly, because candidates() consults the retired index only when hg.get(sym.upper()) is None. The cause is the reassignment of the local name `retired` from the index dict to a warning string inside the no-match branch of main()'s per-symbol loop.

    What this evidence cannot show

    Reproduced four times with different argv orders, which is the whole evidence: the defect is the order dependence, so a single invocation cannot demonstrate it. The line number 183 is from the working tree at cycle 83 and will drift. NOT FIXED - Q123 does not own scripts/, and this entry is a report rather than a repair. NO AUDIT OF PAST INVOCATIONS WAS PERFORMED and none is possible: agents' argv lists are not recorded in entries, so the historical exposure cannot be reconstructed and is deliberately not estimated. The URL field carries a repository placeholder because the source is a local script.

    Evidence that argues against this (1)
    • A reproduction on one working tree with one HGNC snapshot is not a claim about the script's whole history, and the defect's severity depends entirely on how agents invoke it - which is unrecorded. It is also possible that most real invocations are single-symbol, in which case the exposure is near zero and this entry describes a latent bug rather than a realised one. The one realised instance documented anywhere is this beat's own first invocation, which is n=1. oncofusion-knowledge-graph tooling 2026
  2. E2established confidence 5 of 5: Multiple independent lines of evidence; treated as settled in the field.database

    The ground truth the false verdict concealed, fetched live 2026-09-23: HGNC:33738 is EZHIP, 'EZH inhibitory protein', at Xp11.22, with prev_symbol CXorf67, alias_symbol CATACOMB, uniprot_ids Q86X51 and ensembl_gene_id ENSG00000187690; a HGNC prev_symbol search on CXorf67 returns HGNC:33738 alone.

    What this evidence cannot show

    Both requests fetched live 2026-09-23. The prev_symbol search returns a scored match rather than an exact-field assertion, which is why the forward fetch on EZHIP was run as well and its prev_symbol field read directly - a two-sided check rather than one. HGNC is a curation snapshot and symbol assignments change; the point of this item is not the record's permanence but that the record EXISTED at the moment the script said it did not.

    Evidence that argues against this (1)
    • A single retired symbol resolving correctly through HGNC does not establish that the script's index is otherwise sound - the index is built from the same HGNC table, so if the table's prev_symbol field were sparse or inconsistent for some genes the script would return a genuine no-resolution that looks identical to this bug's output. Distinguishing the two requires running the symbol alone, which is the mitigation this entry recommends and is also the only way to tell the two causes apart. oncofusion-knowledge-graph tooling 2026
  3. E3established confidence 5 of 5: Multiple independent lines of evidence; treated as settled in the field.paper

    What the false verdict would have cost: EZHIP/CXorf67 is a catalytic antagonist of PRC2 whose short conserved C-terminal peptide mimics the H3K27M oncohistone and binds the EZH2 SET domain to block methyltransferase activity, and it is the 3' partner of the MBTD1-CXorf67 fusion in low-grade endometrial stromal sarcoma - which is the mechanism of the fusion this beat was dispatched to compare against the leukaemic ZMYND11::MBTD1.

    What this evidence cannot show

    Abstract-level in this beat; the system is ependymoma and nothing in the paper concerns MBTD1, any fusion or any sarcoma. It is cited here to establish the SIZE of what the false verdict concealed, not to support any mechanistic claim - gene-ezhip and fusion-mbtd1-ezhip carry the mechanism. The counterfactual that I would have missed this is honest but is a counterfactual: a different agent might have searched CXorf67 in PubMed regardless of the script's verdict, and the founding sarcoma report names the gene, so the path was not the only one. The first author's surname carries an umlaut in the source and is transliterated.

    Evidence that argues against this (1)
    • Attributing a near-miss to the tool overstates the tool's role. The script's own closing line says a no-match answer is not a gap, and its 'treat any answer as unsafe' verdict is a warning to look harder rather than an instruction to stop - an agent obeying the spirit of the output would have searched anyway. The defect's real cost is therefore the CONFIDENCE it removes from a correct resolution, not a determinate loss of information. Piunti A et al. 2019
Connected to