Spaces:
Sleeping
Sleeping
avoigt1121
merge: label-join decided as route (c) β no cBioPortal PDAC subtype call exists
83b08b3 | # pdac-genomics-agent β memory | |
| Current status + history. Architecture β `CLAUDE.md`. Backlog β `TODO.md`. | |
| --- | |
| ## 2026-08-14 β The subtype-label search is closed: no cohort exists, and the tab now says so | |
| Re-opened the "find a cohort with variant data **and** an in-metadata subtype label" item, and it | |
| closes as a **negative result with evidence** rather than a curation. The 2026-07-28 sweep was | |
| re-run, not trusted: **539 cBioPortal studies** (one added since), **19,165 clinical attributes**, | |
| **680** of them on pancreatic studies. **No study carries a Moffitt / Bailey / Collisson / PurIST | |
| molecular-subtype attribute.** | |
| Three near-misses, all correctly rejected β and the third is new to the record: | |
| | candidate | values | why it is not a subtype label | | |
| |---|---|---| | |
| | `paad_tcga_pan_can_atlas_2018.SUBTYPE` | `"PAAD"` Γ152 | a disease code, not a subtype | | |
| | `paad_iatlas_prince_2022.IMMUNE_SUBTYPE` | C1βC6 | Thorsson immune clusters, different taxonomy | | |
| | **`pdac_msk_2024.GENOMIC_GROUP`** ("Genomic Subtype") | `KRAS-MUT` 2209 / `Other-MAPK-MUT` 76 / `MAPK-WT` 51 | **DNA-derived** β it *is* mutation status. Joining it against KRAS would return a near-perfect association that is pure tautology wearing a p-value. | | |
| `GENOMIC_GROUP` is the one worth remembering: its display name is literally "Genomic Subtype", it | |
| sits on our largest cohort, and it would have produced the most persuasive-looking result in the | |
| repo. The closed vocabulary (ADR-0006 Β§3) rejects it on values, and it is not in | |
| `SUBTYPE_ATTRIBUTES` so it is never even probed β the guard held twice. | |
| **So the documented fallback was taken.** The Variant Γ subtype tab now states plainly that the | |
| join is **orchestrator-only today**, names why each near-miss is refused, and says a routed result | |
| is the *correct answer rather than a bug* β the failure mode being a user who clicks "Test | |
| association" seven times, gets a route every time, and reads it as broken. The controls stay live: | |
| a cohort that does carry a label (or one named explicitly via `subtype_attribute`) still joins. | |
| **A false claim in ADR-0006 was corrected, not quietly patched.** Its Context Β§2 cited | |
| `paad_tcga β MOFFITT_SUBTYPE` as a live local-join example, and Β§1's sample payload still uses it. | |
| That cohort has never carried it. A dated correction block now records this: **the contract is | |
| unaffected** β the local branch is specified, implemented and fixture-tested, and is correct the | |
| moment a labelled cohort is registered. What changed is only its *status*. | |
| **Two tests lock it** (`tests/test_variant_by_subtype.py`): every curated artifact must route to | |
| the orchestrator (with a non-empty-list guard so it cannot pass vacuously), and the UI caution must | |
| carry the "no curated cohort does today" sentence. The first is the useful one β a future curation | |
| that *does* add a label fails it, forcing the UI copy to be revisited alongside the data instead of | |
| silently going stale. **247 tests pass** (was 245). | |
| Curation-time API only (ADR-0005 C4); nothing was curated, so no artifact changed. | |
| --- | |
| ## 2026-08-14 β the same question from the orchestrator side: route (a) was already built | |
| Ran independently of, and reached the same negative result as, the entry above β **read that one | |
| for the portal-wide sweep and the near-miss table; it is the fuller record.** Confirmed here on a | |
| narrower probe (all attributes of the 7 curated studies + all 59 pancreas-matching studies), with | |
| one addition worth keeping: `paad_cptac_2021`'s full 43-attribute list contains nothing | |
| subtype-like at all, so the "CPTAC carries Moffitt" intuition from the registry table does not | |
| transfer β that label lives in the **expression** manifest (`cptac_pda_counts`), the side a DNA | |
| agent structurally does not read. "Curate in a labelled cohort" was never a small unlisted- | |
| attribute fix; there is no attribute to list. | |
| **What this lane adds: the route with the real product value is already implemented and live, in | |
| the other repo.** `pdac-analysis-orchestrator` has `cross_modality_join.py` (its ADR-0003), | |
| `router._maybe_cross_modality_join`, and three test files, and its `agents.yaml` registers this | |
| agent with `cross_modality_join: {status: implemented, partner_agent: pdac_subtype_agent}`. Its | |
| `EXPRESSION_COUNTERPART` maps `paad_tcga β tcga_paad_tpm` at **150/150** sample ids matched β after | |
| a barcode-normalisation fix, since cBioPortal's 15-char barcodes vs the h5ad's vial letter matched | |
| 0/150 exactly at first β and `paad_cptac_2021 β cptac_pda` at 131/131. Its statistics are a | |
| deliberate mirror of `subtype_join.contingency_and_test`, so one cohort gets one answer whichever | |
| route reaches it. | |
| So the three-route question in `TODO.md` resolves to **(c)** on evidence, not preference: (b) is | |
| refuted upstream and (a) needs no scoping because it already ships. Nothing to build; the in-agent | |
| half stays correct, tested and dormant, and the only real defect was wording β now merged with the | |
| other lane's tab copy (both survived; neither was discarded). | |
| Left for the orchestrator repo, not touched from here (file partitioning): its `agents.yaml` | |
| `dataset_clarification` still calls `paad_cptac_2021` "the one cohort the variant Γ subtype join | |
| can run on" β stale, and contradicted by a comment four lines above it. Display-only text. | |
| ## 2026-08-14 β the 300,000-pixel raw-result accordion (`2f6f12d`, dev-verified) | |
| **The TODO's stated cause was wrong, and acting on it would not have fixed the page.** The item | |
| blamed `samples_profiled` printing 2,336 ids three times. Measured: that is 7,008 lines of 21,187. | |
| `per_sample` + `provenance` are ~13,200 more β KRAS alone is altered in 2,188 of 2,336 samples. | |
| Eliding the named block would have left ~14,000 lines and a still-unusable page. **Measure the | |
| payload before believing a size diagnosis**; the block that is easiest to name is not necessarily | |
| the one that dominates. | |
| **Nothing was removed from the payload, and that was the constraint, not a courtesy.** | |
| `samples_profiled` is the only way a machine caller can tell wild-type from never-sequenced (the | |
| `per_sample_contract` in `deploy/orchestrator_registration.yaml`); dropping it is exactly what made | |
| the orchestrator's join report every gene as "no variation in alteration status". So the fix is a | |
| DISPLAY split: `_as_json` (machine) is untouched and was verified over `gradio_client` to still | |
| return 3 Γ 2,336 ids as lists, while the UI gets `_ui_json` over the same object. | |
| **Four tiers, widest first.** A summary table (gene Γ modality, each cell carrying the percentage | |
| AND the fraction β `93.7% (2188 / 2336)`, because the denominator is the thing people get wrong); | |
| the three charts; a download of the complete payload; then the abridged JSON. The table is what | |
| makes the abridgement acceptable β the numbers are on the page rather than inside a collapsed | |
| accordion. | |
| **The abridged block is deliberately a different SHAPE from what it replaces** β a dict where a | |
| list was, stating the true count. A truncated list that still looks like a list is how a reader | |
| ends up believing a cohort has 8 profiled samples. Small payloads (denial, refusal, error) are | |
| under the threshold and pass through verbatim, so the ADR-0004 denial-visibility guarantee is | |
| untouched; the anonymous click-through on the dev Space confirms it live. | |
| **`gr.Code(max_lines=...)` DOES NOT WORK in gradio 6.18.** Measured in the browser: the code box | |
| still rendered 23,323px tall with `max_lines=25` set. The height cap is CSS on an `elem_id`, and | |
| that is what makes the page height constant rather than a function of the payload β abridgement | |
| alone only got ~300,000px down to ~25,681px. Final: **2,818px with the accordion open.** If this | |
| regresses, check the CSS before checking the abridgement. | |
| **Empty summary cells NAME their absence** ("not on DNA panel" / "not in cohort" / "not curated") | |
| and are never blank or zero β the same anti-padding rule as the charts. NRG1 on `pdac_msk_2024` is | |
| the case that proves it: `0.3% (6 / 2336)` in the SV column, refused in the other two. | |
| The clearing frame blanks the two new slots as it does the charts. A stale download link is the | |
| worst version of that bug: the file leaves the page entirely and gets opened later with nothing to | |
| say which query produced it. | |
| --- | |
| ## 2026-08-14 β the modality radio was out of date with ADR-0008 (`9f3852c`, dev-verified) | |
| The TODO item asked a judgment question β should the UI offer a third modality choice, or should | |
| the test suite own the `unsupported_modality` case alone? **Both, and they are separable questions | |
| that were tangled together.** | |
| **The refusal can never be reached from the radio, whatever its choices.** A `gr.Radio` validates | |
| the submitted value against its OWN `choices`, so gradio returns the generic | |
| `Value: 'fusion' β¦ is not in the list of choices` before `variant_by_subtype` runs. Widening the | |
| radio moves that wall; it does not open it. The only surface that can carry a free-form modality is | |
| the `gr.api` endpoint the orchestrator calls, so that is where the refusal is now exercised β | |
| permanently, not as a stopgap. **"Make the refusal reachable from the UI" was the wrong frame.** | |
| **But the radio was independently wrong.** `["mutation", "cnv"]` was a hand-written literal that | |
| outlived the SV modality (ADR-0008) for its whole existence: the tool accepted `sv`, | |
| `deploy/orchestrator_registration.yaml` published `sv`, and the only surface refusing it was the UI | |
| β offering a NARROWER contract than the machine endpoint. That is the same family as the modality | |
| fallback the refusal exists to prevent (a surface answering a different question than its label), | |
| one level up. Choices are now derived from a new `variant_by_subtype.SUPPORTED_MODALITIES`; the | |
| regression test asserts **parity with the tool's closed set**, not the refusal, so it fails on the | |
| old literal *and* on a future fourth modality. Verified failing against the two-choice radio first. | |
| **Checked the SV choice was not decorative before adding it.** On `ccle_broad_2019` (SV plus a | |
| named subtype attribute) the join runs and returns contingency counts; most genes fall below | |
| `MIN_ALTERED_FOR_TEST` and report `testable: false`. Sparse-but-labelled with the SV rarity caveat | |
| beats a capability the UI hides β the alternative is the UI deciding for the reader that an honest | |
| answer isn't worth showing. | |
| **A bug this change would otherwise have shipped.** `unavailable_modality` and | |
| `unsupported_modality` had no branch in `_subtype_caution_md` and fell through to `caveats`, which | |
| a refusal does not carry β the box stayed `visible=False` and the reader saw an empty plot and | |
| nothing else. **That is the ADR-0004 invisible-denial failure in a second place**, harmless only | |
| while unreachable: 5 of the 7 curated cohorts publish no SV, so adding the choice made | |
| `unavailable_modality` an ordinary click result. Both routes now render; `unavailable_modality` | |
| also echoes `modality` so the answer can name its own subject. | |
| **Surveyed while deciding, worth keeping:** *no* curated cohort auto-detects a subtype label. Six | |
| carry only `CANCER_TYPE` + `ONCOTREE_CODE`; `ccle_broad_2019`'s `SUBTYPE` is histology, which | |
| `looks_like_pdac_subtype` rejects on purpose. So every registered cohort routes to the orchestrator | |
| unless the caller names `subtype_attribute` explicitly. Correct, but it means the label-join half | |
| and its modality argument are reachable only by the explicit path today β new TODO item. Also note | |
| the ordering: the label check precedes the modality check, so a bad modality on a label-less cohort | |
| reports `orchestrator`, not `unsupported_modality`. | |
| **Dev-verified live** on `anne-voigt/pdac-genomics-agent-dev` at `9f3852c`: `/config` serves | |
| `[mutation, cnv, sv]` with `mutation` as default; via `gradio_client` with an explicit | |
| `x-orchestrator-token` header, `"fusion"` and `"mutaton"` both return | |
| `route: "unsupported_modality"` with `supported_modalities: [cnv, mutation, sv]`, `""` and `" "` | |
| still resolve to `mutation`, and `sv` returns a real join. 255 tests pass. | |
| **PROMOTED TO PROD same day** (`f9cee52..f2d366c`, Annie's call). Gates clean β 255 tests, | |
| pip-audit "no known vulnerabilities", gitleaks 97 commits no leaks; hook mirrored to `github` | |
| first; build watched to `RUNNING` (the wedged-restart trap did not fire). Verified on prod: | |
| `/config` serves `[mutation, cnv, sv]`, `"fusion"` β `unsupported_modality`, blank β `mutation`, | |
| `sv` β a real join. **The first push was rejected non-fast-forward** β another lane promoted while | |
| the gates were running. Reconciled by **merge**, not rebase (those shas are published), then | |
| re-pushed. Worth remembering: run the gates, but re-fetch immediately before the push, because a | |
| clean gate run is not evidence the remote stood still. | |
| **Integration note:** the push to `hf-dev/main` fast-forwarded cleanly from `35722c3`, so the work | |
| is on the integration tip without a merge commit. The local `dev` branch is checked out in another | |
| lane's worktree and was left untouched β it needs only a pull. Not promoted to prod; that is Annie's | |
| call. No ADR claimed: this applies ADR-0008 to the UI surface rather than contesting it. | |
| --- | |
| ## 2026-08-13 β dev Space configured & verified: the gate is live on staging (no code change) | |
| The dev Space `anne-voigt/pdac-genomics-agent-dev` went from "RUNNING but unconfigured" to a | |
| valid staging target for gate changes. Anne entered the settings; this lane prepared the exact | |
| values and verified afterward. | |
| **Settings now on dev** β variables: `ACCESS_CONTROL=enforce`, `LOG_SINK=hf`, | |
| `LOG_SINK_HF_DATASET=anne-voigt/pdac-genomics-results-dev`; secrets: `ADMIN_IDS`, `ALLOWED_IDS`, | |
| `LOG_SINK_HF_TOKEN`. Two corrections to the TODO item as written: | |
| 1. **It omitted `LOG_SINK=hf`** β the dataset/token vars are inert while the sink defaults to | |
| `local` (`logging_sink.get_log_sink`). | |
| 2. **"`ADMIN_IDS`/`ALLOWED_IDS` as variables, matching prod" was wrong about prod** β checked via | |
| the HF API: prod holds them as *secrets* (only `ACCESS_CONTROL` is a visible variable). Dev now | |
| matches prod's actual posture. | |
| Also found while checking prod: **prod had no `LOG_SINK*` settings at all and its default dataset | |
| `anne-voigt/pdac-genomics-results` did not exist** β prod audit records were going to ephemeral | |
| `./run_logs` and dying on every restart. Spun off to a separate lane (which created the prod | |
| dataset and set the prod variables; its `LOG_SINK_HF_TOKEN` secret was still pending Anne at the | |
| time of writing β see the status fragment). | |
| **Verification (all live against the dev Space):** machine API with no credential β | |
| `machine_auth: no_token_header` denial; bogus token β `token_unresolved` fail-closed denial; | |
| valid token β resolves and returns the 36-gene panel. Audit records with `enforced: true` land in | |
| `pdac-genomics-results-dev` under `runs/`, and none in prod's dataset. Signed-out UI access is | |
| moot while the Space is private (HF's own wall precedes ours), and the anonymous-denial records | |
| prove the gate's fail-closed path regardless. Note learned: a *successful* `panel:api` call | |
| writes no audit record by design β `panel()` returns after the gate without `record_run`; only | |
| denials are recorded on that endpoint. | |
| Created the private dataset `anne-voigt/pdac-genomics-results-dev`. This item gated the | |
| "make the dev Space public?" decision β that is now unblocked. | |
| ## 2026-08-13 (latest) β dev Space PUBLIC; the staging lane is whole | |
| Anne flipped visibility the same day. Post-flip verification, all green: | |
| - `space_info` (anonymous) β `private: False`; `anne-voigt-pdac-genomics-agent-dev.hf.space` | |
| returns HTTP 200 with no cookies and renders the full app β the 404 / cross-origin-iframe / | |
| `__sign=` password-form maze from 2026-08-05 is gone. | |
| - Signed-out **click-through** on "Query variant status" β `{"status": "denied", "reason": | |
| "π Please sign inβ¦"}`. The ADR-0004 UI gate is enforced and fail-closed on staging, matching | |
| prod's public+gated model. | |
| - Allow-listed access answers with grounded numbers (machine path, same day: KRAS 136/150 = | |
| 90.7% on `paad_tcga`); audit records land in `pdac-genomics-results-dev` only. | |
| **Consequence: the dev lane can now verify that the Space answers, not merely that it boots.** | |
| Server-shaped changes (auth, secrets, artifact loading) no longer take their first real | |
| click-through in production. The 2026-08-05 caveat about local-run verification being the only | |
| option is retired. | |
| --- | |
| ## 2026-08-13 (later) β dev Space configured; gate verified pre-flip; only the public flip remains | |
| Anne set the dev Space settings the same day. Verified from here while the Space is still private | |
| (machine path works through privacy; only browser click-through doesn't): | |
| - Variables read via `HfApi.get_space_variables`: `ACCESS_CONTROL=enforce`, `LOG_SINK=hf`, | |
| `LOG_SINK_HF_DATASET=anne-voigt/pdac-genomics-results-dev`. `ADMIN_IDS`/`ALLOWED_IDS` are | |
| secrets β and that MATCHES prod (another lane checked prod the same day); the old TODO wording | |
| "as variables, matching prod" misdescribed prod. `CBIOPORTAL_CONTACT` deliberately unset. | |
| - Gate, three ways: allow-listed token in `x-orchestrator-token` β real grounded answer | |
| (KRAS 136/150 = 90.7% on `paad_tcga` β the post-denominator-fix number); garbage token β | |
| `token_unresolved`; no header β `no_token_header`. Fail-closed confirmed on staging. | |
| - Sink separation: the test runs wrote 6 `runs/*.json` into `pdac-genomics-results-dev`; | |
| prod's `pdac-genomics-results` has none. | |
| - β οΈ Tooling trap worth remembering: gradio 6's `Client(token=β¦)` does **not** send | |
| `x-hf-authorization` β without an explicit `headers={"x-orchestrator-token": β¦}` even a valid | |
| admin token reads as `no_token_header`. The 2026-07-28 "gradio_client attaches it automatically" | |
| note no longer holds on the pinned client. | |
| Remaining: Anne flips visibility β public (Settings UI), then the browser-level checks | |
| (`*.hf.space` loads, signed-out UI denied, signed-in allow-listed answers). | |
| --- | |
| ## 2026-08-13 β dev-Space visibility: recommendation written, flip awaits Anne | |
| The "public vs prod-only click-through" decision (TODO, open since 2026-08-05) now carries a | |
| worked recommendation + runbook in TODO.md: **make dev public with `ACCESS_CONTROL=enforce`**, | |
| matching prod's model. The load-bearing findings: the licensing argument for privacy is stale β | |
| `ccle_broad_2019` and `pdac_msk_2024` have been served by the *public* prod Space since | |
| 2026-07-28, with the Broad continuity clause discharged in-repo and the ND question resolved | |
| 2026-08-05, so a public dev Space adds no new data exposure; secrets are unaffected by | |
| visibility; the only genuine precondition is sequencing β the gate is DARK on dev, so secrets/ | |
| variables must be configured (and gate-verified via `gradio_client` + token, which works while | |
| private) BEFORE the flip. Both the secret-setting and the visibility flip need the Space settings | |
| UI, so this is Annie's action; verification steps are in the runbook. No push made from this lane. | |
| --- | |
| ## 2026-08-06 β legibility: the header names SV, and the chart no longer stacks (`408908b`, NOT deployed) | |
| **Header.** Title and the variant-status blurb both said "mutation and copy-number status". SV has | |
| been shipping since `0f39c3b` and was named nowhere a reader looks first. | |
| **The stacked chart was wrong twice over, not merely cramped.** A sample can be BOTH mutated and | |
| copy-number altered, so the segments are not disjoint and stacking double-counts; and the modalities | |
| have **different denominators** (`ccle_broad_2019`: 53 / 44 / 41 profiled; `paad_tcga`: 150 mutation | |
| vs 184 cnv), so the segments are not percentages of the same thing. A stacked bar asserts "these | |
| parts make up the whole". KRAS reached ~120% on an axis labelled "% altered". | |
| Now **one chart per modality**, each naming its own denominator, each a single series. Gradio's | |
| native BarPlot has no grouping control and the Space has no other plotting library, so splitting is | |
| the only honest option inside the current dependency set β and it deletes the categorical colour | |
| scale whose stale domain caused both wrong-answer bugs of 2026-08-05. | |
| **Two further distortions surfaced only by looking at the rendered result:** | |
| 1. Autoscale started the y-axis at the **data minimum** β a CCLE chart ran from 20%, so SMAD4 at | |
| ~21% looked like nothing beside KRAS at 90%. Baseline now pinned to zero. | |
| 2. Fixing the ceiling at 100 to match then made SV unreadable: NRG1 is 6/2336 = **0.26%**, a | |
| sub-pixel sliver β the same "reported as nothing" failure in new clothes. Ceiling is fitted per | |
| chart, floored so one tiny bar is not stretched to full height, capped at 100. | |
| **A third bug caught BEFORE shipping, which is the point.** The clearing frame first hid all three | |
| charts; the answer then re-showed them. That hideβshow flip lost the race for the third chart about | |
| half the time β it stayed unmounted (browser: "null does not exist"), so a cohort **with** structural | |
| variants rendered as mutation+CNV only, reading as "no fusions here". The clearing frame now blanks | |
| each chart's VALUE and leaves visibility alone. | |
| **Deployed `3ad3d85` 2026-08-07 and re-verified on the live prod Space:** header + tab blurb name | |
| the fusion modality; a fresh-load FIRST query draws all three charts (53 / 44 / 41 profiled); | |
| `paad_tcga` draws exactly two and no SV; narrowing to NRG1 draws one SV chart on a single click | |
| and widening back draws three with all 33 genes and KRAS present. Gates clean beforehand (237 | |
| tests, pip-audit, gitleaks). | |
| **What caught it was re-running the same query from a fresh load instead of accepting one good | |
| render.** The first render looked right; the failure only appeared on a first query after load, so a | |
| single happy screenshot would have shipped it. That is the third time in two days this exact class β | |
| a chart silently not showing β has appeared, and the first time it was caught before deploy. | |
| Verified across 4 fresh-load trials (including a click during hydration and the double-click that | |
| first exposed it), plus `paad_tcga` showing exactly 2 charts, plus both narrowβwide and wideβnarrow | |
| on single clicks. **237 tests** (was 235). | |
| --- | |
| ## 2026-08-05 (latest) β I shipped a regression to prod and caught it in the post-deploy click-through (`819da30`, NOT deployed) | |
| `23d37b6` went to prod and fixed the three real bugs. It also introduced a fourth, which the | |
| post-deploy click-through found: **on a query that NARROWED the gene set, the chart body lagged one | |
| render behind** β caution and legend updated, the bars kept the previous answer's data. One cohort's | |
| numbers under another cohort's warning, which is the same class of wrong answer the whole change set | |
| existed to remove. | |
| **`colors_in_legend` was the cause, and I had added it as the *cosmetic* half of the fix.** Passing a | |
| value that CHANGES between renders is what forced the colour scale to rebuild β which is what fixed | |
| the narrowβwide direction. It was never cosmetic; it was load-bearing for a reason I had not | |
| understood. Measured, each against `40ab3dd` as control: | |
| | build | narrow β wide | wide β narrow | | |
| |---|---|---| | |
| | `40ab3dd` (before) | marks dropped | correct | | |
| | `23d37b6` (`colors_in_legend`) | correct | **stale one render** | | |
| | `color_map` alone | marks dropped | correct | | |
| So the two configurations I had were **the same bug pointing in opposite directions**, and the | |
| direction I happened to test first was the one that looked fixed. | |
| **Also tried and rejected:** holding the modality categories open with NaN placeholder rows so the | |
| domain never changes shape. NaN poisons the y extent β no bars and no axis render at all. (Zeros | |
| would have worked visually and were never an option: NRG1 is not 0% mutated, it is unassayed.) | |
| **The fix stops relying on the plot diffing correctly.** `_ui_variant_status` is now a generator: it | |
| blanks the chart, then yields the answer. The worst intermediate state a reader can catch is an | |
| EMPTY chart, which claims nothing, rather than a stale one, which claims something false. Both | |
| directions verified repainting on a single click. `test_ui_handler_gating` reads the final frame via | |
| `_final()`; the ADR-0004 assertions are untouched and a denied path still yields exactly one frame. | |
| **Deployed `125b8a5` 2026-08-06** (gates clean: 234 tests, pip-audit, gitleaks; hook mirrored to | |
| `github` first). **Verified on the live prod Space in both directions, each on a single click** β | |
| CCLE wide β MSK+NRG1 narrow (correct NRG1 bar immediately, not CCLE's) β full panel wide (KRAS ~94% | |
| across all 33 genes). The OAuth session drops when the Space rebuilds, so sign in again before | |
| verifying; an anonymous query renders nothing visible because the plot is None and the JSON | |
| accordion is collapsed. | |
| **Three process lessons, all of them mine:** | |
| 1. **I explained away the symptom the first time.** I saw the stale chart locally, guessed "my click | |
| missed the button", and moved on. It had not missed. A surprising observation deserves one cheap | |
| check before a comfortable explanation. | |
| 2. **A fix verified in one direction is not verified.** Both bugs here are the same scale-rebuild | |
| fault; testing narrowβwide only was the whole gap. | |
| 3. **Green tests proved nothing about either.** Every version passed its own tests, because the tests | |
| asserted the payload and the hypothesis, not the rendered result across a SEQUENCE of renders. | |
| --- | |
| ## 2026-08-05 β first signed-in click-through since 2026-07-27; two display bugs found (`6c7b8cc`, NOT deployed) | |
| Everything shipped that day had been verified only by reading the deployed tree over `resolve/main` | |
| and by the local suite. That proves what is *served*, not that it *answers* correctly, and the panel | |
| had since gone 19 β 36 genes and gained a third modality. So: signed in as `anne-voigt` and ran real | |
| queries. **Every payload the Space produced was correct.** Both bugs are display-layer, which is why | |
| 219 passing tests saw neither β and which also means the orchestrator's machine path, reading JSON | |
| rather than pixels, was never affected. | |
| **Passed as specified:** NRG1 on `pdac_msk_2024` returns `assayed: false` *and* a valid `sv` block at | |
| 6/2336 (the ADR-0008 modality-scoped gate, live); `paad_tcga` names `sv` in `unavailable_modalities` | |
| with no SV numbers anywhere; BYOD refuses a `Fusion` MAF with `sv_not_supported` and accepts a clean | |
| one as `grounded=false`; licence caution + `citation.curation` ride on every answer; the machine | |
| endpoints still refuse a cookie-only caller. | |
| **The chart could show an empty panel while the JSON said 93.7%.** The colour scale's domain was | |
| derived from the current frame and never rebuilt when the next frame carried more categories. NRG1 | |
| alone β sv-only chart; the next full-panel query redrew with the legend still `["sv"]` and two thirds | |
| of the marks dropped. The y-axis rescaled to 0β100 β proof the data had arrived and only the colour | |
| scale was stale. Same staleness in the other direction put an `sv` swatch on `paad_tcga` under a | |
| caution saying it has no SV data: **one cause, both symptoms**, and it only loses data narrow β wide, | |
| which is exactly the direction a scientist moves (check one gene, then open the panel). | |
| Fixed by pinning `color_map` + per-answer `colors_in_legend`. | |
| **The tempting fix for that is wrong and there is now a test saying so.** Padding every gene to all | |
| three modalities would stabilise the category set β and report NRG1 as 0% mutated on a cohort that | |
| never sequenced it. That is the false negative `assayed: false` exists to prevent. Stabilise the | |
| SCALE, never the DATA. | |
| **`caveats` never reached the variant-status tab.** Only `_subtype_caution_md` read it, so | |
| `ccle_broad_2019`'s uncharacterized-SV warning β generated correctly, carried correctly β stopped one | |
| function short of the screen, on the one tab where SV data is actually queried. A warning that exists | |
| so "no in-frame fusions" cannot be read as biology is not doing its job from inside a collapsed JSON | |
| accordion. | |
| **226 tests pass** (was 219). The new `tests/test_plot_and_caveat_rendering.py` asserts on the | |
| *rendered surface* β the plot's colour scale and the caution markdown β because that is the only | |
| place either bug existed. | |
| **Still open, reported not fixed** (see `TODO.md`): item 5 of the check (unsupported-modality refusal) | |
| is unreachable from the UI because Modality is a radio, and Gradio's own choice validation intercepts | |
| first; no curated cohort carries a subtype label, so the label-join half cannot be demonstrated in | |
| the Space. | |
| --- | |
| ## 2026-08-05 (later) β the SV under-call: a plausible cause that was wrong, and the real one (`03e0388`, `e0d0ac0`, NOT deployed) | |
| Follow-up to the click-through above. The six NRG1 events on `pdac_msk_2024` really were being | |
| reported as bare `rearrangement` while the payload's own provenance called them in-frame fusions β | |
| but **the cause was not what I first said it was**, and the way that surfaced is the part worth | |
| keeping. | |
| **The wrong answer.** The provenance text reads "an **in-frame** fusion", hyphenated, and | |
| `classify_sv` tested `"in frame"`. Clean, sufficient, matched the evidence I had. Fixed it, added | |
| tests, they passed, committed (`03e0388`). Then re-curated β **and the diff was empty. Zero cells | |
| moved.** Nothing else would have caught it: the tests were green, because they tested the | |
| hypothesis rather than the cohort. | |
| **The real answer**, read off the live API rather than inferred from the artifact: | |
| | field | value | | |
| |---|---| | |
| | `eventInfo` | `ATP1B1-NRG1 Fusion - Archer` β a bare label | | |
| | `annotation` | `POSITIVE FOR GENE FUSIONSβ¦: The rearrangement is an in-frame fusion between genes ATP1B1 Exon2 (NM_001677) and NRG1 Exon2 (NM_004495).` β the finding | | |
| | `site2EffectOnFrame` | `'NA'` β on **all 110** somatic panel rows | | |
| `classify_sv` read only `eventInfo`, so those rows fell to the bare-partner default **correctly**. | |
| The frame claim lived in a field nothing consulted. The hyphenation was real but downstream of the | |
| actual gap β it only matters once `annotation` is read at all, because that is where the hyphenated | |
| spelling lives. `site2EffectOnFrame` is the field that ought to carry this and is the same trap | |
| `variantClass` already set for CCLE: structured, present, and `NA` throughout. | |
| **Scope was larger than first reported: 9 cells over the whole KRAS-wild-type actionable tier** β | |
| NRG1 Γ6, NTRK1, NTRK3 (ETV6-NTRK3), RET (NCOA4-RET) β not 6 NRG1 alone. The RET cell went | |
| `fusion_out_of_frame` β `fusion_in_frame`, the only transition that *reduces* caution: that sample | |
| carries two RET events and the in-frame row now outranks the other under the existing specificity | |
| ordering, with provenance moving to match. | |
| **Why `annotation` is safe to read**, checked across both SV cohorts before trusting it: no row says | |
| "negative", none says "out of frame" there, and CCLE's 322 annotations never mention a frame at all | |
| (they are partner/panel strings). So it can only fire where the source states a frame. `eventInfo` | |
| still wins where both make a claim β annotation is a fallback, not an override. `--dry-run` confirms | |
| CCLE still reports `uncharacterized`, so its caveat is untouched; that was checked deliberately, | |
| since the caveat rests entirely on bare labels never being promoted. | |
| **All three fixes verified by clicking through the merged build** (`d956d91`), 2026-08-05: | |
| - `ccle_broad_2019` now renders the caveat in full β "β¦this cohort can never return | |
| `fusion_in_frame`. Read an absence of in-frame fusions here as a fact about the annotation, NOT | |
| about the biology" β **alongside** the Broad licence line, not instead of it. | |
| - The chart regression is gone: NRG1-alone (single red `sv` bar, y-axis 0β0.30, `sv`-only legend) | |
| followed immediately by the full panel now draws KRAS ~94%, TP53 ~78%, CDKN2A ~40% across all 33 | |
| genes with the full mutation/cnv/sv legend. That is the exact sequence that used to draw empty. | |
| - `sv` renders **red** in both the narrow and wide charts β the colour no longer depends on how many | |
| modalities happen to be present. | |
| - NRG1 on `pdac_msk_2024`: `assayed: false` + its note (the ADR-0008 gate is intact), 6/2336 = | |
| 0.257%, and all six now `fusion_in_frame` rather than bare `rearrangement`. | |
| β οΈ **The dev Space could NOT be used for this, and this is structural, not a one-off.** It is | |
| **private**, so `anne-voigt-pdac-genomics-agent-dev.hf.space` returns **404** β the session cookie | |
| lives on `huggingface.co`, not on the `hf.space` origin. Through the embed on the Space page the app | |
| renders but is a cross-origin iframe, and synthetic clicks do not reach it. Promoting the iframe's | |
| signed `__sign=` URL to top level lands on a **username/password form** with "Could not get API | |
| info. Connection errored out." So: **while the dev Space is private, it can be built and booted but | |
| not clicked through.** The verification above was done against the merged branch running locally on | |
| the pinned gradio 6.18.0 β legitimate here because both display bugs are purely client-side, but it | |
| is NOT a substitute for a real click-through of anything server-shaped. Make the dev Space public | |
| (or accept prod-only click-through) before relying on the dev lane for UI verification. | |
| **Two habits earned their keep and should be kept:** re-curating with `--check` first (all seven | |
| `up_to_date`, so this was a re-derivation and not a data refresh hiding behind a code change), and | |
| reading the re-curation diff **cell by cell** instead of trusting the summary line β which was | |
| identical (`106 SV cells (characterized)`) before and after both attempts. | |
| --- | |
| ## 2026-07-29 β the registration descriptor caught up with the Space (docs-only) | |
| `deploy/orchestrator_registration.yaml` exists so "the trigger keywords and tool list can't drift | |
| from what the agent actually exposes". It had drifted on six counts, all now fixed against the live | |
| Space at `894e710`: `genes` is a **comma-separated string** (not `list[str]`; blank β the full | |
| panel), `variant_by_subtype` takes **four** args (`genes, source, subtype_attribute, modality`), | |
| `/panel` was missing entirely, `samples_profiled` was undocumented, the cohort list still said four, | |
| and the auth/visibility notes still said "PRIVATE until the gate is enforced" (the gate is | |
| ENFORCED, the Space PUBLIC since `0ffadd1`) with no mention that a machine caller must use the | |
| custom `x-orchestrator-token` header. | |
| Nothing was broken by any of it β the orchestrator's own `agents.yaml` was written from the live | |
| endpoints, not from this file. The risk was only that the next reader trusts it, which is exactly | |
| the risk the header claims the file removes. | |
| Three things worth keeping: | |
| - **The `per_sample` sparsity rule is now a named section of the descriptor** (`per_sample_contract`), | |
| not a footnote: for a gene with `assayed != false`, a sample in `samples_profiled[modality]` but | |
| absent from `per_sample` is WT/neutral; a sample NOT in that list was never interrogated and is | |
| neither β exclude it from a join rather than counting it WT. The orchestrator shipped a join | |
| reporting all 19 genes as "no variation" precisely because it had to guess this. | |
| - **The cohort set is no longer restated as a list of facts** β the descriptor now says the set IS | |
| `src/resources/curated/*.json` and gives the seven only as an illustrative tail. This is the | |
| second time the restated list went stale, and prose is not the thing to trust twice. | |
| - **Bare `KRAS` was DROPPED from `trigger_keywords`, with the reason recorded inline.** This is the | |
| orchestrator's deliberate divergence, not an oversight to correct: bare KRAS collides with | |
| `pdac-subtype-agent`'s KRAS-Science-2024 signature sets and DecoupleRpy pathway questions, and the | |
| hotspot alleles G12D/G12V/G12R carry the DNA intent. Verified live in prod β "Score the KRAS | |
| signatures on gse205154_sears" reaches the subtype agent. The comment says don't re-add it. | |
| **The drift was invisible to the tests, so the tests changed too.** `test_deploy_surface.py` only | |
| did string containment on tool names, which catches a rename but not a wrong signature. Two new | |
| structural tests: the descriptor's `args` are compared to the live `inspect.signature` (names AND | |
| `str` types, per endpoint), and the cohort ids it mentions are compared to the curated directory. | |
| Both fail on the drift they exist to catch. **174 tests pass** (was 169; the descriptor file also | |
| turns out not to have been valid YAML before β `{genes: list[str]}` is not a legal flow mapping, so | |
| nothing had ever parsed it). | |
| ## 2026-07-28 β `paad_cptac_2021` curated + DEPLOYED (`b79f05e`); and a payload gap it exposed | |
| Curated **alongside** `pancreas_cptac_gdc`, not instead of it. Same CPTAC-3 patients, same paper | |
| (Cao et al. *Cell* 2021) β never pool them. Each covers what the other cannot: | |
| | cohort | n | CNV? | sample ids | why it exists | | |
| |---|---|---|---|---| | |
| | `pancreas_cptac_gdc` | 183 | **yes** | `C3L-00017-02` (aliquot-suffixed) | richer modality coverage β why it was picked first, and that still holds | | |
| | `paad_cptac_2021` | 140 | no (mutation only) | `C3L-00017` | ids match the expression registry's `cptac_pda` **131/131**; the suffixed GDC ids match it **zero** times | | |
| That id match is the point: it is what lets `pdac-analysis-orchestrator` run its cross-modality | |
| variantΓsubtype join (its ADR-0003), which aligns PurIST subtypes computed from `cptac_pda` against | |
| variant status from here. The earlier note calling `paad_cptac_2021` "superseded by the GDC rebuild" | |
| was right about CNV and simply could not see the join constraint. Both cohorts are needed. | |
| C2 discharged BEFORE fetching: access tier from cBioPortal's own `publicStudy`/`readPermission` | |
| (open), plus a `COMMERCIAL_USE_REVIEW` entry β **permitted**, CPTAC-3 open tier, confidence recorded as | |
| one step less direct than the GDC instrument since this is the authors' own deposition rather than a | |
| GDC rebuild. `ATTRIBUTION.md` carries the never-pool warning. | |
| **Deployed and verified live** through the orchestrator's own `x-orchestrator-token` path: 140 samples, | |
| `unavailable_modalities: ['cnv']`, KRAS 96.4% / TP53 75.0% / CDKN2A 20.7% / SMAD4 17.1% (identical to | |
| local), `study_terms` surfacing open + permitted, `variant_by_subtype` correctly routing to the | |
| orchestrator. **169 tests pass.** | |
| **Two process notes worth keeping.** The push was rejected on a diverged remote (another lane had added | |
| the GitHub-mirror pre-push hook and docs) and was **rebased, not forced** β their two new tests then ran | |
| against the new artifact and passed. But the rebase **silently dropped this entry's first version**: | |
| both lanes had appended to `memory.md` at the same anchor, and the replay treated the hunk as already | |
| applied, keeping only the `TODO.md` half of the same commit. Nothing conflicted and nothing warned. If | |
| you rebase a docs commit in this family, re-read the file afterwards rather than trusting a clean exit. | |
| ## The finding that matters more than the curation: `per_sample` is SPARSE | |
| The first real run of the orchestrator's join exposed a **payload contract gap in | |
| `/query_variant_status`** β not a curation problem: | |
| `genes.<G>.mutation.per_sample` contains **only altered samples**; WT is omitted. Verified on | |
| `paad_cptac_2021` (KRAS 135 entries, all `hotspot`; SMAD4 24, none WT) and `paad_tcga` (KRAS 136, | |
| SMAD4 37) β the general shape, not new. | |
| Internally harmless: `variant_by_subtype` feeds `contingency_and_test` a **dense** StatusMatrix row, so | |
| the local label-join is correct. But a **machine caller cannot reconstruct WT** β it receives altered | |
| samples only, and the payload carries `n_samples`/`n_profiled` **counts** with no profiled-sample **id | |
| list**. The orchestrator therefore scored a WT column of 0 for all 19 genes and reported "no variation | |
| in alteration status": a wrong reason wearing the clothes of a real finding. It fails safe (no | |
| fabricated p-value) but it misleads. Its alignment was perfect β 131 joined / 140 / 131. | |
| **FIXED (same day).** `/query_variant_status` now returns **`samples_profiled`** β the denominator as | |
| an ID SET, per modality, not just as a count. `samples_profiled` was chosen over a dense `per_sample` | |
| for two reasons: one list per modality instead of the same list repeated across 19 genes (`pdac_msk_2024` | |
| has 2336 samples), and it keeps `per_sample` meaning "what was found" rather than "the whole matrix". | |
| **Per MODALITY is the correct granularity here, and that had to be checked rather than assumed:** within | |
| an assayed gene the denominator IS the modality's profiled set (`StatusMatrix.denominator`); genes a | |
| targeted panel never carried are refused separately via `assayed: False`/`unassayed_genes`, so they never | |
| reach the densification rule. | |
| The documented caller rule, now stated in the payload docstring: for a gene with `assayed != False`, a | |
| sample in `samples_profiled[modality]` but absent from that gene's `per_sample` is WT/neutral β and a | |
| sample NOT in that list was never interrogated and is neither. Invariant asserted in tests: | |
| `len(samples_profiled[m]) == n_profiled[m]`. A modality the cohort lacks contributes **no key at all** | |
| rather than an empty list, because an empty list reads as "profiled nobody", which is a claim. | |
| **172 tests** (+3, on the existing 4-sample partial-profiling fixture where S4 was never sequenced β | |
| precisely the sample a naive "absent β WT" would invent). | |
| --- | |
| ## 2026-07-23 β Repo created (scaffold) | |
| New **third specialist** behind the PDAC Analysis Orchestrator, for DNA-level analysis | |
| (mutation/CNV status + the variantβsubtype question). Split out as its own agent per | |
| `docs/adr/ADR-0001-genomics-specialist.md`, which repeats `pdac-subtype-agent`'s ADR-0001 reasoning | |
| (distinct domain β distinct agent) for the larger DNA-vs-RNA gap. Originating decision + | |
| resolved scope: `pdac-subtype-agent` **ADR-0019** (Accepted 2026-07-23); Carl resolved scope | |
| (build now/ship later; mutation + CNV; variantΓsubtype join *and* lookup; cell-lines first, human v1). | |
| **Feasibility proven before scaffolding** (`pdac-subtype-agent` `docs/spikes/SPIKE-dna-panel-feasibility.md`): | |
| cBioPortal's public REST API serves mutation + CNV + expression for PDAC cell lines | |
| (`ccle_broad_2019`) and tumors (`paad_tcga`) via per-gene JSON β no bulk downloads β and the | |
| sample-id join across all three modalities works. KRAS returned `G12D/G12R/G12V`; driver | |
| frequencies matched the literature. | |
| Scaffold in place: README (HF Space front-matter, `hf_oauth: true`), CLAUDE.md, TODO.md (milestones), | |
| ADR-0001 + the number ledger (started day one), the cBioPortal client seeded from the spike | |
| (`src/workflows/cbioportal_io.py`), and the 19-gene v1 panel (`src/resources/pdac_panel.tsv`). | |
| Tools are **contracts, not implemented**. **Local only β no HF/GitHub remote yet** (awaiting an | |
| account/Space choice; working name `pdac-genomics-agent`). | |
| **Now in `SHOWCASE_STATUS.md`** (row added 2026-07-24) β see the 2026-07-24 Space-created entry below. | |
| Next: Milestone 1 β curate the cBioPortal status matrix + `query_variant_status`. | |
| --- | |
| ## 2026-07-24 β Milestone 1 done (status matrix + `query_variant_status`) | |
| Core curation implemented and tested hermetically. **Contract pinned in ADR-0002** | |
| (cBioPortal β geneΓsample status-matrix curation contract). | |
| - **`variant_status.py`** β `build_status_matrix(source, sample_ids=None)` pulls mutations + | |
| discrete CNV via `cbioportal_io`, classifies each: mutation β `WT/missense/truncating/hotspot` | |
| (hotspot = `HOTSPOTS` explicit set **+** `HOTSPOT_CODONS` recurrent-codon rule β KRAS G12x/G13x/Q61x | |
| caught for any residue; truncating from `mutationType`; silent β WT), CNV `-2..2` β | |
| `deep_del/loss/neutral/gain/amp`. Specific variant kept as provenance. `WT` resolved against the | |
| full sample-id list (mutation queries return only mutated samples). | |
| - **Per-study modality gate** β `modalities` set from real profile presence; no CNV profile β | |
| `cnv=None`, tool omits it + lists it in `unavailable_modalities`. Never a null "no alteration" column. | |
| - **`lineage.py`** β pancreatic subset of pan-cancer CCLE via `CANCER_TYPE`/`ONCOTREE_CODE`; refuses | |
| on empty. Registered PDAC cohorts skip it. | |
| - **`query_variant_status(genes, source)`** β per-sample status + cohort frequency + provenance + | |
| `modalities_available`/`grounded`. Applies the lineage filter for pan-cancer sources. | |
| - Added two client helpers: `cbioportal_io.sample_ids()` and `.clinical_data()`. | |
| - **Tests** β `tests/fixtures/cbioportal_paad_mini.json` (real field shapes, anonymized samples) + | |
| `test_variant_status.py` / `test_lineage.py`; hermetic (conftest monkeypatches the client). Smoke | |
| test updated (M1 contracts now callable; `variant_by_subtype` still an M2 stub). **13 passed, no network.** | |
| - **Live spot-check reproduces the spike** (not committed to CI): TCGA-PDAC CDKN2A deep-del 52, | |
| MYC amp 23 (exact); KRAS 136 / TP53 104 / SMAD4 37 (within a few of the spike's 140/105/38); | |
| `paad_qcmg_uq_2016` reports cnv unavailable; CCLE narrows to 57 pancreatic lines (KRAS 84%). | |
| Run tests: `DecoupleRpy_Agent/.venv/bin/python -m pytest -q` (this repo has no own venv). | |
| Still **local only β no remote**. Next: Milestone 2 β `variant_by_subtype` label-join + association test. | |
| --- | |
| ## 2026-07-24 β Milestone 2 done (`variant_by_subtype` label-join) | |
| The stateless half of the cross-modality question. No new ADR β this implements what ADR-0001 | |
| already decided (general join is orchestration; the label-join is the half this agent can do alone). | |
| - **`subtype_join.py`** β `find_subtype_labels(study, attr=None)` probes known subtype clinical | |
| attributes (`SUBTYPE_ATTRIBUTES`: Moffitt/MRNA/Bailey/Collisson/PurIST/β¦) or an explicit one; | |
| `contingency_and_test(per_sample_status, labels, altered)` inner-joins on sample id and runs | |
| **Fisher's exact** (2Γ2) or **chi-square** (RΓ2). Returns `testable=False` + reason for a single | |
| subtype class or an all-altered/all-WT column (no spurious p-value). | |
| - **`variant_by_subtype(genes, source, subtype_attribute=None, modality="mutation")`** β for a | |
| label-bearing cohort returns `{join_available: True, subtype_source, modality, n_labeled, | |
| genes: {G: {contingency, n, testable, test?/reason}}, caveats}`. `modality="cnv"` supported. | |
| Honors the M1 coverage + lineage gates. | |
| - **Routing contract**: BYOD source, or no subtype attribute in metadata, returns a structured | |
| `{join_available: False, route: "orchestrator", reason, note}` β the orchestrator then gets | |
| subtype from pdac-subtype-agent + variant status from `query_variant_status` and joins on sample | |
| id. Orchestrator-side wiring is NOT built here (belongs in `pdac-analysis-orchestrator`). | |
| - **Tests** β `test_variant_by_subtype.py` (association unit: Fisher/chi-square/untestable; tool | |
| join over the labeled fixture; CNV modality; both orchestrator-routing paths). Fixture gained a | |
| `MOFFITT_SUBTYPE` clinical block; conftest patches `clinical_data`. Smoke test updated | |
| (variant_by_subtype now callable, no longer a stub). **20 passed, no network.** | |
| - **Live check**: real `paad_tcga` carries no subtype clinical attribute β correctly routes to the | |
| orchestrator (confirms the label-join only fires when a label genuinely exists). | |
| Still **local only β no remote**. Next: Milestone 3 β BYOD upload + deploy hardening. | |
| --- | |
| ## 2026-07-24 β Milestone 3 done (BYOD + deploy hardening, code) | |
| Code-complete; the remaining M3 items are ops tasks that need an HF Space (see TODO.md | |
| "M3 deploy checklist"). Two ADRs published: **ADR-0003** (BYOD contract), **ADR-0004** (auth gate). | |
| - **BYOD** (`src/workflows/byod.py`, ADR-0003) β `BYODSpec(genome_build, species, maf_text?, | |
| cnv_text?)` β `curate_byod_upload()` β StatusMatrix `grounded=False`, never persisted. Build + | |
| species are REQUIRED declarations (untrusted, ADR-0006 mirror). Hard-gate refusals via | |
| `BYODRefusal(reason=β¦)`: `build_unsupported`, `build_mismatch` (file build column contradicts | |
| declared), `non_human_species` (mouse), `unparseable` (missing cols / non-integer / out-of-range | |
| GISTIC / empty). Reuses M1 `classify_mutation`/`classify_cnv`/`HOTSPOTS` verbatim; off-panel genes | |
| dropped. Routed from `build_status_matrix(BYODSpec)` (lazy import β byod depends on variant_status). | |
| - **Auth gate** (`access.py` + `tests/test_access.py`, ADR-0004) β ported verbatim from the siblings: | |
| `ALLOWED_IDS`/`ADMIN_IDS`(+`UPLOAD_ADMIN_IDS`) allow-list, `ACCESS_CONTROL` dark-launch (OFF by | |
| default, fail-closed when ON), `check_access`/`role_for`/`is_authorized`/`principal_trace_fields`. | |
| - **cBioPortal cache/backoff** β `cbioportal_io._req` now caches idempotent GET metadata | |
| (process-local, `clear_cache()`/`_CACHE_GET` for tests) and backs off exponentially. **ToS | |
| confirmation for automated use is still an open pre-prod ops task.** | |
| - **Orchestrator registration** β `deploy/orchestrator_registration.yaml` (this repo's artifact; | |
| tools, DNA-specific trigger keywords, the cross-modality handoff contract). NOT wired into the | |
| orchestrator here β that repo merges it at deploy (ADR-0001: we don't touch siblings). | |
| - **Security scan port** β `security/scan.sh` (bandit/pip-audit/gitleaks/trivy β git-ignored | |
| timestamped reports) + `security/.gitignore`, mirroring the siblings. Not run in CI; run before | |
| the OHSU review. (bandit not in the shared venv.) | |
| - **Tests** β `test_byod.py` (curate + all five refusal gates + routing through build_status_matrix) | |
| and `test_access.py` (ported gate suite). **49 passed, no network.** | |
| Deploy surface still stubbed on purpose: `gradio_ui.py`/`src/server.py` are built at the deploy step | |
| (M3 checklist), where the `access.py` gate + audit sink get wired in. Still **local only β no remote**. | |
| --- | |
| ## 2026-07-24 β cBioPortal terms & deployability (pre-prod gate closed) | |
| Closed the spike's open item ("confirm cBioPortal automated-use terms + add caching/backoff") β | |
| `docs/cbioportal-terms-and-deployability.md`. **Verdict: usable for a deployed prod Space WITH | |
| CONDITIONS** β no hard blocker. | |
| - **No ToS ban on automated/programmatic access** to the public API (`www.cbioportal.org/api`) β the | |
| website runs on the same API and maintainers publish official clients. **Self-host NOT required for | |
| v1.** v1 = public instance + **curate-and-cache** (runtime reads the cached status matrix; API is a | |
| curation-time dependency only), which neutralizes rate-limit/availability risk without ops cost. | |
| - **Conditions (all engineering/provenance, fold into M1/M2):** (C1) attribution β cite cBioPortal | |
| [Cerami 2012 / Gao 2013 / de Bruijn 2023] **and** each per-study source; (C2) carry per-study | |
| license flag (ODbL default; some studies restrict commercial use; TCGA/GDC + ICGC per-study terms; | |
| serve only open-access, refuse controlled); (C3) polite client β exp backoff+jitter on 429/5xx, | |
| self-throttle, descriptive `User-Agent` w/ contact; (C4) curate-and-cache so no live API call on | |
| the request path. | |
| - **No documented public rate limit** β absence β permission; self-throttle (~2β5 req/s, serial). | |
| ## 2026-07-24 β HF Space created | |
| Space **`anne-voigt/pdac-genomics-agent`** created via `hf` CLI (auth'd as `anne-voigt`), SDK gradio | |
| (matches README front-matter), **PRIVATE + empty β nothing deployed** (app code isn't ready and the | |
| M3 auth gate is unbuilt, so keep it private/ungated until launch). Working name is now confirmed as | |
| the real slug. `SHOWCASE_STATUS.md` Fleet row added (honest state: created/private/no-deploy). | |
| **Repo is still LOCAL ONLY** β `origin` is NOT yet pointed at the Space (per the sibling model, | |
| `origin` remote IS the Space; wire that up at first deploy, on `main`, after M1/M2 merge from | |
| `claude/serene-diffie-b17166`). Do NOT push an empty/broken tree to it. | |
| ## 2026-07-24 β cBioPortal public-vs-self-host ADR | |
| - **Public-vs-self-host decision ratified** as its own ADR β `docs/adr/ADR-0005-cbioportal-public-vs-selfhost.md` | |
| (Accepted 2026-07-24): **public instance + curate-and-cache for v1, no self-host**; revisit triggers = | |
| unavailable studies / sustained throttling / controlled-access need. **ADR-number note:** written on | |
| this lane as 0003 (it had seen the sibling lane's 0002 and skipped it) and **renumbered to 0005 on | |
| 2026-07-25**, because that lane had *also* published 0003 (BYOD) ~2 min earlier plus 0004 (auth | |
| gate). Different filenames β git would have merged two 0003s silently. Earlier claim keeps the | |
| number; see `docs/adr/RESERVED.md`. | |
| - **License:** data default **ODbL** (attribution + share-alike on redistributed derived *database*; | |
| serving derived *answers* is lower-friction). TODO Milestone 3 item marked done (research half); | |
| implementation of cache/backoff+attribution remains in M1/M2. | |
| --- | |
| ## 2026-07-25 β Founding docs seeded, 0003 collision fixed, both lanes merged to `main` | |
| Three things, in order: the founding planning docs were seeded and reconciled, the ADR-number | |
| collision between the two lanes was resolved, and both branches were merged. **`main` now holds all | |
| of M0βM3.** | |
| **Seeded** β the founding material had been drafted in `pdac-subtype-agent/docs/genomics-agent-seed/` | |
| before this repo existed (its README said "move these to the new repo"). Now here: | |
| - `docs/design/DESIGN-pdac-genomics-agent.md` β the north-star design. Body kept **verbatim**; a new | |
| **Β§0** records what held and what diverged. (It self-dates 2026-07-26 β a drafting artifact.) | |
| - **ADR-0001 enriched, not duplicated.** The seed's own draft ADR-0001 was a longer version of the | |
| one already published here; its missing content (founding commitments β new modality never a | |
| `data_level`, status matrix, hard gates, family maturity from commit one; alternatives 4β6; | |
| follow-ups) was merged into the existing file rather than landing a second ADR-0001. | |
| - **`RESERVED.md` rebuilt** with the family-wide **EXTERNAL** rows (0014 security scan β | |
| `DecoupleRpy_Agent`; 0015 registry semantics β `biodata-registry`), which were missing here, plus a | |
| new protocol rule 5: claims land on `main`, alone and immediately. | |
| **The design's "hard blocker" is not one.** It made a `biodata-registry` `modality` extension | |
| (`somatic_mutation`/`copy_number`) a hard prerequisite for M1. M1βM3 shipped without it: the build | |
| reads cBioPortal directly and **does not depend on `biodata-registry` at all** (absent from | |
| `requirements.txt`, no imports). Recorded as an **ADR-0001 amendment** β not a blocker, but an open | |
| fork (direct-source vs registry-routed) needing its own ADR if reopened. | |
| **ADR-0003 collision β resolved.** `serene` published 0003 (BYOD) at 14:04:34; `upbeat` claimed 0003 | |
| (cBioPortal public-vs-self-host) at 14:06:28, having skipped the sibling's 0002 but not its | |
| two-minute-old 0003. Different filenames β git would have merged both **silently**. The ledger failed | |
| to catch it because each lane kept its *own copy* of `RESERVED.md`; the one-shared-row design only | |
| conflicts when both lanes edit the same file in the same history. Fixed on the branch before merge | |
| (`15b3ade`): earlier claim keeps 0003, cBioPortal ADR renumbered **0003 β 0005** (file, header | |
| rationale, terms-doc ref, memory refs, ledger). When the two lanes then met in `main`, the ledger rows | |
| **did** conflict β the mechanism works, it just needs one shared copy. | |
| **Merged** β `claude/serene-diffie-b17166` (M1/M2/M3: status matrix, `query_variant_status`, | |
| `variant_by_subtype`, BYOD, `access.py`, security scan, orchestrator descriptor, ADR-0002/0003/0004) | |
| then `claude/upbeat-hofstadter-353bb8` (cBioPortal terms doc, ADR-0005, Space-created note). | |
| Conflicts resolved by hand: ledger rows unioned to a clean 0001β0005; `memory.md` keeps both lanes' | |
| history chronologically; `TODO.md` took serene's completed M3 with the ToS checklist item flipped to | |
| done by upbeat's research. **49 tests pass, no network.** | |
| **Carried into `TODO.md` as pre-deploy gates:** the Rosie/Carl confirm that per-sample status is the | |
| real near-term need (specified pre-M1, never done, now pre-deploy), and cBioPortal conditions C1βC4 | |
| (attribution, per-study license flag, polite client, curate-and-cache) β research closed, only the | |
| backoff/cache half implemented. | |
| **Left alone deliberately:** the seed folder still exists in `pdac-subtype-agent`; its README says to | |
| delete it after moving, but deleting files in a sibling repo is that repo's call. | |
| Still **local only β no remote**. Next: the deploy step (`gradio_ui.py` + `src/server.py` with the | |
| gate and audit sink wired in), which is what unblocks pointing `origin` at the Space. | |
| --- | |
| ## 2026-07-25 β Deploy surface built (`gradio_ui.py` + `src/server.py`) | |
| The last M3 checklist item that needed code. The Space can now actually run; what remains before | |
| a deploy is ops (secrets, enforcement) and the C4 caching gap, not app-building. | |
| **`gradio_ui.py`** β three tabs (variant status / variantΓsubtype / BYOD upload) over the existing | |
| tools, `gr.LoginButton()` for OAuth, and three `gr.api` machine endpoints for the orchestrator. | |
| Handler shape ported from `pdac-subtype-agent`: gate first via `access.check_access`, audit either | |
| way, return `(caution markdown, plot, JSON)`. Refusals are **data, never exceptions** β a BYOD | |
| hard-gate refusal (mouse, bad build, unparseable) surfaces as `{status: "refused", reason}` with an | |
| explanation, and any tool blow-up becomes `{status: "error"}` rather than a crashed Space. | |
| **`src/server.py`** β one sub-`FastMCP` per tool module, mounted, mirroring both siblings. | |
| **Ported to support the handlers** (ADR-0004 explicitly deferred the sink to this step): | |
| `audit.py` + `logging_sink.py` (`LOG_SINK` = local|hf) and `licenses.py`. | |
| **C1 (attribution) is now discharged** β `licenses.py` stamps a `citation` block onto every grounded | |
| payload with the cBioPortal papers *and* the per-study source, which is what the terms review said | |
| belongs in the response contract. C2/C4 remain open; **C4 is the real gap**: ADR-0005 ratified | |
| curate-and-cache, but every tool call still hits cBioPortal live on the request path. | |
| **Three MCP/api surfaces, one set of names.** Found and fixed: gradio's MCP bridge | |
| (`launch(mcp_server=True)`) names each tool after the **Python function**, `gradio_client` | |
| dispatches on **`api_name`**, and `src/server.py` registers its **own** names β so the same tool was | |
| appearing as `_run_variant_status`, `query_variant_status`, and `query_variant_status_tool`. The | |
| endpoint functions are now named exactly as the contract does (tools imported under `_`-aliases to | |
| free the names), and the FastMCP registrations pin `name=` explicitly. A test asserts the MCP names | |
| equal the descriptor's. Verified live: all three surfaces report `query_variant_status` / | |
| `variant_by_subtype`. | |
| **Live finding β `gradio_client` sends the caller's own HF token.** A tokenless `gradio_client` | |
| call still arrived carrying `x-hf-authorization: Bearer <the calling machine's HF token>`, which the | |
| gate resolved to that machine's username. Two consequences, both now in a code comment: identity | |
| locally is ambient, and per the inherited ADR-0018 finding HF Space edge infra **strips** `x-hf-*`, | |
| so **the same call that authenticates locally will be denied in the deployed Space** unless the | |
| orchestrator sets the custom `x-orchestrator-token`. Fail-closed either way, but it will look like a | |
| bug if forgotten. | |
| **Verified, not assumed:** app boots and serves HTTP 200; `/panel` answered end-to-end over | |
| `gradio_client` (19 genes); with `ACCESS_CONTROL=enforce` and a non-matching allow-list the same | |
| call was **denied** by name. **79 tests pass, no network** (30 new: UI gating, machine-endpoint | |
| auth, audit-record shape, deploy-surface contract). | |
| The audit test worth keeping honest is the negative one: sample ids, per-sample status, and | |
| provenance strings must never reach the sink. Gene names may (a fixed public panel). | |
| Deps pinned to what this was built against (gradio 6.18.0, fastmcp 3.2.3, huggingface_hub 1.10.1) | |
| and `sdk_version`/`python_version` added to the README front-matter. `run_logs/` gitignored. | |
| Still **local only β no remote**. Next: the ops half β point `origin` at the Space, set | |
| `ALLOWED_IDS`/`ADMIN_IDS`, flip `ACCESS_CONTROL=enforce`, and close C4 (curate-and-cache) before | |
| prod traffic. | |
| --- | |
| ## 2026-07-25 β C4 closed: curate-and-cache (the API is no longer on the request path) | |
| ADR-0005 ratified "public cBioPortal + curate-and-cache" back on 2026-07-24, but the code kept | |
| calling the API live on every request β the ADR and the implementation disagreed. They now agree. | |
| **The split.** `python -m src.curate <study>` (new, `src/curate.py`) is the only thing that touches | |
| cBioPortal: it fetches, classifies through the same M1 rules, and writes | |
| `src/resources/curated/<study>.json`. The request path reads that artifact via | |
| `src/workflows/curated_store.py` and **never fetches**. An uncurated study raises `NotCuratedError` | |
| telling the operator to curate it. No fallback on purpose β a fallback makes the coupling | |
| intermittent rather than absent, which is harder to debug and shows up under load. | |
| **Artifacts carry clinical attributes, not just the matrix.** Caching the matrix alone would have | |
| left two live calls on the path: the pancreatic-lineage filter and the subtype label-join both | |
| queried `clinical-data` per request. Both now read the artifact. Artifacts are **sparse** (only | |
| non-WT / non-neutral cells) β a dense wall of "WT" is noise in a diff. | |
| **Four cohorts curated and committed** (real data, ~940KB total): `paad_tcga` (186 samples), | |
| `ccle_broad_2019` (1739), `paad_qcmg_uq_2016` (456, **no CNV** β the coverage gate confirmed against | |
| live data), `paad_utsw_2015` (109). The offline path reproduces the spike exactly: TCGA KRAS 136 / | |
| TP53 104 / SMAD4 37, CCLE narrowing to 57 pancreatic lines with KRAS 84%. A live `/query_variant_status` | |
| now answers in **0.44s**; an uncurated study refuses in 0.07s. The UI's cohort dropdown is read from | |
| the artifacts rather than hardcoded β "registered" and "curated" are the same set now. | |
| **Two bugs found by verification, not by unit tests.** | |
| 1. **A C4 leak.** `clinical_values` returned `None` both for "study not curated" and for "curated but | |
| has no such attribute", and the second case fell through to a live call. Probing the eight known | |
| subtype attributes against a cohort carrying none of them meant **eight live API calls per | |
| request** β on real `paad_tcga`, which has no subtype attribute at all. Absence has to be an | |
| *answer* (`{}`), not a miss. Caught by sabotaging `urllib` against the real artifacts; the unit | |
| test missed it because the fixture study happens to carry `MOFFITT_SUBTYPE` and matched on the | |
| first probe. Regression test added. | |
| 2. **CCLE's `SUBTYPE` is histology, not molecular subtype** β "Adenocarcinoma", "Melanoma", "Small | |
| Cell Lung Cancer". `SUBTYPE` is in `SUBTYPE_ATTRIBUTES`, so auto-detection would have reported a | |
| confident "variant Γ subtype association" that was really variant Γ histology across a pan-cancer | |
| panel. Pre-existing, but curating CCLE is what made it reachable. Auto-detection now validates a | |
| candidate's **values** against a closed PDAC subtype vocabulary (basal/classical/squamous/ | |
| progenitor/immunogenic/ADEX/β¦) and skips attributes that don't speak it; an explicitly named | |
| attribute is still trusted. A plausible-looking wrong answer is worse than no answer. | |
| **Tests: 90 pass, no network** (12 new). `patched_cbio` now patches the client *and curates from it*, | |
| so tests exercise the offline path production actually takes; `no_curated_artifacts` isolates the | |
| curation-time tests, since committed artifacts correctly take precedence over a patched client. | |
| **Remaining on the source conditions:** C2 (per-study license flag) is untouched; C3 is partial | |
| (backoff + cache, still no jitter/self-throttle/`User-Agent` contact). New backlog item: nothing | |
| warns when an artifact goes stale β each carries `curated_at`, but re-curation cadence is undecided. | |
| Still **local only β no remote**. Next: the ops half (point `origin` at the Space, allow-list, | |
| `ACCESS_CONTROL=enforce`). | |
| --- | |
| ## 2026-07-25 β C2 + C3 done: the cBioPortal pre-prod gate is GREEN | |
| All four source conditions from `docs/cbioportal-terms-and-deployability.md` (ratified in | |
| ADR-0005) are now implemented rather than merely accepted. The checklist in that doc is ticked. | |
| **C2 β per-study license/access, grounded in the source.** The key call: the access tier comes | |
| from cBioPortal's own `publicStudy` / `readPermission` fields (`cbioportal_io.study_metadata`), | |
| not from a list we maintain by hand β a hand-kept list is exactly the thing that goes stale | |
| silently and is then trusted. `licenses.study_terms()` builds the block; it is stored in every | |
| artifact and surfaced in every answer as `citation.study_terms`, because provenance that stops at | |
| the storage layer is provenance nobody reads. | |
| Enforced in two places: **curation refuses** a non-open study *before* fetching any variant data | |
| (`ControlledAccessError`), and the **serve path refuses** an artifact declaring controlled access | |
| (`ControlledAccessArtifactError`). The second should be unreachable β which is precisely why it | |
| exists, since artifacts are files a human can hand-add or copy between deployments. | |
| Commercial-use restrictions are not an API field, so they stay a hand-maintained overlay | |
| (`licenses.COMMERCIAL_USE_RESTRICTED`) whose default is **"unreviewed", never "unrestricted"**. | |
| Silence about terms must not read as permission. | |
| **Schema bumped v1 β v2.** A pre-C2 artifact has no terms block, and the serve gate only fires on | |
| a *present* non-open block β so a v1 artifact would have served with C2 silently dropped. Refusing | |
| it outright and re-curating was the only honest option. All four cohorts re-curated. | |
| **C3 β polite client.** Descriptive `User-Agent` carrying the project URL, plus an optional | |
| `CBIOPORTAL_CONTACT` env var (env, not hardcoded, so no personal address lands in a public repo β | |
| **a real address still needs setting in the Space before prod**, now a TODO gate). ~4 req/s serial | |
| self-throttle, backoff with **jitter** honouring `Retry-After`, and retries limited to 429/5xx: a | |
| 404 is our own bug and retrying it just repeats the mistake at their expense. All of this is | |
| curation-time cost only (C4), so being conservative is effectively free. | |
| **Tests: 106 pass, no network** (16 new). Includes a test that every artifact committed to this | |
| repo is open-access β the C2 posture asserted, not assumed. | |
| Two follow-ups recorded in `TODO.md`: set `CBIOPORTAL_CONTACT` at deploy, and actually review | |
| commercial-use terms per curated study (today they all correctly read "unreviewed"). | |
| Still **local only β no remote**. Next: the ops half of the deploy checklist. | |
| --- | |
| ## 2026-07-25 β Both C-condition follow-ups closed; CCLE turns out to be commercially RESTRICTED | |
| **Follow-up 1 β the contact address β was a requirement I invented.** Annie pushed back: why an | |
| email? Correct. The polite-bot convention identifies via `+<URL>` (as `Googlebot/2.1; | |
| +http://www.google.com/bot.html` does), and the Space URL we already carry *is* the contact | |
| channel β the Space page has a Community tab anyone at cBioPortal can post to. No sibling repo | |
| sets a User-Agent at all. `CBIOPORTAL_CONTACT` stays as an optional *second* channel; unset is now | |
| documented as a complete configuration, the curation nag is gone, and the pre-deploy gate is | |
| deleted rather than deferred. Added `.env.example` covering the auth gate, audit sink, and this. | |
| **Follow-up 2 β commercial-use review β overturned an assumption in our own code.** | |
| `licenses.COMMERCIAL_USE_REVIEW` now records all four curated cohorts with instrument, source URL, | |
| confidence, and obligations: | |
| | study | verdict | basis | | |
| |---|---|---| | |
| | `paad_tcga` | permitted | NIH/NCI GDS open-access tier; TCGA moratoria lifted | | |
| | `ccle_broad_2019` | **restricted** | Broad DepMap/CCLE Portal Terms | | |
| | `paad_qcmg_uq_2016` | no_restriction_found | ICGC E.8.1 open tier | | |
| | `paad_utsw_2015` | permitted | CC BY 4.0 (article + supplementary) | | |
| **CCLE is not CC BY 4.0.** That licence covers DepMap *releases*; `ccle_broad_2019` is sourced from | |
| the Broad CCLE Portal, whose terms bar commercial use and also bar using the data to train or | |
| enhance ML/AI models beyond internal research. We had asserted "CC-BY-4.0" in both `licenses.py` | |
| and `CLAUDE.md` β wrong, now corrected in both. Our academic use stays clear either way; the flag | |
| exists so nobody downstream reads our silence as permission. | |
| **Two structural findings about cBioPortal licensing, worth not re-learning:** it has **no | |
| machine-readable licence field** (`meta_study.txt` has no such key), and every datahub study ships | |
| an *identical* ODbL boilerplate `LICENSE` file β so that file is not a per-study assertion. The | |
| FAQ's promise that restrictions "will be explicitly mentioned in the study information" is not | |
| reliably kept (CCLE's description says nothing). **Terms must always resolve to the upstream | |
| source, never to cBioPortal's redistribution**, and where they conflict β they do for CCLE β | |
| upstream governs. | |
| **Vocabulary distinction that is load-bearing:** `no_restriction_found` β `permitted`. | |
| `commercial_use_permitted()` returns False for it, and for `unreviewed`. Absence of a prohibition | |
| is not a grant, and anyone asking programmatically is about to rely on the answer. | |
| The restriction also surfaces **in the UI**, not only in `citation.study_terms` β a licence | |
| constraint nobody reads is one nobody honours. | |
| **Two items a human must confirm before anyone relies on this commercially** (both in `TODO.md`): | |
| read https://depmap.org/portal/terms/ in a real browser (bot-protected, so the verbatim CCLE clause | |
| was obtained indirectly β substance high-confidence, wording not directly verified), and establish | |
| whether ICGC *affirmatively permits* open-tier commercial use rather than merely not prohibiting it. | |
| **Also rescued:** the founding seed originals were untracked in `pdac-subtype-agent` (present in one | |
| working tree, in no commit, in a checkout several lanes share). Their content had been *merged* | |
| here rather than copied, so the originals existed in no history at all. Archived verbatim under | |
| `docs/design/seed/` with a map to where each one's content now lives (`bdac9d2`). | |
| Tests: **109 pass, no network.** Still local only β no remote. | |
| --- | |
| ## 2026-07-25 β DEPLOYED: `origin` points at the Space, app is RUNNING (private) | |
| First deploy. `origin` now **is** the HF Space (`anne-voigt/pdac-genomics-agent`), matching the | |
| sibling model β there is no separate GitHub repo, and `git push origin main` rebuilds the Space. | |
| **Merged HF's scaffold rather than force-pushing over it.** The Space carried an auto-generated | |
| `initial commit` (a placeholder README + `.gitattributes`). Force-pushing would have discarded | |
| `.gitattributes`, which configures LFS patterns; I merged with `--allow-unrelated-histories` and | |
| resolved the README to ours. HF's default front-matter was wrong for us anyway β it pointed at | |
| `app.py`, gradio 6.20.0 and python 3.13, against our tested `gradio_ui.py` / 6.18.0 / 3.11 (and | |
| `sdk_version` must match the `requirements.txt` pin or the Space builds a different UI than the | |
| tests cover). Checked first: `.gitattributes` has no `*.json` LFS rule, so the curated artifacts | |
| stay plain git. | |
| **Shipped an attribution file for the artifacts before pushing.** Committing the curated matrices | |
| makes them *downloadable*, which is exactly the case the terms review singled out: serving derived | |
| answers is a produced work, but "if the matrix is ever offered for download, ship it ODbL + | |
| attribution". `src/resources/curated/ATTRIBUTION.md` carries the platform citation, each study's | |
| original citation, and each study's commercial-use verdict (CCLE restricted). | |
| **Verified live, not just reported RUNNING:** `/panel` β 19 genes; `/query_variant_status` on | |
| `paad_tcga` β 186 samples with KRAS 136 / TP53 104 / SMAD4 37, identical to local and to the | |
| original spike; CCLE β 57 pancreatic lines with `commercial_use: restricted` surfacing. Response | |
| **0.80s** β curate-and-cache (C4) working in the Space, no API on the request path. | |
| **Auth state β deliberately incomplete.** `hf_oauth: true` is live and `ADMIN_IDS=anne-voigt` is set | |
| as a Space secret. `ACCESS_CONTROL` is **not** flipped, so the gate is still dark-launched OFF; the | |
| Space is **PRIVATE**, which is what contains the exposure. Two decisions left for a human, both in | |
| `TODO.md`: who goes in `ALLOWED_IDS` (a membership question, not an ops step), and when to enforce. | |
| **Enforcement has a known consequence, worth deciding with eyes open:** HF edge infra strips the | |
| `x-hf-*` header `gradio_client` sends automatically, so under `ACCESS_CONTROL=enforce` the machine | |
| API denies **every** caller until the orchestrator sends a custom `x-orchestrator-token` whose | |
| identity is allow-listed. The OAuth UI path is unaffected. Nothing consumes the machine API yet, so | |
| this costs nothing today β but it will look like a broken deploy if hit unaware. | |
| **The one combination that must never exist is public + gate-off.** Private + gate-off is fine; | |
| flipping the Space public requires enforcing first. | |
| Still outstanding before this is real to users: the Rosie/Carl confirm that per-sample variant | |
| status is the near-term need (the "should it ship at all" gate β never done), and merging | |
| `deploy/orchestrator_registration.yaml` into the orchestrator's `agents.yaml`. | |
| --- | |
| ## 2026-07-27 β `ACCESS_CONTROL=enforce` flipped; gate verified fail-closed | |
| The gate is on. `ACCESS_CONTROL=enforce` is set as a Space **variable** rather than a secret β | |
| it carries nothing sensitive, and someone auditing "is the gate actually on?" should be able to | |
| read that off the settings page without needing access to secrets. `ADMIN_IDS=anne-voigt` stays a | |
| secret. | |
| **Verified live:** the machine API (`/panel`) now returns `{"status": "denied"}` with the sign-in | |
| message. That is the correct, predicted behaviour and it confirms the documented asymmetry β | |
| HF edge infra strips the `x-hf-*` header `gradio_client` attaches automatically, so no identity | |
| reaches the handler and the gate fails closed. **The orchestrator cannot call this Space until it | |
| sends a custom `x-orchestrator-token` with an allow-listed identity.** Expected, not a regression. | |
| **Two things are NOT done, and the first one matters today:** | |
| 1. **`ALLOWED_IDS` is unset, so only `anne-voigt` can use the Space.** Setting it needs the lab | |
| members' actual HF usernames. Guessing them fails in both directions β a wrong guess either | |
| locks out someone real or hands access to an unrelated account that happens to own that | |
| username. Asked; not invented. | |
| 2. **The *allowed* path is unverified.** The denial is confirmed; nobody has signed in through | |
| OAuth and run a query. That is the path that must work, and it cannot be tested without an | |
| interactive browser sign-in. Rollback if the owner is locked out: set `ACCESS_CONTROL` to empty. | |
| The Space also **slept** between the deploy and this change (idle timeout on the free tier, not an | |
| error) β a restart brought it back in ~2 minutes. Worth knowing before someone reads SLEEPING as a | |
| failure. | |
| --- | |
| ## 2026-07-27 β Carl allow-listed; UI gate verified BOTH ways; machine auth still failing | |
| **`ALLOWED_IDS=anne-voigt,cpelz741`.** Carl's HF id was taken from `pdac-subtype-agent` prod | |
| (`SHOWCASE_STATUS.md` records its live config) rather than guessed β a wrong username would either | |
| lock out a colleague or hand access to an unrelated account that owns that name. | |
| **The OAuth UI path is now VERIFIED, which was the last open deploy question.** Signed in as | |
| `anne-voigt` in the deployed Space: a CCLE query rendered the frequency chart (KRAS ~84% across the | |
| 57 lineage-filtered pancreatic lines) *and* displayed the licence caution β "commercial use of this | |
| cohort is restricted under Broad DepMap/CCLE Portal Terms". Signed out, the same query returns | |
| `{"status":"denied"}`. Both halves of the gate confirmed end-to-end in the real UI. | |
| Incidental: the Space **does not render inside the huggingface.co page frame** (blank), but works at | |
| the direct host `https://anne-voigt-pdac-genomics-agent.hf.space`. Use the direct host for testing. | |
| The cohort dropdown renders its value only on focus β a Gradio display quirk, not an empty default. | |
| **Broad terms read directly** (see previous entry): confirmed, and the *Continuity of Terms* clause | |
| means rehosting requires reposting them in full β done in `CCLE_BROAD_TERMS.md`. | |
| **Open and now spun off: the machine API denies the orchestrator despite correct configuration.** | |
| The orchestrator already sends `x-orchestrator-token` (its commit `05bda30`, built for the subtype | |
| agent's ADR-0018 gate) and that token resolves to `anne-voigt`, which is allow-listed here. It is | |
| still denied. What is ruled out: the header is *not* being dropped β a local probe confirmed | |
| `x-orchestrator-token` arrives in `gr.Request.headers` alongside `x-hf-authorization`. So the | |
| failure is **server-side identity resolution**: `_resolve_token_identity()` calls | |
| `huggingface_hub.whoami()` at request time from inside the Space and returns None on any failure. | |
| It fails closed, which is right, but silently, which is why this took a live test to notice. | |
| The design question worth answering before patching: **should a gate depend on an outbound network | |
| call per request at all?** That is a third-party dependency on the request path β the same class of | |
| coupling ADR-0005 C4 removed for cBioPortal. A shared-secret comparison would have neither the | |
| latency nor the failure mode. | |
| --- | |
| ## 2026-07-28 β ADR-0006: the variantΓsubtype join contract (written down at last) | |
| The agent's differentiator was the only major decision without an ADR β it lived as prose in | |
| `TODO.md`, `DESIGN-pdac-genomics-agent.md` Β§4, and module docstrings, despite being the one part of | |
| the design that **spans two Spaces**. Now `docs/adr/ADR-0006-variant-subtype-join-contract.md`. | |
| **No behaviour changed.** This documents what M2 already ships (plus the vocabulary fix already in | |
| code) and, crucially, states the orchestrator's obligations normatively so the other repo has a | |
| contract to build against rather than a docstring to interpret. | |
| What it settles: | |
| 1. **The split.** Label-join is local (cohort carries a validated subtype clinical attribute β join | |
| on sample id + test here); the live-PurIST join is orchestration (ADR-0001/0011 β stateless | |
| siblings never call each other), returning `{join_available: false, route: "orchestrator"}`. | |
| `altered` vocabularies and the Fisher-2Γ2 / ΟΒ²-RΓ2 rule are pinned here as the single normative | |
| statement, because the orchestrator will implement the *same* test on its side. | |
| 2. **The routing contract** β the discriminated return shape (switch on `join_available`, never on | |
| the presence of `genes`), the orchestrator's three ordered steps, and an explicit prohibition: | |
| it may **not** synthesize a label and call back through `subtype_attribute`. | |
| **Sample-id alignment across Spaces stays OPEN** (the spike measured 145/146 *within* one | |
| cBioPortal cohort β not the cross-Space case that would actually need a map). Interim rule: | |
| exact string equality, **report join coverage**, no fuzzy/prefix matching, and a collapsed join | |
| is a refusal **with the numbers shown**. A silent low-coverage join is the dangerous failure. | |
| 3. **Label validation is load-bearing.** Attribute-id matching alone is unsafe *on evidence*: | |
| `ccle_broad_2019`'s `SUBTYPE` holds histology ("Adenocarcinoma", "Melanoma"), which produced a | |
| confident variantΓhistology association dressed as variantΓsubtype. Auto-detect now validates | |
| VALUES against a **closed** PDAC subtype vocabulary (β₯0.5 of non-blank values); an explicitly | |
| named attribute bypasses it (caller took responsibility) and is always visible in `subtype_source`. | |
| 4. **Statistical honesty β kept as-is, deliberately.** No multiple-testing correction across genes | |
| (descriptive screen over a user-chosen panel; the right denominator is session-level knowledge a | |
| stateless tool lacks β FDR is the orchestrator's call), mandatory caveat saying so, and | |
| `testable=false` + machine-readable reason instead of a bogus p-value. Contingency table and `n` | |
| are returned even when untestable β KRAS is altered in ~90%+ of PDAC, so the flagship gene is the | |
| one that degenerates most often. No hard minimum-n refusal; show n, caveat, refuse only the | |
| genuinely undefined. | |
| 5. **Metadata label β live PurIST call.** Never interchangeable, never mixed in one contingency | |
| table, never blended to fill gaps. `subtype_source` is required on every joined result | |
| (`clinical_attribute:<ATTR>` vs `purist:<version>`) and may not be dropped downstream. | |
| "Do they agree?" is a *concordance analysis*, not a licence to pool. | |
| Cross-references `pdac-subtype-agent` **ADR-0020**, whose alignment-by-sample-id primitive this | |
| reuses β variantΓsubtype is a cross-*modality* instance of the same cross-source alignment pattern, | |
| and inherits its align-never-pool + mandatory-caveat stance. | |
| **Ledger protocol followed to the letter**: 0006 claimed with status `CLAIMED`, committed **alone**, | |
| pushed to `main` (`a792086`) *before* the ADR was written β the step that exists because of the real | |
| 0003 collision. Flipped to `PUBLISHED` after. | |
| **Known follow-up (not a blocker):** two association-test implementations will exist (here and | |
| orchestrator-side) and must agree on Β§1's rules, or the same cohort answers differently by route. | |
| --- | |
| ## 2026-07-28 β Machine-API auth: the Space was never broken; the bug is caller-side | |
| **The 2026-07-27 diagnosis in the entry above is WRONG and is corrected here.** It concluded the | |
| failure was "server-side identity resolution β `_resolve_token_identity()` calls `whoami` per | |
| request and returns None". Probing the deployed Space directly disproves that: | |
| | probe (against prod, `ACCESS_CONTROL=enforce`) | result | | |
| |---|---| | |
| | valid HF token in `x-orchestrator-token` | **`status: ok`** β full panel returned | | |
| | bogus token in `x-orchestrator-token` | denied | | |
| | header absent entirely | denied | | |
| So `whoami` resolves fine from inside the Space and the gate discriminates exactly as designed. | |
| **Root cause is the caller:** `pdac-analysis-orchestrator`'s `router._build_client_kwargs()` | |
| attaches `x-orchestrator-token` **only when its own `HF_TOKEN` is set** β an unset secret on *that* | |
| Space sends no header at all. The fix is a secret on the orchestrator Space; **no code change was | |
| needed here.** | |
| **Why it was misdiagnosed, and the actual fix shipped.** All three failure modes returned one | |
| message β *"π Please sign in with your HuggingFace account"* β which reads as a this-Space auth | |
| bug and sent the investigation server-side, where nothing was wrong. A machine caller cannot sign | |
| in; telling it to is a category error. Machine denials now carry a `machine_auth` diagnosis: | |
| - `no_token_header` β **caller misconfigured**, message names the orchestrator's `HF_TOKEN`. | |
| - `token_unresolved` β a token arrived but did not resolve (bad/expired, or an upstream blip). | |
| - `not_allowlisted` β resolved fine, simply not on `ALLOWED_IDS`. The *only* one of the three that | |
| is a real authorization decision; still names who was refused. | |
| 4 regression tests pin the three states apart (`tests/test_machine_api_auth.py`). **113 green.** | |
| **Method note worth keeping:** the earlier conclusion came from reading the code and a local | |
| header probe; it took ~4 curl calls against the deployed Space to overturn it. When a gate is | |
| "failing", probe the deployed thing with a known-good credential *before* theorising about why the | |
| resolution path is broken β the cheap experiment discriminates caller-side from server-side in one | |
| step. The lookalike messages are what made theorising feel productive. | |
| **Still open (unchanged, not urgent):** should an auth gate depend on an outbound `whoami` on the | |
| request path at all, versus a shared-secret comparison? Same coupling class ADR-0005 C4 removed for | |
| cBioPortal. It works today; this is a latency/failure-mode question, not a correctness one. | |
| --- | |
| ## 2026-07-28 β Re-curation cadence (ADR-0007): drift, visibility, and no refusal | |
| Closed the gap ADR-0005 C4 created and left open. `curated_at` had been written into every | |
| artifact since commit one and **read by nothing** β no threshold, no warning, no policy. A cohort | |
| could drift from upstream for years and every answer would look equally confident. | |
| **Cadence is upstream-driven, not calendar-driven.** `python -m src.curate --check` reads one | |
| `/studies/<id>` per curated study and compares upstream's `importDate` against the stamp now | |
| recorded at curation (`source_import_date`). Drift β re-curate; no drift β do nothing, however old | |
| the file is. **The empirical confirmation is the point:** all four cohorts were last imported | |
| upstream in **January 2026** and curated in July β six months of a published cohort not moving. | |
| A "refresh monthly" policy would have rewritten four byte-identical files and spent the C3 | |
| politeness budget for zero new data. `REVIEW_AFTER_DAYS=180` / `STALE_AFTER_DAYS=365` are only a | |
| backstop for when nobody runs the check. | |
| **Age is visible to the reader, not just the maintainer.** Every grounded answer's `citation` block | |
| now carries `curation` (`curated_at`, `age_days`, `level`, `source_import_date`, `drift_comparable`, | |
| plain-language `note`); the UI renders a line past the horizon, next to the existing licence | |
| caution. Same stance that put `terms` in the response contract β provenance that stops at the | |
| storage layer is provenance nobody honours. | |
| **Stale never refuses.** A stale-but-valid cohort is old, not wrong: its calls were correct for the | |
| release they were curated from. Refusing would withhold a correct answer over a maintenance lapse | |
| and would dilute what a refusal from this agent means. Refusal stays for the *incorrectness* gates | |
| (build, species, absent modality, access tier). Pinned by | |
| `test_stale_artifact_still_answers_and_says_so`. | |
| **`--check` never writes**, deliberately β re-curation stays a reviewed commit. A checker that | |
| repaired what it found would put the API back on an unattended path, i.e. undo C4 quietly. | |
| Two implementation notes worth keeping: | |
| - **No schema bump.** `source_import_date` is optional; absence degrades to an honest | |
| `drift_comparable: false`. Bumping to v3 would have refused all four existing artifacts and | |
| forced exactly the blanket re-curation this cadence exists to avoid. | |
| - The four pre-existing artifacts had their `source_import_date` **backfilled once** in the same | |
| commit from a live metadata read (metadata only β no status call touched), so `--check` is useful | |
| today rather than "unknown until each happens to be re-curated". | |
| 121 tests pass (11 new, `tests/test_recuration_cadence.py`). `--check` verified live against the | |
| public instance: all four `up_to_date`. **Not deployed** β the branch | |
| `claude/infallible-kowalevski-2d4648` is pushed to origin and rebased onto `main`, but | |
| `origin/main` IS the live Space, so promoting is a deliberate deploy decision. | |
| **Renumbered 0006 β 0007, and the near-miss is the lesson again.** A concurrent lane had already | |
| claimed *and published* 0006 (the variantΓsubtype join contract) on `main`. This lane's worktree | |
| held its own copy of `RESERVED.md` and predated that claim, so the one-shared-row conflict could | |
| not fire β the **third** time that root cause has bitten this family. The claim here was committed | |
| alone and pushed, but to the feature branch rather than `main` (protocol rule 5), because | |
| `origin/main` IS the live Space and pushing it is a deploy: rule 5 and the deploy model are in | |
| genuine tension on this repo. **What actually caught it was reading `SHOWCASE_STATUS.md` before | |
| updating it.** Practical check until rule 5 has a deploy-safe form here: | |
| `git fetch origin main && git show origin/main:docs/adr/RESERVED.md` *before* claiming. | |
| **Weekly job added the same day** (`.github/workflows/curation-freshness.yml`, Mondays 07:00 UTC + | |
| `workflow_dispatch`, report-only). **It does not run, and the file says so in its own header rather | |
| than looking green.** `origin` IS the HF Space and HuggingFace does not execute GitHub Actions β | |
| and unlike **every** sibling (`DecoupleRpy_Agent`, `pdac-subtype-agent`, | |
| `pdac-analysis-orchestrator`, each of which carries a second `github` remote), | |
| **this repo has no GitHub mirror at all**, so it has never had CI of any kind. Committed anyway on | |
| the `DecoupleRpy_Agent/.github/workflows/security.yml` precedent: the definition never forks, and | |
| adding the mirror later is then zero work. **The one remaining ops step is creating | |
| `Anne-Voigt/pdac-genomics-agent` on GitHub and `git push github main`** β that turns the check on | |
| and gives the repo CI. A local launchd agent was deliberately NOT installed: a job that runs on one | |
| laptop is invisible to everyone else, which is how the original gap happened. A test | |
| (`test_weekly_job_is_report_only`) asserts the job can never gain a writing `src.curate` step β | |
| that would re-establish the unattended API path C4 removed, in a file nobody reads once it is | |
| green. 122 tests. | |
| **Deployed 2026-07-28** (`6f23703` β live; a third lane's `4b8cb52` landed on top minutes later and | |
| the Space is RUNNING at that sha, which contains ours). Rebased onto `fdfbfb3` (the machine-auth | |
| diagnosis lane) before pushing β **126 tests green** with both lanes' changes together, up from our | |
| 122. **Verified in prod, not just RUNNING:** `paad_tcga` β 186 samples, KRAS 136, TP53 104 β | |
| identical to local and to the original spike β and `citation.curation` serves | |
| `level: current, age_days: 2, source_import_date: 2026-01-12, drift_comparable: true`, confirming | |
| the backfill landed in the artifacts the Space actually reads. | |
| **Fixed the same day (see the next entry):** the machine endpoints take `genes` as a | |
| **comma-separated string** (`_split_genes`), not a list, and a list used to return | |
| `AttributeError: 'list' object has no attribute 'split'`. | |
| --- | |
| ## 2026-07-28 β Cohort survey; +2 curated (6 total); per-GENE coverage gate added | |
| Surveyed all 538 cBioPortal studies for pancreatic cohorts and curated the two that earned it. | |
| **Curated set is now 6:** `paad_tcga`, `ccle_broad_2019`, `paad_qcmg_uq_2016`, `paad_utsw_2015`, | |
| **`pancreas_cptac_gdc`** (new), **`pdac_msk_2024`** (new). The UI cohort list reads from the | |
| artifacts, so both appeared with no code change. | |
| **The scarce thing is confirmed absent.** No pancreatic study in cBioPortal carries a real | |
| Moffitt/Bailey/Collisson/PurIST subtype clinical attribute β checked by sweeping all 19,136 | |
| clinical attributes portal-wide, not just the candidates. The two near-misses are both correctly | |
| rejected by `subtype_join.looks_like_pdac_subtype`: `paad_tcga_pan_can_atlas_2018`'s `SUBTYPE` is | |
| the literal string `"PAAD"` (a disease code, 152 patients), and `paad_iatlas_prince_2022`'s | |
| `IMMUNE_SUBTYPE` is Thorsson C1βC6 immune clusters, not PDAC molecular subtypes. | |
| **So `variant_by_subtype`'s in-agent label-join still has no cohort to run on, and this is now a | |
| searched-and-settled fact rather than an untested assumption** β the live-PurIST route through the | |
| orchestrator is the only path, exactly as ADR-0001 has it. Do not re-survey for this; re-check only | |
| if cBioPortal adds a study. | |
| **Curated, with why:** | |
| | Study | n | Modalities | Note | | |
| |---|---|---|---| | |
| | `pdac_msk_2024` | 2,336 | mut + **discrete CNV** | Largest PDAC cohort available anywhere here β 12Γ TCGA. Textbook frequencies (below). **Targeted MSK-IMPACT panel** β see the gate. Commercial use **RESTRICTED**. | | |
| | `pancreas_cptac_gdc` | 183 | mut + **discrete CNV** + expr | CPTAC-3 pancreas, GRCh38, exome-wide, commercial use permitted. The GDC rebuild was chosen over `paad_cptac_2021` (140, same cohort) precisely because that one has **no CNV**. | | |
| **Driver-frequency sanity check passed** (the check that a wildly-off number is a curation bug, not | |
| biology): `pdac_msk_2024` KRAS 93.7% / TP53 76.1% / SMAD4 21.9% (+4.4% deep-del) / CDKN2A 23.4% | |
| (+15.4% deep-del); `pancreas_cptac_gdc` KRAS 83.1% / TP53 69.4% / SMAD4 19.1% / CDKN2A 21.3%. Both | |
| sit on the published ~90/60-70/20/20. (`paad_tcga`'s KRAS 73% remains the known outlier β that | |
| cohort carries non-PDAC histologies.) | |
| **Rejected, and why β more cohorts is not better, each one is committed data someone maintains:** | |
| - `pancreas_msk_2024` (395) β **348 of its 395 samples are also in `pdac_msk_2024`.** Curating both | |
| would double-count every frequency. Its 22 non-PDAC pancreatic histologies are not worth that. | |
| - `paad_icgc` (99) β same ICGC PACA-AU patients as `paad_qcmg_uq_2016` (456), and mutation-only. | |
| Strictly dominated by the cohort already curated. | |
| - `paad_tcga_gdc` (186) / `paad_tcga_pan_can_atlas_2018` (184) β re-releases of `paad_tcga`. | |
| - `pancan_pcawg_2020` (285 PAAD) β an *aggregation* of ICGC PACA + TCGA; overlaps two curated cohorts. | |
| - `ccle_broad_2025` (1,981) β supersedes `ccle_broad_2019` on paper but ships **no discrete CNV** | |
| (LOG2-VALUE only), so it would be a downgrade, and it duplicates the lines we have. | |
| - `paad_iatlas_prince_2022` (93) β mutation-only, and KRAS 73% in a pure-PAAD *metastatic* cohort is | |
| low enough to suggest under-powered calling. No subtype label (see above). Nothing gained. | |
| - `pancreas_ctdna_msk_2025` (412) β **cfDNA, not tumour tissue.** Different detection sensitivity; | |
| its frequencies are not comparable to the tissue cohorts and pooling them would mislead. | |
| - `paad_cptac_2021` (140) β superseded by the GDC rebuild, which has CNV. | |
| - `paad_msk_2025` (29) β too small to add anything. | |
| - `paac_*` / `panet_*` / `pact_jhu_2011` β acinar, neuroendocrine, cystic. Not PDAC; the driver | |
| panel does not describe them. | |
| **New: the per-GENE assay-coverage gate** (`assayed_panel_genes` + `genes_assayed`). The existing | |
| per-*study* modality gate does nothing for a targeted cohort. MSK-IMPACT does not sequence | |
| **`ELAVL1`, `GATA6`, `ACVR1B`** β 3 of our 19 panel genes β so without this, `pdac_msk_2024` would | |
| have reported GATA6 as *0% altered in 2,336 samples*: not a low frequency but no measurement, | |
| wearing the most persuasive number in the repo. GATA6 is the one that would have hurt, since | |
| `CLAUDE.md` names it the clearest variantΓsubtype signal. Curation now resolves coverage from the | |
| `GENE_PANEL` attribute (**intersection** across panel versions, not union β a gene on only some | |
| samples' panel has no cohort-wide denominator), stores `genes_assayed`, and both tools return | |
| `assayed: false` + `unassayed_genes` instead of a frequency. `genes_assayed: null` means | |
| exome/genome-wide; all four pre-existing artifacts are, so **no re-curation was needed** and the | |
| schema was *not* bumped (unlike v1βv2 for terms, a missing key here is a true statement about them). | |
| **Licensing resolved upstream for both** (`licenses.COMMERCIAL_USE_REVIEW` + `STUDY_CITATIONS` + | |
| `ATTRIBUTION.md`), each with its own `reviewed: 2026-07-28` β `study_terms` now honours a per-entry | |
| review date instead of stamping everything with the module default. | |
| - `pancreas_cptac_gdc` β **permitted**, high confidence. NIH/NCI GDS open tier (GDC), the same | |
| instrument as `paad_tcga`; this study is literally a GDC rebuild. CPTAC's embargo is retired. | |
| - `pdac_msk_2024` β **restricted**, and this one needs a human eye. The article | |
| (Varghese et al., Nat Med 2025;31:466β477) is **CC BY-NC-ND 4.0** β non-commercial β while the | |
| authors' own data-availability statement deposits the cohort in cBioPortal as "publicly | |
| accessible and downloadable", where it carries the generic ODbL boilerplate. Both are recorded; | |
| the stricter reading governs, consistent with "resolve upstream". **The open question is ND, not | |
| NC:** the artifact we commit is a *derived* status matrix, arguably an adaptation, and we | |
| redistribute it. Fine for the non-profit research this agent does; anyone going further should | |
| resolve ND with the authors. Tracked in `TODO.md`. | |
| **Found in passing, then fixed the same day β the denominator was wrong, on BOTH modalities.** | |
| Frequencies divided by the whole cohort, but a study assays only part of what it contains, and | |
| never-assayed samples read as `WT`/`neutral` and got counted as denominators. The headline: | |
| > **`paad_tcga` reported KRAS at 73% (136/186). The truth is 91% (136/150) β 36 of its samples | |
| > were never sequenced.** `paad_qcmg_uq_2016` likewise 75% β 90%. | |
| **This retires a "known quirk" that was actually a bug.** The 73% had been explained away as | |
| "the TCGA cohort includes non-PDAC histologies". That explanation was wrong, and it was load-bearing | |
| β it made a wrong number look understood, which is why it survived so long. All four tumour cohorts | |
| now land KRAS at 90β94%, matching the published ~90%. Do not re-derive the histology story. | |
| **The fix** (`profiled` block, schema **v2βv3**): curation records cBioPortal's own declared | |
| per-modality sample lists (`<study>_sequenced`, `<study>_cna`) via | |
| `cbioportal_io.profiled_sample_ids`; `StatusMatrix.denominator(modality)` intersects them with the | |
| (possibly lineage-filtered) cohort; every frequency divides by that and reports `n_profiled` | |
| alongside `n_altered`, so the denominator is stated rather than inferred. Unprofiled samples are | |
| also dropped from `per_sample` (a `neutral` there reads as "measured, normal") and from | |
| `variant_by_subtype`'s contingency table (where they had inflated the WT column and biased the test | |
| toward "no association" β a denominator error that moves a p-value). The UI axis now says | |
| "% of samples profiled for that modality", not "% of cohort". | |
| **Bumped the schema on purpose.** A v2 artifact carries no `profiled` block, which reads as "all | |
| samples assayed" β false for three of the six, and it produces a wrong number that looks right. That | |
| must fail loudly, so v2 artifacts are refused and all six were re-curated. (Contrast `genes_assayed`, | |
| added in the same pass *without* a bump: its absence was a true statement about every prior artifact.) | |
| **Corrected denominators now in the artifacts:** `paad_tcga` 186 cohort / 150 sequenced / 184 CNV; | |
| `paad_qcmg_uq_2016` 456 / 383 (no CNV); `pancreas_cptac_gdc` 183 / 181 / 149; `ccle_broad_2019` 57 | |
| pancreatic lines / 53 / 44; `paad_utsw_2015` and `pdac_msk_2024` fully profiled. | |
| **Tests: 136 passed** (109 before; +12 `tests/test_panel_coverage.py`, +15 `tests/test_profiled_denominator.py`). | |
| **The new literature test is the real guardrail.** `test_kras_lands_near_the_published_90_percent` | |
| pins KRAS at ~90% in every tumour cohort. That test would have caught the denominator bug on day | |
| one; the sanity-check-against-published-frequencies step had been done by eye and not encoded. | |
| **Also fixed: the test suite was not actually hermetic.** `conftest.py`'s docstring claimed "NO | |
| live network", but `patched_cbio` calls `src.curate.curate`, which fetches `study_metadata` β and | |
| that function was never patched. Every fixture-using test made a real request to cbioportal.org per | |
| fixture study. It surfaced as a 12-error, 54-second run mid-session that passed on retry: a third | |
| party's latency reported as our bug. `study_metadata` is now patched from the fixture, and `_req` | |
| is stubbed to `pytest.fail`, so an unanticipated call fails loudly instead of leaving CI. Verified | |
| by running the suite with `socket.connect` blocked outright β **111 tests pass with no network at | |
| all** (the three UI test modules still touch it, but that is Gradio's mocked-OAuth HF call at | |
| import in a local dev env, not our code and not present on a Space). | |
| --- | |
| ## 2026-07-28 β Machine endpoints: caller-side argument errors now say so | |
| Third instance of one lesson in two days, and the pattern is now explicit: **a message that | |
| doesn't name the responsible side sends the reader to the wrong repo.** The auth lane fixed it for | |
| denials (`machine_auth: no_token_header | token_unresolved | not_allowlisted`); this does the same | |
| for arguments (`machine_input: <field>_not_a_string`), including the exact string to send instead | |
| (`genes="KRAS,TP53"`) and an explicit "this is a caller-side argument error β nothing is wrong with | |
| this Space". | |
| **A real bug surfaced next to the message.** `registered_source(source)` is called to build the | |
| audit descriptor *before* the auth gate β outside `_safe_result` β so a non-string `source` raised | |
| there and became an **uncaught 500 rather than a result**. It was the one input that escaped the | |
| catch-all. `_safe_text` now feeds the descriptor defensively; the caller gets the typed error after | |
| the gate. | |
| **Deliberately NOT coercing a list into a string.** Gradio publishes these parameters as `str` in | |
| the endpoint schema (`Client.view_api()`); silently accepting a second undeclared shape would make | |
| the published contract and the real one disagree, and the next caller reads the schema and is | |
| misled again. One contract, with instructions when you miss it. | |
| Ordering is gate-first: an unauthenticated caller gets `denied` and learns nothing about our | |
| inputs β pinned by `test_argument_errors_are_reported_only_after_the_auth_gate`. 132 tests (7 new, | |
| alongside the denial-diagnosis guards they mirror). | |
| --- | |
| ## 2026-07-28 β Hermetic is now *enforced*, and the whole suite is hermetic | |
| Finishes the item above. That fix patched the one hole that had been found; this one removes the | |
| reason holes could survive β and closes the last remaining one. | |
| **The lesson being encoded:** the "NO live network" claim lived in a docstring for weeks while | |
| being false. Nothing checked it. So the property is now enforced by a guard and the guard is | |
| itself under test. Do not "simplify" either away. | |
| - **Socket guard** (`tests/conftest.py`, module scope) β wraps `socket.socket.connect` / | |
| `connect_ex` / `socket.create_connection` for the whole session and raises on any non-loopback | |
| address, naming the host and pointing at conftest for the fix. Installed at *import* of | |
| conftest, not as an autouse fixture, deliberately: the leaks that mattered happened at | |
| **collection** time (a test module's `import gradio_ui` runs the app graph at module scope), and | |
| a fixture runs too late to catch those. Loopback and AF_UNIX are allowed β local sockets are not | |
| what the guard is about. | |
| - **The three UI modules no longer touch the network.** `test_ui_handler_gating.py`, | |
| `test_deploy_surface.py`, `test_machine_api_auth.py` all `import gradio_ui`, whose `gr.Blocks` | |
| graph is built at module scope. Off a Space, gradio's `attach_oauth` falls back to | |
| `_add_mocked_oauth_routes`, which calls HF `whoami` to mock a local login profile. **That is | |
| gradio's debug convenience, not our code**, and it does not happen on the deployed Space (there | |
| `SPACE_ID` is set and the real OAuth routes are used). conftest now stubs | |
| `gradio.oauth._get_mocked_oauth_info`. | |
| - **Why that seam and not the others considered:** it is the only function on that path that | |
| touches the network, so stubbing it leaves every route gradio registers intact and the UI | |
| tests still exercise the real graph. Restructuring `gradio_ui.py` to build Blocks inside a | |
| function was rejected as far more invasive for a test-only problem β and its module-level | |
| `demo` is what HF Spaces launches, so it must keep working. **`gradio_ui.py` is unchanged; | |
| the entire fix is test-only.** | |
| - **Known coupling:** `_get_mocked_oauth_info` is a gradio private. A gradio upgrade that | |
| renames it would break the stub β which is why `test_hermetic.py` imports all three UI | |
| modules explicitly, so that breakage surfaces as a named test failure rather than as a | |
| mystery connection at collection time. | |
| - **`tests/test_hermetic.py`** (new, 7 tests) β asserts the guard is installed, that it refuses a | |
| routable address, that the message names the host *and* points at the fix (a guard with an | |
| unhelpful message gets disabled by the next person who trips it), that loopback still works, and | |
| that the three UI modules import cleanly. | |
| - **Bonus: CI no longer needs an HF login.** `_get_mocked_oauth_info` raises `ValueError` when the | |
| machine has no HF token, so those three modules could not have been imported on a clean CI box | |
| at all. Verified with `HF_TOKEN` unset and `HF_HOME` pointed at an empty dir. | |
| **Verified β 143 passed with sockets blocked outright and no `--ignore`** (136 before + 7 new), | |
| using the same method that exposed the original bug: stub `socket.socket.connect` / | |
| `socket.create_connection` to raise, then `pytest.main(["-q"])`. Previously 111 passed that way | |
| with the three UI modules ignored. | |
| **Mutation-tested, so the guard is not vacuous:** removing the `study_metadata` patch and the | |
| `_req` stub reproduces the original bug, and the guard catches it with | |
| `AssertionError: the test suite attempted a network connection to ('www.cbioportal.org', 443)`. | |
| Restored after. | |
| **Suite runtime 22s β 4s** β that difference *was* the network, on a good day. On a bad one it was | |
| the 54-second, 12-error run that started this. | |
| **No ADR.** The contested part (stub a gradio private vs. restructure `gradio_ui`) is test-only, | |
| reversible, and documented at both the seam and the test. If it should be one, **next free is | |
| 0008** β `main` has 0006 (variantΓsubtype join) *and* 0007 (re-curation cadence) published β and | |
| per `RESERVED.md` the claim must land on `main` alone, first. | |
| β οΈ **This branch's `RESERVED.md` is stale: it stops at 0005 and states "next free 0006".** Both | |
| 0006 and 0007 are burned. This is the third instance of the same failure the ledger's own | |
| postmortem describes β per-lane copies mean the one-shared-row conflict cannot fire between | |
| branches, so *the ledger in your working tree is exactly the thing you cannot trust for this*. | |
| Verify with `git show main:docs/adr/RESERVED.md` before claiming a number. (The rollup suggests | |
| `origin/main`, but `origin` here IS the HF Space; local `main` is the reliable read.) Found by | |
| reading `SHOWCASE_STATUS.md` before updating it β the same habit that caught collision #3. | |
| **Environment note:** this repo now has a venv at `../../.venv` (Python 3.14) created for this | |
| work β `gradio[oauth]==6.18.0` (the `oauth` extra is required or the UI modules fail to import at | |
| all), plus the rest of `requirements.txt` and pytest. Previously the memory said to borrow | |
| `DecoupleRpy_Agent/.venv`. | |
| --- | |
| ## 2026-07-28 β Routing leg VERIFIED end-to-end; dispatch blocked by a private-Space 404 | |
| The one thing the orchestrator registration task could not check headlessly β does the router | |
| actually *classify* a DNA question and dispatch here β is now verified in a signed-in browser | |
| session against the orchestrator **dev** Space (`5cbf1d9`). | |
| Asked: *"Which samples in the TCGA pancreatic cohort carry a KRAS G12D mutation, and what is the | |
| KRAS alteration frequency?"* | |
| **Classification: PASS.** The orchestrator answered *"Routing to PDAC Genomics Agent for | |
| computation."* β it recognised a DNA question and picked this specialist over the subtype and | |
| DE agents. That is the registration working, including the deliberate choice to drop bare `KRAS` | |
| from the trigger keywords (the hotspot allele `G12D` carried the intent instead). | |
| **Dispatch: FAIL, and the error names its own cause.** The call came back: | |
| > The specialist tool server could not be reached⦠`404 Client Error. Repository Not Found for | |
| > url: https://huggingface.co/api/spaces/anne-voigt/pdac-genomics-agent`. If you are trying to | |
| > access a private or gated repo, make sure you are authenticated⦠| |
| This **Space is PRIVATE**, so without credentials the orchestrator cannot even *resolve* the repo, | |
| let alone reach the gate. The failure is a repo-lookup 404, not an allow-list denial β consistent | |
| with the caller-side root cause already found: the orchestrator attaches its token only when | |
| `HF_TOKEN` is set, and that secret is unset on its Space. | |
| **Nothing to change here.** The fix is one secret on the *orchestrator's* Space, and it is a | |
| credential operation for a human. Worth noting the order of failure for whoever does it: with | |
| `HF_TOKEN` unset the symptom is a 404 (invisible private repo); once set, the next thing exercised | |
| is the allow-list β and the identity it resolves to must be on `ALLOWED_IDS` here | |
| (`anne-voigt,cpelz741` today). | |
| --- | |
| ## 2026-07-28 β Repaired `main`: conflict markers were committed and deployed | |
| `main` carried **literal `<<<<<<< HEAD` / `=======` / `>>>>>>>` markers in `memory.md`**, and they | |
| went out to the live Space. Introduced by `b676112`, which was a cherry-pick of the hermetic-suite | |
| commit (`dbaeaa3`) that hit an append-only collision in `memory.md`, was committed unresolved, and | |
| was pushed. Two further things about that commit are worth knowing, because both make it hard to | |
| find again: | |
| - **Its message describes different work.** It reads "verify: routing leg passes; dispatch blocked | |
| by private-Space 404", but its diff is the hermetic suite β `tests/conftest.py` (+109), | |
| `tests/test_hermetic.py` (+69), plus TODO/memory. Searching the log for the socket guard will | |
| not find it. | |
| - **The code half landed fine.** The socket guard and `test_hermetic.py` are correct and complete | |
| on `main`; only the prose merge was broken. So this was cosmetically invisible β tests pass with | |
| conflict markers sitting in `memory.md`, because nothing tests prose. | |
| **Repair:** kept both colliding entries (machine-endpoint argument errors, and hermetic | |
| enforcement) in chronological order β an append-only log has no genuine conflict, only an ordering | |
| question. | |
| **Also corrected a statement `main` was making about itself.** That cherry-pick carried over a | |
| TODO item reading "*This branch's* `RESERVED.md` is stale β it stops at 0005". True of the branch | |
| it was written on; **false on `main`**, whose ledger correctly shows 0006 + 0007 published and next | |
| free 0008. Rewritten to keep the lesson (check `git show main:docs/adr/RESERVED.md`, never your | |
| working copy β the per-lane-copy failure means the shared-row conflict cannot fire) without the | |
| false claim. | |
| **Deliberately NOT carried over** from that branch's follow-up commit (`52bb514`): its narrative | |
| records a *rebase* onto main, but `main` received a **merge** (`80b5e01`) instead, and it flags a | |
| gap β `pancreas_cptac_gdc` / `pdac_msk_2024` missing `source_import_date` β that **does not exist | |
| here**. All six artifacts on `main` carry it (verified), because the merge re-curated all six | |
| rather than trusting git's textual merge of the JSON. Importing that entry would have written a | |
| history that did not happen and a backlog item already closed. | |
| **The generalizable bit:** prose is the only part of this repo nothing verifies. Code has tests, | |
| artifacts have `--check`, licences have the review table β `memory.md` and `TODO.md` have nothing, | |
| so a broken merge in them survives a fully green suite and ships. Worth a lint step (grep the tree | |
| for conflict markers) if it happens twice. | |
| --- | |
| ## 2026-07-28 β Routing re-tested with `HF_TOKEN` set: still 404. It is a VISIBILITY problem. | |
| Re-ran the signed-in dev-orchestrator test after `HF_TOKEN` was set and the Space restarted | |
| (confirmed RUNNING, fresh container start). **Routing passed again** β "Routing to PDAC Genomics | |
| Agent for computation" β and dispatch failed with the *same* repo-lookup 404. | |
| **The token is set.** `router.py` prints a loud startup warning when `HF_TOKEN` is unset; the | |
| container log for the 23:47:29 start shows no such warning. So the earlier "just set HF_TOKEN" | |
| theory is now discharged and the remaining cause is narrower: the orchestrator has *a* token, but | |
| it cannot **see** `anne-voigt/pdac-genomics-agent`. HF returns 404 (not 403) for a private repo the | |
| caller cannot view, which is exactly the observed error. | |
| **The decisive context β this Space is the only private specialist:** | |
| | Space | private? | | |
| |---|---| | |
| | `pdac-subtype-agent-prod` | **No** | | |
| | `Paper2Agent_decoupleRpy` | **No** | | |
| | `pdac-genomics-agent` | **YES** | | |
| The two specialists the orchestrator already calls successfully are public, and they are protected | |
| by the ADR-0004 allow-list gate rather than by repo privacy. We are the outlier. | |
| **Two ways forward, and the choice is architectural, not a bug fix:** | |
| 1. **Grant the token repo-read scope** on this Space (fine-grained tokens need explicit read on the | |
| namespace). Keeps the Space private; keeps us different from the siblings; every future caller | |
| hits the same wall. | |
| 2. **Make this Space public and rely on the enforced gate** β the family's actual convention. | |
| `ACCESS_CONTROL=enforce` + `ALLOWED_IDS` is exactly the protection `pdac-subtype-agent-prod` | |
| runs on, and ours is already enforced and verified in both directions. The redistribution | |
| question this would raise was already handled: `ATTRIBUTION.md` and the full-text | |
| `CCLE_BROAD_TERMS.md` ship beside the artifacts precisely because they are downloadable. | |
| Not doing either unilaterally: option 2 publishes a repository, which is the owner's call. | |
| --- | |
| ## 2026-07-28 β GitHub mirror live (first CI ever); scope gate closed | |
| **Two long-standing items closed, both of which had been waiting on a human.** | |
| **1. GitHub mirror β `Anne-Voigt/pdac-genomics-agent`, PRIVATE.** `main` pushed; `curation-freshness` | |
| is active and was proven with a manual `workflow_dispatch` run rather than left to fire on Monday | |
| and be discovered broken. **This repo had never had CI of any kind** β unlike every sibling it had | |
| no `github` remote at all, because `origin` IS the HF Space and HuggingFace does not run GitHub | |
| Actions. That is why ADR-0007's weekly check shipped inert. | |
| **PRIVATE was a deliberate call, not a default, and it should not be flipped casually.** The Space | |
| is private on purpose, and `src/resources/curated/` redistributes two cohorts with real | |
| restrictions: `pdac_msk_2024` under **CC BY-NC-ND** (the ND term over a *derived* status matrix is | |
| explicitly unresolved β see `licenses.COMMERCIAL_USE_REVIEW`) and `ccle_broad_2019` under Broad's | |
| terms (commercial use restricted, plus a continuity clause binding onward users). A public mirror | |
| publishes both, to a second host, under terms nobody has cleared. **Resolve ND before going | |
| public.** | |
| **Remotes now mean two different things, and confusing them deploys something:** | |
| `origin` β the live HF Space (push to `main` = DEPLOY). `github` β the mirror (push = CI, no | |
| deploy). Push both; they are not interchangeable. | |
| **2. The "should this exist at all" gate is closed.** Per-sample variant *status* is confirmed as | |
| the near-term need. The design doc made this a **pre-M1** confirm; M1βM3 were built without it on | |
| Carl's "build now, ship later", and it had quietly become a pre-deploy confirm that the deploy went | |
| ahead of. Recorded with its provenance rather than as a bare tick: **this is Anne's call as the | |
| consultant, not a direct statement from Dr. Sears or Carl Pelz**, and the original item asked for | |
| theirs. Enough to proceed; NOT the lab having reviewed the tool. A session with Carl is still worth | |
| having before anyone leans on this for real analysis. | |
| **Still blocked on a human, and it is now the only thing between the orchestrator and this agent:** | |
| `HF_TOKEN` must be set in the **orchestrator's** prod Space secrets. Not doable from here β | |
| handling an API token is out of scope for an assistant, and it is the one action in this whole | |
| sequence that genuinely requires the person who owns the credential. This agent's gate is verified | |
| working from both sides; the orchestrator simply sends no header when that secret is unset. | |
| --- | |
| ## 2026-07-28 β Space made PUBLIC; the orchestrator boundary is fixed and proven | |
| Flipped `anne-voigt/pdac-genomics-agent` to **public**, matching the two sibling specialists the | |
| orchestrator already calls (`pdac-subtype-agent-prod`, `Paper2Agent_decoupleRpy`). The protection | |
| model is now the family's: **repo visibility is not the boundary β the ADR-0004 gate is.** | |
| **Scanned before publishing, and it caught something.** `docs/cbioportal-terms-and-deployability.md` | |
| carried a literal personal gmail address in an example `User-Agent`. The implementation had | |
| deliberately never hardcoded a contact (the `+URL` is the channel, with an optional | |
| `CBIOPORTAL_CONTACT` env var); the doc had not caught up. Removed before the flip β publishing | |
| would have made it world-readable and scrapeable. Also verified: no `.env`, no token-shaped | |
| strings, no tracked `run_logs`, audit records go to a separate dataset repo. | |
| **Verified after the flip:** | |
| | check | result | | |
| |---|---| | |
| | anonymous `/panel` (no credential at all) | **denied** β `machine_auth: no_token_header` | | |
| | tokenless client can *resolve* the Space | **yes** (this is exactly what 404'd while private) | | |
| | orchestrator's pattern: resolvable repo + `x-orchestrator-token` | **ok** β real data returned | | |
| `ALLOWED_IDS` remains a **secret**, not a variable β on a public Space, variables are world-readable, | |
| so the sibling's `ALLOWED_IDS` (a variable) currently exposes its collaborator list. Keep ours a | |
| secret; worth fixing there. | |
| **A correctness signal worth noting:** the live answer is now **KRAS 136/150 = 91%**, not the | |
| 136/186 = 73% seen earlier. The difference is the denominator fix that landed from another lane β | |
| frequencies are now over the *profiled* subset rather than the whole cohort. 91% is what the PDAC | |
| literature reports for KRAS; 73% was an artifact of counting unsequenced samples as wild-type. | |
| **Still unverified: the orchestrator's UI end-to-end.** Its chat panel returned no rendered answer | |
| within ~20s across two attempts, and its Space restarted mid-test (fresh container starts at | |
| 23:47:29 and 23:53:05), which wipes conversation state. Routing itself was already proven twice | |
| before the flip ("Routing to PDAC Genomics Agent for computation"), and the dispatch leg is now | |
| proven at the API level β so the gap is orchestrator-side rendering/session, not this agent. | |
| --- | |
| ## 2026-07-28 β Pre-push hook: the mirror now follows the deploy | |
| `scripts/hooks/pre-push` + `make install-hooks` (this repo's first Makefile; the pattern copies | |
| `DecoupleRpy_Agent/scripts/hooks/pre-push`). Pushing `main` to `origin` β i.e. deploying β now | |
| mirrors that same commit to `github` first, so **CI can never run against less than what is live**. | |
| **Why "first" and not "after": git has no post-push hook.** So a failed deploy can leave the mirror | |
| briefly AHEAD. That is deliberately the error we accept: CI running a superset of production | |
| self-corrects on the next deploy, whereas CI running a *subset* β the stale-mirror case β reads as | |
| a pass. Picking which way to be wrong is the whole design decision here. | |
| **A mirror failure warns and never blocks.** The mirror is CI; `origin` is production. Refusing to | |
| ship because a secondary remote was unreachable is disproportionate, so every path exits 0 and the | |
| warning names the one command that fixes it (a test asserts `exit 1` appears nowhere in the hook). | |
| Verified by driving the hook's stdin protocol directly rather than by pushing six times: non-deploy | |
| remote, non-`main` branch, **branch delete** (all-zero local sha β must never mirror a deletion), | |
| missing mirror remote, a real in-sync mirror push, and a deliberately broken remote to see the | |
| warn-don't-block path. All six correct. | |
| **Worktree gotcha worth keeping:** the installer uses `git rev-parse --git-path hooks`, not a | |
| literal `.git/hooks`. Every lane in this repo works from a linked worktree where `.git` is a *file*, | |
| so the naive path installs nothing and fails silently. Checked empirically that git honours the | |
| resolved path from inside a worktree β and that it resolves to the SHARED `.git/hooks`, so one | |
| install covers every lane. Pinned by `test_makefile_installs_the_hook_where_git_looks`. | |
| **Still needs one manual step per clone:** hooks are not versioned by git, so `make install-hooks` | |
| has to be run once. A lane that skips it is back to remembering `git push github main`. | |
| Also corrected `CLAUDE.md`: the Space went **public** in `0ffadd1`, so the two-remote table's | |
| "(private)" was already wrong. The mirror stays private. | |
| 169 tests. | |
| --- | |
| ## 2026-07-28 β The orchestrator β this agent path is LIVE and proven in production | |
| The three-tier architecture stopped being a design and started answering questions. Verified in the | |
| prod orchestrator UI, signed in as `anne-voigt`: | |
| > *"Which pancreatic cell lines carry a KRAS mutation?"* β **"Routing to PDAC Genomics Agent for | |
| > computation"** β 57 pancreatic lines (lineage filter), **48/53 mutation-profiled (90.6%) | |
| > KRAS-hotspot** (G12D dominant; MIAPACA2 = G12C; Q61H/Q61R), **13/44 CNV-profiled (29.5%) also | |
| > amplified**, CCLE commercial-use restriction + Ghandi 2019 / cBioPortal citations in the answer. | |
| Two things worth noting in that output. The **two denominators** (53 vs 44) are the per-study | |
| modality-coverage gate working β it did not pretend 57 everywhere. And **BxPC-3 is correctly absent** | |
| from the mutant list: the canonical KRAS-wild-type control line, so the biology is right, not just | |
| the plumbing. | |
| **The blocker was never auth β and there were TWO blockers wearing one symptom.** Four theories died | |
| on the way here: "transient `whoami`" (wrong), "`Bearer None` broken probe" (right about that probe, | |
| not about why prod failed), "`HF_TOKEN` unset on prod" (wrong β it was set all along; the prod | |
| orchestrator's startup block carries no unset-token warning), and by then the *dev*-side failure had | |
| already been correctly reframed by another lane as **visibility**, fixed by making this Space public | |
| (`0ffadd1`) β see that entry above. | |
| What remained, and what this entry adds: **`agents.yaml` as deployed on the orchestrator's PROD | |
| (`f32afbd`, 07-24) contained no `pdac-genomics-agent` at all.** The registration had only ever | |
| reached its `dev`. A router cannot dispatch to an agent it does not know exists, so prod answered | |
| from the Claude API and recommended DepMap/COSMIC/cBioPortal β while this agent, built on cBioPortal, | |
| sat one dispatch away. Fixed by promoting `dev` β prod (`11452f7`, 6 commits, +1381). | |
| So the two Spaces were failing for two unrelated reasons at once β dev couldn't *see* this Space, | |
| prod didn't *know about* it β which is exactly why single-cause theories kept half-fitting the | |
| evidence and dying. | |
| **Method note, again the same lesson as the machine-auth entry above:** the theories were all built | |
| from code and logs; the answer came from *driving the actual UI end-to-end*. When a cross-Space path | |
| fails, exercise the real path before theorising about which layer broke β and check the deployed | |
| config, not the branch you happen to have checked out. | |
| **Bonus, relevant to ADR-0006:** the orchestrator's half of the join now exists | |
| (`cross_modality_join.py` + its own ADR-0003), and it carries an **agreement test that imports this | |
| repo's `contingency_and_test` and asserts identical output** β which is exactly the "two | |
| implementations must agree or the same cohort answers differently by route" risk ADR-0006's | |
| Consequences flagged. That risk is now pinned by a test rather than by prose. β οΈ Its success path is | |
| still fixture-only: **no cohort can actually run the live-PurIST join yet** (id-alignment failures on | |
| every pair tried so far), so treat the first real cohort as a first run, not a regression check. | |
| --- | |
| ## 2026-07-28 β Mouse/KPC spike: the refusal STANDS (no code changed) | |
| Discovery spike, no implementation: `docs/spikes/SPIKE-mouse-kpc-variant-path.md`. | |
| **Verdict FAILED as a feature.** `byod.py`'s `non_human_species` gate and CLAUDE.md's mouse hard | |
| gate are unchanged and should stay unchanged. `RESERVED.md`'s "Mouse/species variant boundary" | |
| entry stays **unclaimed** β this spike is the evidence for *not* writing that ADR. | |
| Four independent reasons, each sufficient: | |
| 1. **No data.** Probed the public cBioPortal: **538 studies, refGenome β {hg19Γ466, hg38Γ72}, | |
| zero mouse builds**; the two keyword hits are human (one is a **PDX** study β human tumour in a | |
| mouse, i.e. evidence *against* mouse coverage). The gene namespace rejects MGI symbols | |
| (`Trp53`/`Kras`/`Cdkn2a`/`Smad4` β `Gene not found`), so `curate_from_cbioportal` would return | |
| an empty matrix. A registered mouse path = adopting a whole second source, not a new study id. | |
| 2. **The flagship question is tautological.** KPC is engineered (`LSL-Kras^G12D; Trp53^R172H; | |
| Pdx1-Cre`), so "KRAS status in my KPC mice" restates the breeding record β and our response | |
| shape (per-sample status + cohort frequency) is structurally indistinguishable from a genuine | |
| somatic finding. 100% KRAS-hotspot would be a fact about the colony, not the disease. | |
| 3. **The hotspot rules silently corrupt.** Human TP53 (393 aa) vs mouse Trp53 (390 aa) is a clean | |
| **β3 offset**: our `HOTSPOTS["TP53"]` 175/248/273/282 β mouse 172/245/270/279. So the canonical | |
| KPC allele **`Trp53 R172H` classifies as `missense`, not `hotspot`**, and a mouse variant at 175 | |
| would be *promoted* spuriously. KRAS is the trap β codons 12/13/61 ARE conserved (identical | |
| first 90 aa), which generalises to the wrong conclusion. | |
| 4. **CNV has no cross-species mapping at all** β GISTIC calls are assembly-specific, and half the | |
| panel's signal (CDKN2A deep-del, MYC amp) is CNV. CDKN2A also breaks orthology at the protein | |
| level (ARF: human p14 132 aa vs mouse p19 169 aa). | |
| **ADR-0017 is not precedent for this β reading it that way inverts its argument.** It permitted the | |
| analyses with no fitted human-specific object (GSVA) and gated the one that had (PurIST). Our | |
| hotspot table is exactly a fitted human object (human residue positions), so it lands in the PurIST | |
| column β and differs only in being *worse*: PurIST on mouse is unvalidated, the TP53 rule is | |
| arithmetically wrong. ADR-0017's 1:1 gene-level ortholog table also cannot help: expression needs | |
| **gene**-level mapping, variants need **residue**-level equivalence per isoform. | |
| **The real need reframed:** not per-sample mouse status but "how does my engineered model compare | |
| to the human cohorts?" β mostly an *expression* question already owned by `pdac-subtype-agent`. | |
| Spike proposes one small alternative for Carl's review, deliberately **not** built: | |
| `compare_model_genotype_to_human_cohort`, which takes the *declared* genotype (not a MAF), maps | |
| ~5 KPC alleles to human equivalents via a tiny hand-curated table, and reports the human cohort | |
| frequency from artifacts we already have. No mouse data, no new source, no ADR needed. Default | |
| outcome β build nothing β remains fully defensible. | |
| --- | |
| ## 2026-08-01 β Branch reconciliation: seven lanes collapsed into `main` | |
| A week of parallel worktree lanes had left **7 local branches + 5 worktrees**, several of them | |
| rebase copies of each other, and it was no longer obvious which carried work `main` lacked. Audited | |
| all of them; the answer was that `main` was already a superset of everything except **one** commit. | |
| **The only unmerged work: `claude/infallible-payne-2134a8` (`e364948`)** β the mouse/KPC spike. | |
| Cherry-picked onto `main` as `84001c2`. Its `docs/spikes/SPIKE-mouse-kpc-variant-path.md` and the | |
| `RESERVED.md` annotation applied cleanly; `TODO.md` and `memory.md` conflicted because the branch | |
| forked back at `085b9f7` and both files have been rewritten since. Resolution kept `main`'s newer | |
| text in both and grafted in only the genuinely new spike entries. Two blocks from the branch were | |
| **deliberately dropped as superseded, not lost**: | |
| - its scope-confirm entry said "confirmed with Rosie/Carl"; `main`'s version records the same gate | |
| more precisely (it was **Anne's** call as consultant, explicitly *not* the lab having reviewed the | |
| tool). Over-reading that is the exact error `main`'s wording guards against. | |
| - its "three operational blockers" list, plus the correction underneath saying they were already | |
| closed. `main` records the closed state directly (`ALLOWED_IDS` set, gate verified both ways, | |
| orchestrator reaching the machine API), so re-importing the stale-then-corrected pair would have | |
| put a contradiction back into the file. The durable lesson from it β `_resolve_token_identity()` | |
| doing an outbound `whoami` per request under a broad `except` β is already recorded on `main`. | |
| **`claude/suspicious-cannon-b68c4b` looked like unmerged work and was not.** All three of its | |
| commits (`75746cf` cohorts/denominators, `dbaeaa3` socket guard, `52bb514` memory) are content that | |
| reached `main` by another route β `git cherry` flagged them `+` only because the patch-ids differ | |
| after rebasing. Verified file-by-file rather than by subject line: `tests/conftest.py` is identical | |
| and `main`'s `test_hermetic.py` is strictly *larger* (it has the conflict-marker test the branch | |
| lacks). Same story for `origin/claude/infallible-kowalevski-2d4648` β its three "unique" | |
| `gradio_ui.py` lines are pre-denominator-fix code that `main` superseded. | |
| **The method worth reusing:** `git cherry` / branch names / commit subjects all lie after a rebase. | |
| What actually settled it was `diff <(git ls-tree -r main) <(git ls-tree -r <branch>)` for unique | |
| *files*, then a per-file `diff` of the suspects. Only one branch of seven had a unique file. | |
| Deleted: 7 branches, 5 worktrees, and the 2 redundant `origin/claude/*` remote branches. | |
| **Every deleted tip is preserved as an annotated `archive/<branch>` tag**, matching the convention | |
| the orchestrator and `pdac-subtype-agent` lanes used the same day β pushed to the `github` mirror | |
| (the private remote; deliberately **not** to `origin`, which is the public Space). Each tag's | |
| message carries that branch's audit verdict, so the reasoning survives with the object instead of | |
| only in this file: | |
| | tag | sha | verdict | | |
| |---|---|---| | |
| | `archive/claude/busy-herschel-2bc3c9` | `f91fafb` | identical to `main` | | |
| | `archive/claude/dazzling-moser-5d97a7` | `6a17ae2` | ancestor of `main` | | |
| | `archive/claude/gallant-ellis-5ca74e` | `64d7f04` | merged via `80b5e01` | | |
| | `archive/claude/infallible-kowalevski-2d4648` | `09cba72` | ancestor of `main` | | |
| | `archive/claude/infallible-payne-2134a8` | `e364948` | β οΈ **unreachable** β the one real branch; cherry-picked as `84001c2` | | |
| | `archive/claude/suspicious-cannon-b68c4b` | `52bb514` | β οΈ **unreachable** β no unique content (the rebase-lookalike case) | | |
| | `archive/pick/hermetic` | `a90b9fc` | ancestor of `main` | | |
| **Only the two marked unreachable actually needed a tag** β the other five are ancestors of `main` | |
| and were never at risk. Verified rather than assumed: after `git reflog expire | |
| --expire-unreachable=now --all && git gc --prune=now`, both `e364948` and `52bb514` still resolve. | |
| Recorded SHAs in prose would **not** have survived that; this is the difference between the tag | |
| habit and the note habit, and it is why the tag habit is the right one. | |
| The `infallible-payne` tag additionally records the two blocks of `e364948` that were deliberately | |
| **not** carried onto `main` (the over-read scope-confirm; the stale-then-corrected blockers list), | |
| so a later reader who diffs the tag against `main` and sees content "missing" finds the reason | |
| attached to the object rather than concluding it was lost. | |
| 174 tests pass after the merge, including the conflict-marker guard β the test that exists | |
| precisely because a botched prose merge once shipped to the Space. | |
| **Deployed:** `f91fafb..2ca2107` to prod; build watched through `RUNNING_BUILDING` β | |
| `RUNNING_APP_STARTING` β `RUNNING`, direct host HTTP 200. The `pre-push` hook mirrored to `github` | |
| first, so CI tests the deployed tree. All three refs at `2ca2107`, one branch. | |
| β οΈ **Still unverified:** the gate was not re-exercised (needs the `x-orchestrator-token` | |
| credential) and no signed-in click-through was done. This deploy is docs/tests only over `894e710`, | |
| so served behaviour should be unchanged β but `RUNNING` + HTTP 200 proves the app boots and serves, | |
| not that it answers correctly. | |
| --- | |
| ## 2026-08-03 β the two remaining small code items, closed | |
| Both were long-tail "Open β payload contract" / mouse-spike follow-ups; neither changes behaviour | |
| for any deployed caller. | |
| **1. `query_variant_status([])` now expands to the full panel** (`src/tools/query_variant_status.py`). | |
| The machine endpoint's blank `genes` already did this via `_split_genes` in `gradio_ui.py`; calling | |
| the Python function directly with `[]` returned an *empty* `genes` map, which reads as "nothing | |
| altered" rather than "you asked about nothing" β the same class of confident-false-negative the | |
| coverage gates exist to prevent, just aimed at a direct caller instead of an orchestrator. The two | |
| entry points now agree. Test asserts `[]` and `load_panel()` return the identical `genes` map. | |
| **2. The BYOD `non_human_species` refusal now names its reason** (`src/workflows/byod.py`). It said | |
| only "not supported", which reads as an arbitrary gate. It now states the actual failure mode: | |
| classification is keyed on **human residue coordinates**, so mouse `Trp53 R172H` β the ortholog of | |
| human `R175H` β would score as generic missense against our human hotspot table, and CNV has no | |
| cross-species mapping at all. Points at the mouse **expression** path in `pdac-subtype-agent` and at | |
| `SPIKE-mouse-kpc-variant-path.md`. String change only; the refusal itself is unchanged and still | |
| STANDS per that spike. | |
| 176 tests pass (was 174) β two new, one per item. | |
| --- | |
| ## 2026-08-05 β six backlog decisions taken, and the panel widened 19 β 29 | |
| A backlog-triage session with Anne. Five open items closed by decision rather than by code, one | |
| reopened with a real shape, and the one remaining code item β the panel widening β built. | |
| ### The decisions | |
| **ND on `pdac_msk_2024` β RESOLVED, and it unblocks the mirror.** The CC BY-NC-ND licence attaches | |
| to the *article and its supplementary material*; the authors deposited this *cohort* separately as | |
| publicly accessible and downloadable data, so the deposition is the instrument governing the data | |
| and a derived status matrix is not an ND-restricted adaptation. **NC is untouched** β commercial use | |
| still barred, verdict stays `restricted`. Recorded as a reasoned consultant position, explicitly NOT | |
| an author grant. This removes one of the two blockers on making the GitHub mirror public; | |
| `ccle_broad_2019`'s Broad terms are the other and have **not** moved. | |
| **ICGC open tier β `no_restriction_found` β `permitted`.** `commercial_use_permitted()` now returns | |
| True for `paad_qcmg_uq_2016`. E.4 survives the upgrade and still binds commercially (no IP claims | |
| over primary somatic data; downstream licensing must not obstruct further research) β a test pins | |
| that clause to the entry, because a grant is not the absence of obligations. β οΈ The granting clause | |
| itself was established off-repo and is **uncited** in the entry; that gap is recorded in the entry's | |
| own `confidence` field, not only in `TODO.md`, since a caller can now rely on the verdict. | |
| **`whoami` stays.** The open item asked whether the machine gate should drop its outbound `whoami` | |
| for a shared secret. It should not, and the reasoning inverts the item: a shared secret is one | |
| credential shared by all callers, so every machine call would audit as the same undifferentiated | |
| principal. `principal_trace_fields` records `{principal, role}` per run precisely so that field | |
| names a person. The coupling buys per-user attribution β unlike ADR-0005 C4's, which bought nothing | |
| the artifact didn't already give us. Successor item is narrower and real: the resolution cache's TTL | |
| and what happens on an HF outage mid-session (fail closed vs. serve on a warm cache). | |
| **Mouse `compare_model_genotype_to_human_cohort` β DECLINED.** Not deferred, not blocked on Carl. | |
| The refusal in `byod.py` is the whole answer; the species-boundary ADR stays unclaimed. | |
| **Stub-vs-restructure ADR β CLOSED, not contested.** An ADR for an uncontested three-line test-only | |
| decision is ceremony. The gradio-upgrade watch item carries the residual risk. | |
| **`biodata-registry` β REOPENED, with the actual obstacle named.** Anne wants the registry's | |
| structure reused, and the objection was never philosophical: the registry **does not model DNA**. | |
| `VALID_MODALITIES` is expression-only, and its value-add (`data_level` β path A/B/P, | |
| `get_integration_plan`) has no meaning for a status matrix. What transfers is the *pattern*, which | |
| this repo already reimplemented locally as `curated/*.json` + `COMMERCIAL_USE_REVIEW`. The proposal | |
| is to extend the registry with a variant modality and move cohort semantics there β a cross-repo | |
| change (new manifest kind, version bump, re-pin everywhere) needing **ADR-0008** and its own | |
| milestone. | |
| ### The panel: 19 β 29 genes, all seven artifacts re-curated | |
| Added HR-beyond-BRCA (`RAD51C`, `RAD51D`, `BRIP1`, `BARD1`, `CHEK2`, `FANCC` β PARP-inhibitor | |
| eligibility) and MMR (`MLH1`, `MSH2`, `MSH6`, `PMS2` β MSI-high / pembrolizumab). Both tiers are | |
| answerable from mutation+CNV alone. | |
| **The fusion-driven actionable genes were deliberately left off** β `NRG1`, `NTRK1/2/3`, `ALK`, | |
| `ROS1`, `RET`. In PDAC these are structural-variant events, so a mutation+CNV status matrix would | |
| report ~0% for exactly the alterations that matter, on the most clinically actionable question this | |
| agent could be asked. They stay off the panel until a structural-variant modality exists (now a | |
| backlog item with its own coverage-gate requirement). | |
| New-gene frequencies are literature-plausible (0β2%; MSI-H PDAC is ~1%), and KRAS is unchanged at | |
| **90.7% TCGA / 93.7% MSK**, so the profiled-denominator work did not regress. MSK-IMPACT assays all | |
| ten new genes, so those MSK numbers are real rather than gated false negatives; its three unassayed | |
| genes are still `ELAVL1`/`GATA6`/`ACVR1B`. | |
| ### The bug the widening exposed | |
| **Widening the panel is not config-only** β every curated artifact bakes in the `panel_genes` it was | |
| curated against, and `query_variant_status` filtered requested genes down to what the artifact | |
| carried. A panel gene absent from a not-yet-re-curated artifact therefore **vanished from the | |
| payload with no note**, and a caller reads a short `genes` map as "we looked and found nothing". | |
| Same confident false negative the `assayed: False` gate exists to prevent, reached by another door, | |
| and it would have fired on any future widening too. | |
| Such a gene now returns `{curated: False, note: ...}` and is named in `uncurated_genes`. Kept | |
| **distinct from `assayed: False` on purpose**: that one means the cohort's assay never interrogated | |
| the gene (a fact about the data, unfixable here), this one means we have not curated it yet (a fact | |
| about our snapshot, fixed by re-running `python -m src.curate <study>`). Collapsing them would tell | |
| a user their cohort cannot answer a question a re-curation answers fine. | |
| Two tests: the gap is stated rather than dropped, and every committed artifact carries the *current* | |
| panel β the second fails loudly if a future widening skips the re-curation pass. | |
| ### Test-suite notes | |
| 179 pass (was 176). Three existing tests needed changing, and two of them were pinning the wrong | |
| thing: `test_panel_endpoint_returns_the_v1_panel` hardcoded `n_genes == 19` and now asserts the | |
| endpoint agrees with `load_panel()`, and `test_no_restriction_found_is_not_reported_as_permitted` | |
| pinned `paad_qcmg_uq_2016` and broke when that study's verdict legitimately changed β it now tests | |
| the *function's* semantics across all verdicts, which is the rule it was always guarding. The | |
| scaffold smoke test's `len(genes) == 29` is pinned deliberately, so a panel edit that skips | |
| re-curation cannot drift in quietly. | |
| **Not deployed** *at the time this entry was written* β code, artifacts, licences and docs only. | |
| **Superseded the same day: this shipped as `7ae3398`** β see the next entry. | |
| --- | |
| ## 2026-08-05 (later) β DEPLOYED the panel widening, then built the structural-variant modality | |
| Two deploys today. The first shipped the morning's 19 β 29 widening; the second is the SV work. | |
| ### Deploy 1 β the panel widening went live (`7ae3398`) | |
| `35c0da0..7ae3398` to `origin`, hook mirrored to `github` first, all three refs aligned. Build | |
| watched `RUNNING_BUILDING` β `RUNNING_APP_STARTING` β `RUNNING` at `7ae3398`, root HTTP 200. | |
| Security scan clean (the three bandit findings β B104 bind-all on the MCP server, B311 jitter, | |
| B310 urlopen β are pre-existing and in files this change did not touch). | |
| **Verification note worth keeping:** the machine endpoint correctly returned `status: denied` to | |
| an unauthenticated `gradio_client` call, which is the enforced gate working, not a deploy failure. | |
| Since a signed-in click-through needs a browser, the deployed TREE was verified instead by | |
| fetching files from the Space over `resolve/main`: panel 29 genes, all seven artifacts | |
| `panel_genes=29` / schema v3 / curated 2026-08-05, and `uncurated_genes` present in the deployed | |
| tool. **A signed-in click-through is still owed** β tree verification proves what is served, not | |
| that it answers correctly. | |
| ### Deploy 2 β the structural-variant modality (ADR-0008) | |
| Third modality alongside mutation/CNV. Statuses `none` / `fusion_in_frame` / | |
| `fusion_out_of_frame` / `intragenic` / `rearrangement`, somatic only, schema **v3βv4**, panel | |
| **29 β 36** with the fusion tier finally added. `ADR-0008` was claimed on `main` alone and pushed | |
| before the ADR was written, per the ledger protocol. | |
| **Probing cBioPortal before designing was the whole game.** Three things the probe found, none of | |
| which were guessable: | |
| **Only 2 of 7 cohorts publish SV at all** β `pdac_msk_2024` and `ccle_broad_2019`. The other five | |
| name `sv` in `unavailable_modalities` and emit no `sv` block. The per-study gate now fires far | |
| more often than it ever did for CNV. | |
| **The two SV cohorts differ in KIND, not just volume.** MSK states frame ("Protein Fusion: in | |
| frame {ETV6:NTRK3}"); CCLE has `variantClass: NA` on **all 153** panel-gene rows and labels | |
| events only as free text ("EML4-ALK fusion"). So classification reads `eventInfo`, never | |
| `variantClass` β the latter describes the genomic event, not its protein consequence, and a | |
| classifier keyed on it would have promoted every translocation to a "fusion". And a per-study | |
| `sv_annotation` flag (`characterized` / `uncharacterized`) collapses every event in an | |
| uncharacterized cohort to `rearrangement`, including the odd row that mentions a frame, so a | |
| frequency is never a mixture of "genuinely in-frame" and "we could not tell". Those answers carry | |
| a caveat saying an absence of in-frame fusions there is a fact about the annotation, not the | |
| biology. Without it, someone comparing MSK to CCLE would read "0 in-frame fusions in cell lines" | |
| as a finding. | |
| **The per-gene gate was wrong for SV β a real bug, surfaced by building the modality.** | |
| `genes_assayed` derives from the DNA gene panel (IMPACT341/468/505β¦), so it answers "was this | |
| sequenced for mutations and copy number?" and nothing else. Fusion calling is a separate assay | |
| with a different gene set: MSK reports **6 somatic NRG1 rearrangements while NRG1 is absent from | |
| the DNA-panel intersection**. The old gate suppressed all six. That is the mirror image of the | |
| failure the gate was built for β refusing an answer we hold, rather than inventing one we don't β | |
| and both are dishonest. The gate is now modality-scoped: off the DNA panel **with** observed SV | |
| events β `assayed: false` *and* a valid `sv` block plus a note forbidding the reader from taking | |
| the SV frequency as a mutation frequency; off the panel with **no** events β still fully refused. | |
| Evidence overrides the gate toward reporting, never toward a zero. | |
| Also: `sv_provenance` is a separate dict from `provenance`. Same `(gene, sample)` key, and | |
| TP53/CDKN2A each carry a mutation *and* a fusion in the same sample, so sharing one would have let | |
| a rearrangement overwrite a `KRAS G12D`. | |
| **ADR-number near-miss, checked and cleared (the fifth in this family).** `audit.py`, | |
| `logging_sink.py` and `tests/test_audit.py` all said *"ADR-0008 parity"* before this repo had an | |
| 0008 β that is the SIBLINGS' numbering (`pdac-subtype-agent` and `DecoupleRpy_Agent` each numbered | |
| their own audit-logging ADR 0008). **ADR numbers are per-repo**; only genuine cross-repo standards | |
| burn a number family-wide, and `RESERVED.md` lists those explicitly (0014, 0015). So 0008 was free | |
| here. The three references were reworded to name the sibling repo, since "ADR-0008 parity" now | |
| points at the wrong decision. Recorded in the ledger with the lesson: never cite a bare `ADR-####` | |
| for a sibling's decision. | |
| Frequencies: NRG1 0.26%, NTRK3 0.21%, ROS1 0.21%, NTRK1 0.17%, RET 0.09%, ALK 0.04% in MSK. KRAS | |
| SV is a real measured **0%** (on the DNA panel, so genuinely interrogated) against its 93.7% | |
| mutation rate β a good illustration that the modalities are independent. | |
| 204 tests pass (was 179): 25 new in `tests/test_structural_variants.py`, most of them about what | |
| the modality *declines* to say. Four existing tests needed shape updates (modalities dict gains | |
| `sv`, MSK's unassayed set gains NRG1, the denominator map gains `sv`, panel count 29 β 36); the | |
| UI's `_frequency_frame` needed no change β its existing `"frequency" in block` guard already | |
| skips the new mixed `assayed`/`note` entries and picks up `sv` automatically. | |
| **Still open on this modality:** `variant_by_subtype` does not accept `modality="sv"` (per-gene | |
| counts are 1β6 in 2,336, so nearly every cell would be `testable: false` β decide if that is worth | |
| exposing), and BYOD has no SV path (an uploaded fusion table cannot answer the annotation-depth | |
| question, so it should refuse explicitly rather than ignore an SV column). | |
| --- | |
| ## 2026-08-05 (third pass) β the two SV follow-ups, which turned out to be two more live bugs | |
| Closing the two items left open by the SV modality. Both were framed as "missing features"; both | |
| were actually **silent wrong answers** already in production. Same defect class as everything else | |
| today: a fallback where there should have been a refusal. | |
| ### `variant_by_subtype` answered a different question than it was asked | |
| The modality branch was `if modality == "cnv": β¦ else: mutation`. So `modality="sv"` produced a | |
| MUTATION association β and so did `"banana"`, and so did a typo like `"mutaton"` β returning | |
| `join_available: true` with real p-values. The payload did say `"modality": "mutation"`, so a | |
| scrupulous caller *could* have caught it, but the orchestrator asked about fusions and received a | |
| confident answer about point mutations. | |
| The modality set is now **closed** (`mutation` / `cnv` / `sv`), and an unrecognized value returns | |
| `route: "unsupported_modality"` with `supported_modalities`. **Blank and whitespace-only still | |
| mean "unspecified"** and default to mutation β the machine endpoint sends blank for an untouched | |
| field, so refusing that would break the most ordinary call there is. (A test caught the | |
| whitespace case: `(" " or "mutation")` is truthy, so defaulting before stripping left `""` and | |
| refused. Strip first, then default.) | |
| Fixing the validation is what made *not* wiring SV a safe choice rather than a silent one β worth | |
| recording, because "leave it unwired for now" looked conservative and was the opposite. | |
| ### The sparse-table floor, and why it applies to everything | |
| `MIN_ALTERED_FOR_TEST = 5`. Below it a gene reports `testable: false` **with its contingency | |
| counts** instead of a p-value. Fisher's exact returns a number from a 2Γ2 containing a 2, and an | |
| odds ratio of β from a single-sample cell reads as a strong finding β arithmetically correct, | |
| scientifically empty. SV made this unmissable (1β6 events per gene in 2,336 samples), but the | |
| floor is applied to **every** modality: a mutation association resting on 3 altered samples is | |
| exactly as empty, and a rule that fired for one modality and not another would be a distinction | |
| the reader cannot see. Nothing is hidden β the counts still ship, only the inference is withheld. | |
| This changed one existing test, and the change is the lesson in miniature: | |
| `test_variant_by_subtype_joins_existing_label` asserted that `fisher_exact` ran on a **five-sample | |
| fixture**. That assertion was the bug written down as an expectation. | |
| ### An uploaded MAF reported fusions as missense mutations | |
| `classify_mutation` has no fusion branch, so `Variant_Classification: Fusion` is neither silent nor | |
| truncating and fell through to **missense**. Latent for months and harmless while the panel held | |
| no fusion genes β it became serious this morning, when NRG1/NTRK1-3/ALK/ROS1/RET joined, because | |
| an upload would then report precisely the actionable fusion genes as point mutations. | |
| Now a `BYODRefusal("sv_not_supported")` naming why: a fusion status is only meaningful alongside an | |
| annotation-depth claim (in frame or not) that an uploaded row cannot make (ADR-0008), and the | |
| alternative is the missense answer above. It points at the registered cohorts that *can* answer. | |
| **The registered path was checked and is clean** β cBioPortal keeps fusions in the SV profile, and | |
| no curated artifact carries a fusion-like string in its mutation provenance. Blast radius was BYOD | |
| only. Also added `"sv": False` to the BYOD modalities dict: the behaviour was already "no SV", but | |
| `unavailable_modalities` is built from that dict, so a BYOD answer never *named* structural | |
| variants as unavailable. Silence about a modality is what this whole family of gates exists to | |
| prevent. | |
| 219 tests pass (was 204). | |
| **Pattern worth naming, since it has now happened four times in one day:** every one of these bugs | |
| was a default that fired where a refusal belonged β absent gene dropped from the payload, DNA-panel | |
| gate applied to fusions, unknown modality falling back to mutation, fusion row falling through to | |
| missense. None of them raised. All of them produced a well-formatted, citation-stamped answer to a | |
| question nobody asked. **When reviewing this codebase, grep for `else:` on a branch that picks a | |
| modality, a gene set, or a status β that is where they live.** | |
| --- | |
| ## 2026-08-05 β dev lane established (local half); mirror hook fixed | |
| `32b56aa` on the new `dev` branch. This was the **only showcase agent repo without a dev Space** β | |
| `main` IS the public prod Space, so every deploy shipped straight to production with no staging. | |
| What landed (all local, nothing on HuggingFace): | |
| - `hf-dev` remote β `anne-voigt/pdac-genomics-agent-dev`. **The Space does not exist yet** β that | |
| is Annie's call. Until it does, `hf-dev` resolves to nothing and `git fetch --all` warns on it. | |
| - `dev` cut from `main`, tracking `hf-dev/main`; `main` tracking `origin/main`. Both branches | |
| previously tracked *nothing*, which is why unpushed work here was invisible to `git status` | |
| and the generated fleet table showed `(no upstream)` instead of an ahead-count. | |
| - **`scripts/hooks/pre-push` bug, found by adding the remote and fixed in the same commit.** The | |
| hook selected deploy pushes with `origin*|*huggingface.co*` and then acted on anything headed | |
| for `refs/heads/main`. `git push hf-dev dev:main` satisfies BOTH β so the first dev-Space push | |
| would have mirrored unreleased dev code onto the GitHub mirror's `main`, where the ADR-0007 | |
| freshness job reads it as production. Exactly the wrong-CI failure the hook exists to prevent, | |
| inverted. Now bails on `hf-dev` (and any `*-dev` URL) before the deploy test. All four paths | |
| exercised by piping synthetic stdin at the hook: hf-dev by name, hf-dev by URL, github, origin. | |
| - `CLAUDE.md`: three-remote table, branch-lifecycle section, and a correction β the | |
| memory-maintenance list still told agents to hand-edit `SHOWCASE_STATUS.md`, which is generated | |
| from `status/` fragments now. | |
| **To activate**: create the Space (private, gradio `6.18.0`, py3.11, hardware matching prod), then | |
| copy `ACCESS_CONTROL` / `ADMIN_IDS` / `ALLOWED_IDS` / `LOG_SINK_HF_DATASET` (a **separate** dataset, | |
| so staging runs don't pollute `anne-voigt/pdac-genomics-results`) / `LOG_SINK_HF_TOKEN` / | |
| `CBIOPORTAL_CONTACT`. Repo is 588 KiB packed with no LFS payload, so the push is cheap. | |
| **Update, same day β the dev Space is LIVE.** Created `anne-voigt/pdac-genomics-agent-dev` | |
| (private, gradio, `cpu-basic` matching prod's free tier), pushed `dev:main`, watched | |
| `BUILDING β APP_STARTING β RUNNING` at `9b5fe82`. `dev` now tracks `hf-dev/main` for real. | |
| `build-status.sh`'s REPOS list needed the dev-ref flipped from `-` to `hf-dev/main` or the fleet | |
| row kept printing `β` for a Space that exists. | |
| Two things the wiring taught, both worth keeping: | |
| 1. **`make install-hooks` COPIES.** `.git/hooks/pre-push` was still the stale pre-fix copy after | |
| the fix was committed. An edited hook is inert until reinstalled β always re-run the target. | |
| 2. **The hook fix is verified against a real push, not just synthetic stdin**: `git push hf-dev | |
| dev:main` emitted no mirror line and left `github/main` at `40ab3dd`. | |
| β οΈ The Space has **no secrets set yet** β gate dark, sink a no-op. Not a valid place to accept a | |
| gate change until that is done (TODO). | |
| ## 2026-08-12 β ADR-0009: the whoami cache's TTL and outage behaviour, decided | |
| Closed the M3 narrowed-successor TODO. `_WHOAMI_CACHE` in `gradio_ui.py` was a bare dict β | |
| resolutions trusted until restart, and an HF outage served warm tokens forever while denying cold | |
| ones, all undecided-by-default. Now (ADR-0009): entries are `(identity, monotonic-stamp)` with | |
| **positive TTL 15 min**, **negative TTL 60 s**, and **stale-if-error grace 24 h** β on a | |
| *transient* whoami failure only, a previously-positive entry is served stale so an HF blip cannot | |
| take the orchestrator down. The deciding frame: `whoami` is **attribution, not authorization** β | |
| `ALLOWED_IDS` is local, fail-closed, and re-checked per call, so the worst the cache can do is let | |
| a *revoked HF token* keep attributing calls for the window. A definitive `HfHubHTTPError` is never | |
| rescued by staleness; a cold token stays denied in an outage; transient failures are never cached. | |
| Mechanics worth remembering: | |
| - Ledger protocol followed: 0009 claimed on `main` alone (`c4f9eb5`), pushed to **`github` only** | |
| (origin push deploys prod and this session was told not to). The claim also fixed the stale | |
| "Next free number: 0008" line β 0008 was already published; next free is **0010**. | |
| - `HfHubHTTPError` cannot be constructed bare β `response=` is a required kwarg now; the test | |
| builds it around a `Mock`. Sockets are blocked in tests, so the mock seam is | |
| `huggingface_hub.HfApi` plus a monkeypatched `time.monotonic`. | |
| - 8 new tests in `tests/test_whoami_cache_policy.py`; full suite 245 green. NOT staged to the dev | |
| Space (it still has no secrets β gate dark there anyway) and NOT pushed to origin. | |
| ## 2026-08-13 β stray secrets removed from the prod Space (config only, no code) | |
| Deleted `decoupler_results_dev_token` and `decoupler_results_token` from the prod Space's | |
| secrets at Annie's request β both were added there by mistake on 2026-08-13 while fixing the | |
| *DecoupleRpy_Agent* dev Space's log-sink token (that saga lives in that repo's memory.md); | |
| nothing in this repo ever read them. Deletion restarted prod (~90 s rebuild, back RUNNING). | |
| Post-restart health check: tokenless `/panel` β `denied / no_token_header` β serving, gate | |
| fail-closed, diagnosis correct. Remaining secrets: `ADMIN_IDS`, `ALLOWED_IDS`, `HF_TOKEN`. | |
| --- | |
| ## 2026-08-13 β prod audit sink wired to a durable HF dataset (settings work, no code) | |
| The prod Space `anne-voigt/pdac-genomics-agent` had **no `LOG_SINK*` settings** (verified via HF | |
| API 2026-08-13), so `logging_sink.py` fell back to the local sink writing `./run_logs` β ephemeral, | |
| lost on every restart of a free-tier Space with no persistent storage. The default dataset the HF | |
| sink points at did not exist either. Done this session: | |
| - **Created the private dataset `anne-voigt/pdac-genomics-results`** (the code's default repo id). | |
| - **Set on the prod Space via API, as *variables*** (auditable without secret access, matching the | |
| ACCESS_CONTROL choice): `LOG_SINK=hf`, `LOG_SINK_HF_DATASET=anne-voigt/pdac-genomics-results`. | |
| The variable change restarted the Space; watched it back through `RUNNING_APP_STARTING`. | |
| - **Safe in the interim**: `HFDatasetLogSink` no-ops without a token (checked the code before | |
| flipping `LOG_SINK`), so audit writes cannot error while the secret is missing β but records are | |
| DROPPED, not written locally, until the token lands. Strictly worse than `local` for retention | |
| until Anne acts, strictly better after. | |
| **Still needs Anne (Space settings UI, secret value):** set `LOG_SINK_HF_TOKEN` (the | |
| `research_agent_token` value) on prod, then run one signed-in query and confirm a record file | |
| lands in the dataset. Note the sink also falls back to a `research_agent_token` env var if that | |
| name already exists as a secret on the Space. | |
| **Discrepancy noticed while in the settings API:** prod's *variables* are only `ACCESS_CONTROL` | |
| + the two new `LOG_SINK*` ones β `ADMIN_IDS`/`ALLOWED_IDS` are evidently secrets on prod, while | |
| the dev-Space TODO says to set them "as *variables*, matching prod". The docs disagree with the | |
| Space; reconcile when configuring the dev Space (either flip prod's to variables or fix the TODO | |
| wording). | |
| --- | |
| ## 2026-08-14 β "dead button" diagnosed; prod token secret landed on the WRONG Space; secrets decision | |
| Anne reported the prod UI's "Query variant status" button does nothing. Reproduced and root-caused | |
| by driving the public Space in a browser and speaking the gradio queue protocol directly: | |
| - **The backend is healthy.** The click DOES fire (`queue/join` β `process_completed`, | |
| `success: true` in ~1 ms) β but a signed-out session gets the ADR-0004 denial, and that payload | |
| updates ONLY the **collapsed "Raw result (JSON)" accordion** while hiding the caution box. So a | |
| signed-out (or session-expired) click is **visually indistinguishable from a dead button** β no | |
| toast, no chart, no visible change. The two prod restarts on 2026-08-13/14 dropped OAuth | |
| sessions, which is almost certainly why it "stopped working". Fix for the user: sign in again. | |
| UX finding worth a lane of its own: a denial should be visible without opening the accordion. | |
| - **The intended prod `LOG_SINK_HF_TOKEN` was set on the DEV Space instead.** Prod's `/secrets` | |
| API shows only `ADMIN_IDS`/`ALLOWED_IDS`/`HF_TOKEN`; dev gained `LOG_SINK_HF_TOKEN` 2026-08-13. | |
| Confirmed live: an authorized machine query (`x-orchestrator-token`, allow-listed caller) | |
| answered correctly (CCLE, 57 lines, n_profiled 53/44/41) and left NO record in the dataset and | |
| NO `[logging_sink] persist_trace failed` line in the run logs β the exact signature of the | |
| no-token no-op. Prod restarted 2026-08-14 to rule out stale env; still no-op. **Remaining | |
| action is Anne's alone: add `LOG_SINK_HF_TOKEN` on PROD Space settings**, then one authorized | |
| query and check `runs/` in the dataset. | |
| - **Decision (Anne): `ADMIN_IDS`/`ALLOWED_IDS` stay SECRETS.** The dev-Space TODO's "as | |
| variables, matching prod" wording was the error; docs corrected, Spaces untouched. | |
| - **The dev Space is now fully configured** (gate + sink, separate `pdac-genomics-results-dev` | |
| dataset, private, exists) β the "not a valid place to accept a gate change" caveat is lifted. | |
| Diagnostic trick worth keeping: with gradio 6 the UI click's fn is reachable without the UI β | |
| `POST /gradio_api/queue/join` with `{fn_index, trigger_id, session_hash}` + an `EventSource` on | |
| `queue/data` shows exactly what the server yields per frame; `/gradio_api/call/<api_name>` + | |
| `x-orchestrator-token` exercises the authorized machine path. Space secrets/variables are | |
| enumerable (keys only) at `/api/spaces/<id>/secrets` and `/variables` β no more guessing what a | |
| Space carries. | |
| --- | |
| ## 2026-08-14 β invisible-denial fix landed on dev (the "dead button" UX lane) | |
| The lane the same-day diagnosis called for: an ADR-0004 denial on any of the three UI tabs now | |
| renders in the **caution markdown box** ("β Access denied. β¦", visible), instead of only filling | |
| the collapsed "Raw result (JSON)" accordion while hiding the caution β which had made a | |
| signed-out/session-expired click visually indistinguishable from a dead button. New helper | |
| `_denial_caution_md` in `gradio_ui.py`; the gate itself is untouched (still first, still | |
| fail-closed). `tests/test_ui_handler_gating.py` now asserts denial visibility | |
| (`_assert_denial_visible`: caution `visible: True` + reason text) in both denial tests, so the | |
| regression can't come back silently. 237/237 tests pass. Merged to **dev only**; prod promotion | |
| is Annie's call. | |
| --- | |
| ## 2026-08-14 β invisible-denial fix PROMOTED TO PROD (`668841c..3853e93`) | |
| Annie's call, executed. Gates first: **245 tests** pass, gitleaks clean over 89 commits; | |
| pip-audit flags one advisory β `cryptography 49.0.0` PYSEC-2026-3552 (fix 50.0.0) β which is a | |
| local-venv transitive, not pinned in `requirements.txt`, so it did not block a UI-only promotion | |
| (worth revisiting if it shows up in the Space build). Promoted with `git push origin dev:main`; | |
| the pre-push hook mirrored `3853e93` to `github` first ("mirror in sync"). Build watched | |
| `RUNNING_APP_STARTING β RUNNING`, root HTTP 200. **Verified live via the queue protocol** | |
| (the 2026-08-14 diagnostic trick): a signed-out click on "Query variant status" now yields | |
| caution `{visible: true, "β Access denied. π Please sign inβ¦"}` alongside the denied JSON β | |
| the dead-button symptom is gone in production. Local `main` could not be fast-forwarded | |
| (checked out in the `peaceful-blackburn` worktree β another lane); `origin/main` is the truth. | |
| ## 2026-08-14 β `cdaniel2008` added to `ALLOWED_IDS` (prod + dev, config only) | |
| Fleet-wide widening (see pdac-subtype-agent memory.md): `ALLOWED_IDS` secret re-set on | |
| `pdac-genomics-agent` and `-dev` to the prior roster (Annie + Carl) plus `cdaniel2008`, via | |
| `HfApi.add_space_secret`. `ADMIN_IDS`/`ACCESS_CONTROL` unchanged. Spaces restarted on the write. | |
| ## 2026-08-14 (later) β dev `ALLOWED_IDS` narrowed to `anne-voigt`: lab members prod-only | |
| Policy (fleet-wide, see pdac-subtype-agent memory.md): Carl + Colin removed from the dev | |
| Space's `ALLOWED_IDS`; prod unchanged (`anne-voigt, cpelz741, cdaniel2008`). The earlier | |
| "dev mirrors prod" posture note is superseded for the roster (posture β secret vs variable β | |
| still matches prod). | |
| ## 2026-08-14 β PROMOTED TO PROD (`2e971df`): invisible-denial fix + ADR-0009 + bookkeeping | |
| Annie's call. Merged `dev` into `main` (`--no-ff`) in the worktree lane; her concurrent | |
| `dev:main` push had already landed `3853e93` on `origin/main` and the merge commit descends from | |
| it, so both pushes composed cleanly β nothing clobbered. Gates before push: **245 tests green** | |
| on the merged tree (repo `.venv`); pre-push hook mirrored `2e971df` to `github` first ("mirror in | |
| sync"). Build watched to `RUNNING` at `2e971df`, root 200. **Verified live signed-out**: the | |
| click now renders "β Access denied β¦ please sign in" in the caution box on the variant-status | |
| tab β the 2026-08-14 dead-button report can no longer reproduce. ADR-0009's claim + cache policy | |
| are now on `origin/main`, closing the "reaches origin at next promotion" note. | |
| Two restarts today dropped OAuth sessions β anyone who "was signed in" must sign in again. | |
| β οΈ **`dev` NOT realigned to `main` yet** β the main checkout has `dev` checked out with another | |
| active session's uncommitted `memory.md`; fast-forwarding under it was not safe. After that lane | |
| finishes: `git switch dev && git merge --ff-only main && git push hf-dev dev:main`. | |
| β οΈ Prod `LOG_SINK_HF_TOKEN` still missing (see 2026-08-14 entry above) β deploy doesn't change that. | |
| --- | |
| ## 2026-08-14 β PROD AUDIT SINK LIVE β (the last mile, and the trap in it) | |
| Anne set `LOG_SINK_HF_TOKEN` on prod. First verification FAILED with the exact signature of a | |
| missing secret β correct answers, no record, no `persist_trace failed` line β because **the | |
| secret-add restart wedged**: prod sat `RUNNING_APP_STARTING` 20+ minutes while the OLD | |
| container (no secret in env) kept serving. Ruled out a code/token problem by querying the DEV | |
| Space, which wrote `runs/20260814_170438_132674.json` immediately. One manual restart later prod | |
| cut over in ~60 s and the authorized query wrote `runs/20260814_170536_091195.json` β full | |
| record: `tool: variant_status:api`, `principal: anne-voigt`, `role: admin`, `enforced: true`. | |
| **Prod audit records are now durable.** Worth keeping: a wedged Space restart serves the | |
| pre-change environment indefinitely while reporting APP_STARTING β after ANY secret/variable | |
| change, confirm the stage reaches `RUNNING`, and treat "no record + no error line" as "the | |
| serving container never saw the config", not "the sink is broken". | |
| Also answered for Anne: a Space secret's NAME (`LOG_SINK_HF_TOKEN`) never appears in the HF | |
| access-token list β that list shows token names (`research_agent_token`); the secret is just an | |
| env key whose value is the token string. | |