Milab 6496 binding profiling impl - #12
Merged
Merged
Conversation
The block computed a dominant feature per cell — one antigen named as the winner. The verdict model asks a different question of every antigen independently, and a single winner is not a simplification of it: a cell that bound three antigens has three verdicts, not one dominant one and two absences. Removes the dominance call, the consensus and specificity outputs, and the four CLI flags that drove them. The workflow, model and UI still reference those flags and outputs; they are rewritten for verdicts in later work, and the block does not run end to end until they are.
The coverage fixture used one unjudged and one not-evaluated measurement, so swapping the two counters left every assertion passing -- the questions "was a line defensible" and "did anybody look" were silently interchangeable. The counts now differ. DEFAULT_OUTLIER_FENCE was unpinned while every fence assertion derived its expected value from it, so the constant was free to move unnoticed. It is now pinned like every other line.
…it unjudged identityDisagreement and tagDisagreement declare the against-the-run route, so the spec says they carry a status. status_for resolved a status only from the line table, and neither has a line there -- correctly, since that route carries no absolute number -- so both returned unjudged. Unjudged is the one wrong answer that hides: it never enters a rollup, so an outlying reagent would leave its whole panel reading clean, which is the failure the rollup exists to invert. status_for now refuses these outright and names outlier_status, turning a silent wrong reading into a loud one.
A spec review checked the shipped lines against the evidence file behind the spec's "lines come from four routes" decision. Three of the four did not survive it. Operator ruled on each; decisions recorded in .meta/milab-6496-decisions.md. The undeclared-barcode line of 0.1 was invented. The field publishes 0.50, and for one aggregate library fraction -- while this measurement is per sequence at tag level, where a fraction's line does not transfer. Given a count instead, any upper bound collapses into "alerting if a single undeclared barcode exists". It now ships unjudged, with its sequences and no claim about what a bad value means. panelAssignedFraction was labelled "Fraction of antigen reads usable", whose published line is 0.20, while shipping 0.50 -- the complement of the unrecognized fraction, which is what the code actually computes. The label was the part that had drifted: the shipped p-column's own description already named the recognized fraction. Relabelled, line unchanged. The id is a p-column name and is untouched. readsPerBarcode applied a per-cell recommendation to a per-observed-barcode rate. In droplet data that denominator runs one to two orders of magnitude above the called-cell count, so a healthy library would alert. Renamed readsPerCell and now divides by the cell list, which the entrypoint supplies; no p-column existed to break. at-most keeps its place in the comparison vocabulary but now has no member, so it is exercised against a registered stand-in rather than left an untested branch.
Composes the twelve modules into one CLI: the panel reader, the identity universe, the count floor, the reference and its gate, the per-cell state reader, the cell-to-set majority vote, the competitor notes, both levels of self-disagreement, and the quality measurements with their statuses and rollups. The grid of every cell against every identity is never built. A cutoff at or below specificity_score(0, 0) is refused, because below that bound the analytic silent count and the row-per-position reference part company with no error raised. One Admissibility bundle reaches read_states, combine_cells and self_disagreement, so none of them can disagree about which cells cannot be compared. What each sample was offered stays keyed by sample throughout. Barcodes outside the cell list are labelled rather than dropped, and the sparse per-tag counts plus the per-cell scalars are exported as the material a reader needs to regroup the panel without a re-run.
…ests The cutoff refusal is "at or below the analytic bound", but the test used 0.04 and 0.05 and never the bound itself, so it could not tell that from "below". Now asserted at specificity_score(0, 0) exactly. Nothing distinguished the comparator served from the one requested. A run asking for a panel comparator with too small a panel now asserts the record says none. Nothing distinguished the depth denominator. The bed now carries four observed barcodes against three listed cells, where dividing by the wrong one crosses the 5000 line: 18000/3 clears it, 18000/4 does not. The sortedness checks ran on a bed of one set and one identity, where sorted and unsorted are the same frame. A wider bed -- three identities declared in descending order across two sets -- makes the order observable. Two frames keep a redundant sort: `offered` and `tag_identity` are already built from sorted iterations, as the verdicts frame is by combine_cells. Left in place as cheap insurance against a future construction change.
Found by reviewing the entrypoint against the spec atoms rather than the plan. All three changed what a default run returns, and none was covered by a test. The comparator skipped a rung. The order is a declared reagent, else the panel's own readings where the panel carries enough members, else nothing -- and only the empty-droplet comparator is conditional on a scientist asking for it. The default resolver answered declared-or-nothing, so a twenty-antigen panel with no declared control read unreliable throughout when it could have been read against itself. A panel below the minimum still falls to no comparator, so the founding three-antigen case is unchanged. The cell list was derived from the antigen counts when neither list input arrived. Which barcodes held a cell is an input, and nothing in the antigen readings separates a cell from an empty droplet. Labelling the derivation in the run record did not make it allowed, and it was not inert: reads-per-cell divides by it, and observed barcodes outnumber cells by one to two orders of magnitude, so a healthy library read undersequenced. With no list, membership is now unknown rather than false and the measurements needing one read not evaluated. The capture rollup discarded the membership it had just computed whenever no capture map was given, which is the ordinary case. The level whose stated job is that nothing hides aggregated nothing, reporting not evaluated over runs whose samples and panels were measured perfectly well.
result_offered.csv and result_tag_identity.csv carried key columns only. A p-column is built from a CSV's value columns, so a key-only file imports as nothing -- silently, since the file exists and is well formed. What each sample was offered, and which identity a tag feeds, would never have left the block, and the tag-to-identity linker the whole cross-layer join depends on could not have been declared at all. Offered carries offered="true"; the linker carries a column named "1" holding 1, matching the cell-linker convention used elsewhere in the platform.
Where a per-tag figure alerts, the identity figures for the identities that tag feeds are now shown beside it. attach_alerting_identities already implemented this and was imported nowhere, so nothing computed which tags alert. A noisy reagent whose identities read steady is a reagent to replace, not a run to distrust, and only the two numbers together say which. Neither is suppressed: the identity rows are still emitted in full, and the attachment is a copy on the row that raised the question. A tag the grouping property could not place is now named in the run record. It keeps its own identity rather than vanishing, so a bare barcode sits among the family identities; previously the only trace was a stderr line truncated to the first eight tags. A property the panel file does not carry narrows what can be answered, and the narrowing belongs where the answers are.
Each scenario writes a counts CSV, a panel CSV and a linker CSV and runs emit_verdicts.py as a subprocess. No test constructs a per-cell state frame, which is how an earlier revision of these checks passed while the reading was turning an antigen every cell failed to bind into 'never asked'. Covers epitope loss produced from zero count rows, an unasked off-target that leaves one clonotype unsettled and a bound off-target that disqualifies another, support of 40 cells against 3 inside one clonotype's row set, and a set whose every cell the gate set aside.
Every verdict test so far writes its own three-line bed, which keeps each one readable and none of them realistic: a run whose panel is one size, whose comparator is one tag and whose cells all come from one sample cannot show what happens when four samples were stained differently. The bed at software/test-data/fixtures/verdicts/ carries those shapes at once -- four samples with panels of 3, 4, 4 and 5 tags, four barcodes recurring under two antigen names each, one antigen on two barcodes, no comparator / one / two, and a barcode declared on one sample and read on another. Three panel files share one counts file, so the same readings can be put against a comparator of 6, of 60 and of none. Every count is chosen against a named threshold and the choice is written down: 8 clears the floor of 4 and still scores 0.0001, so it is compared and fails; 500 scores 100 against a comparator of 6 and 0.1 against 60; 5000 survives either; the one comparator reading of 1 sits below the thin line of 2 and is the bed's only source of *unreliable*. Seven tests read the bed through the CLI: identity keyed by barcode where the names would split, all four states in one run, *never asked* on the short panel, both directions of the per-sample panel-versus-reads check, one antigen read by its highest member, the higher of two comparators serving, and a panel with no comparator standing in as its own. The handles they need are recovered from the files by the role each barcode plays rather than written down, so a bed regenerated under a different seed still exercises the same shapes. The generator is stdlib only and takes one fixed seed. Barcodes are random ACGT, antigens are AgNN, samples are SNN: the repository is public and nothing here comes from a real panel.
The post-fan-out stage that runs emit-verdicts and imports its thirteen outputs, plus the cleanup that made the block runnable again. The reading is answered across the whole run rather than per sample, so the per-sample tag-stat tables are gathered into one sparse counts CSV first (gather-counts.tpl.tengo), keyed with the real sampleId taken from the resource-map key. Keys come through maps.getKeys, which sorts them: a bare range over a map has no defined order, which would vary the concatenated file's handle run to run and cost every downstream node its deduplication with nothing reported. The stage is two templates. verdict-run resolves the bundle, exports the linker as flat rows and runs the exec; verdict-import takes the run record as an input and builds the specs. The comparator and the cell list that SERVED are resolved by the software, not requested — a declared comparator degrades to none where the panel carries no reference tag — and both sit in the emitted columns' domain, so specs built inside the exec's own template would have recorded the request instead. The single-cell V(D)J dataset is resolved in the body and deliberately not in prepare: prepare's bundle is awaited in full, spec and data, before the body runs, so a linker there would hold the whole mitool fan-out behind the clonotyping chain. A dataset may bring one linker per receptor; the one whose clonotype axis is the anchor's is the one the user chose, so a BCR + TCR run needs no panic. A sample-axis mismatch is named instead of joining to nothing. Without a dataset the block still runs and emits its per-cell contract columns, the per-sample QC and the per-feature properties. Threading is now assertable without a backend. verdict-args builds the whole command line, staged filenames included, and unit tests cover every flag — including --qc-summary, without which readsTotal, panelAssignedFraction and readsPerCell have no source at all and read "not evaluated" silently, taking the only sequencing-depth alert the block ships with them. fanout-inputs closes the per-sample body inputs and the resource meta to a checked list, so a verdict parameter cannot reach the fan-out and cost every user a full parse / refine-tags / tag-stat re-run. Cleanup: the consensus and specificity outputs, the four CLI flags that drove them, and the negative control that gated the specificity import are removed from main, fb-pipeline and fb-downstream. per_cell_metrics.py stopped writing either table and stopped accepting the flags, so the block could not complete a run until now. The run record gains the identity list. The pivoted per-identity summary builds one p-column per identity, and the identities are panel data unknown until the software runs; a count cannot name them, so without the list the only per-antigen state a clonotype-anchored reader can see imported as nothing.
A barcode recurring across panels is the ordinary case -- the same reagent is stained into several samples -- and it writes one QC row per panel at the same (level, entity, measurement). With the panel as a value column those rows share an axis key, which is a duplicate-key import: it does not raise, it keeps one row and loses the rest, so a reagent misbehaving in one panel and reading clean in another reports as whichever row happened to survive. Measured on the committed fixture bed: 22 of 122 QC rows collided, one identity appearing under four different panels. Sample-level and capture-level rows carry an empty panel, which is honest -- a per-sample measurement belongs to no single panel. Keying per-tag QC by (panel, tag) is what the architecture already called for.
The block test is deliberately the last task now, so the assertion that no emitted column keys on pl7.app/feature/featureId does not exist until after live verification. That axis is keyed by feature NAME; a new antigen table reusing it while carrying barcode SEQUENCES would keep the axis identity and invert its value space, so no downstream query would fail -- joins would return wrong rows. Every antigen table imported by this block goes through one helper, which makes it the single place the check can run for all of them. The legacy per-cell contract columns are built elsewhere and keep the axis legitimately. The predicate is split from the assertion so it can be tested: tengo has no try/catch, so a panicking guard cannot be exercised from a test at all. Both directions are covered -- the legacy axis is detected, the minted antigen axes pass.
BlockData and BlockArgs gain the binding reading's own parameters: the optional single-cell V(D)J dataset anchor, the role column and reference values, the reference source, the panel minimum and thin line, the count floor, the bound cutoff, minimum voting cells, minimum agreement, the admissibility gate, the high-reference line, the grouping rule and the contending groups, plus the verdict grid state. Field names are the ones verdict-args.lib.tengo already consumes. The dataset is the block's one optional input: the args lambda never throws on its absence, and the workflow skips the verdict stage alone. The dataset is offered as an ANCHOR, not a linker ref, by the same result-pool query VDJ Multiomic Integration uses; the linker itself is infrastructure and stays unpickable. Grouping is a rule over declared panel properties, never a tag->identity map. Contending groups are canonicalised in the args lambda - each group sorted, groups sorted, groups of fewer than two members dropped - so a reorder in the editor cannot stale the block. Their options come from the new identityOptions output; nothing writes that list back to data. A v2 -> v3 migration drops dominanceThreshold and the off-target designation, whose rules no longer exist, and seeds the new defaults. controlFeature, combineColumn and minUmi are KEPT: main.tpl.tengo still passes them to emit_feature_properties.py and per_cell_metrics.py, which still implement them.
The Binding verdicts page: one row per (clonotype set, antigen identity), carrying the four-state verdict, why an unsettled one is unsettled, and the cells behind it. Filtering and default visibility come from the column specs the workflow already builds — the states and the competed flag are discrete filters, and nothing in the family is orderable. Three states the page names rather than leaves the reader to infer: no V(D)J dataset (the run skipped the verdict stage entirely, so no antigen columns were produced at all), no comparator (every reading unreliable, with the setting that changes it), and a comparator request the panel could not serve. The verdicts frame is surfaced as a block output as well as an export, because a block's own exports are not in its own result pool: without it the block that produced the verdicts is the one place that cannot show them. The identity label column travels in the table's column list for the same reason, and is what puts the antigen's name in the row where the identity id is a bare barcode. The reading's settings live in one component mounted in both Settings drawers, so the rule that produced a table can be changed from the table. Picking the role or grouping column snapshots the panel's headers into data, so args can refuse a column the panel no longer carries — emit_verdicts.py ends the whole run over one, and the user meets that as a dead run. A panel swap now clears every verdict setting that names a panel column or value. Also: the sample-column dropdown no longer offers columns already bound to the barcode or feature roles, the dead v-if="false" combine-mode markup is gone (its validation alert stays, since a migrated project can still carry the value), and the two settings strings promising a specificity score are rewritten to say what the negative control actually does.
blockOutputs.antigenQcTable and blockOutputs.antigenPanelMismatchTable were computed, exported and read by nobody: the whole measurement set, its statuses, the coverage triple and the three-level rollup were invisible, and so was the panel-versus-reads check. Adds a Quality checks page carrying both, switched by a button group so each table gets the page's full height. Two model outputs resolve the frames with createPlDataTableV2 for the reason already recorded on perCellTable: V3's discovery walks the entire result pool and hangs on the upstream Samples & Data FASTQ dataset. The panel axis is a twelve-character hash of the sorted tag list, so both tables carry the panel label column from the exported verdict frame — a block's own exports are not in its own pool, so it travels in the columns list the way the identity label already does on the verdict table. The coverage triple and the deferral reason become visible by default. A status says whether what was checked is wrong and the triple says how much of the level was checked at all; a deferral whose reason is one click away is a blank cell to everyone who does not click, which reads as "checked, and fine" — the one thing the four-status vocabulary exists to prevent. Two grid states join the block data, and the v3 migration and the block test's data literal seed them alongside the verdict grid state.
The silent tally drew its numerator and denominator from two different populations. `asked` counts a member only where that member's OWN sample offered the identity; the observed count applied no such test, so a reading from a cell whose sample never offered the identity was counted against a denominator that excluded it. Reachable on ordinary data: a sample-keyed panel, a clonotype set spanning two samples, and a tag declared for one sample but read in the other -- which is the undeclared-in-panel case this block measures on purpose. Two regimes, both reproduced. Above the imbalance threshold the run dies on an assertion blaming a uniqueness violation that is not there. Below it, the verdict is silently wrong: a vote from a cell that was never asked displaces a silent cell's real one, and a tie reads as bound with full agreement. This is not new policy. Where a set sits in one sample the reading is already discarded as never-asked; the multi-sample case now behaves the same way. The same guard goes on the explicit votes in combine_cells and self_disagreement, whose counts must agree with the tally's. Also: panelId is an AXIS of the imported QC frame and was written null on every sample-level and capture-level row -- 45 of 122 on the fixture bed. A null is not a usable p-column key, as panel.py states of itself. Those rows belong to no panel and now carry an empty string, which is what the column spec already documented.
Three error messages named no control the reader could act on, and one sent them to the wrong drawer entirely: the panel minimum lives under "Advanced verdict settings", not the "Advanced Settings" accordion the message named, which holds a different parameter on another page. The four inherited QC numbers were cited as "atom 315 names them" -- a doc reference this workspace forbids in code comments, since a reader cannot follow it from here. Expanded into the four numbers and their published thresholds. _build_grouping was annotated as returning a 2-tuple and has returned a 3-tuple since the unplaceable-tag list was added. The fixture README said a comparator of 6 is "above the thin line of 2", but the test is `reference < thin_line`, so exactly 2 is comparable. Someone calibrating a new fixture at 2 would have predicted the wrong state.
Found by mutation testing the suites: 63 single-edit mutations, 48 caught. The survivors below were all the same shape -- a fixture whose values cannot tell the right behaviour from a wrong one. The parameter-threading suite set every number to the library's own default, so a builder that ignored the block's value and emitted its fallback produced an identical vector. Six threading defects survived the file whose stated purpose is to catch exactly that. The fixture now differs from every default and the values are asserted, not just the flags' presence. The silent-tally oracle drew counts from 0-30 against references of 2-20. The highest score reachable that way is about 11.8 against a cutoff of 75, so no cell in any seed was ever bound, and the assertion that a silent admissible cell is never observed as bound compared zero against zero in every run. Counts now sometimes clear the cutoff: the oracle produces 16 bound rows across the seeds that previously produced none. cellsWithSignal and cellsAboveTheLine landed on the same rows, so counting bound cells for both passed. A cell that reads but does not bind now separates them. Two stated contracts had no test at all: the outlier fence is one-sided, because a tag disagreeing less than its peers is behaving well rather than misbehaving; and three peers is enough to compare against while two is not.
gather-counts.tpl.tengo now carries totalWeight alongside the distinct-UMI count, and emit_verdicts.py calls qc_measures.usable_read_fraction over the per-sample slice of that table and the cell list it already holds. The column is read only when present, so a run built before it existed still reads a stated blank rather than crashing.
emit_verdicts.py is 2494 lines and six of the last nine tasks touched it, which cost two worktree merges and one hand-resolved conflict. The helpers move out by responsibility; main() stays whole, as its own comment requires. - frame_io.py input readers and the sorted writer - identity_tables.py the grouping rule and every table keyed by it - qc_rows.py QcRow, the measurement rows, and the two report shapes Pure move: every body line is byte-identical, verified line-by-line against the original. emit_verdicts.py drops to 1465 lines, of which main() is 1250. The dense-oracle guard now reads all four modules, since it checks source text and a helper moved out of emit_verdicts.py would otherwise escape it. The two tests that monkeypatch MEASUREMENTS now patch qc_rows, which owns that walk.
result_qc.csv gains lineWarn, lineAlert and route: a reader who sees warn can now see the number it warned against and which of the three defence routes backs it. route reads from Measurement.line alone; lineWarn/lineAlert read from the lines dict _add scored the row with, so an operator override renders here too. All three are null where no line stands behind a measurement.
runQualityTable stacks run, sample and tag rows behind one entity column that means a different thing per row. AntigenQcPage.vue now derives one PlDataTableSheet from the frame's own pl7.app/antigen/qcLevel axis at render time (PFrameImpl.listColumns + getUniqueValues), so the sheet options are whichever levels have rows in this run rather than a fixed list of three names.
The note claimed _qc_frame keeps a row for every declared measurement. It does not: it builds from the rows its call sites added. The conclusion holds for a different reason, now stated. listColumns fixes no order, so values[0] made the sheet the page opens on vary between runs.
emit_verdicts.py gains a flag per inherited-line threshold (cellBarcodeValidFraction, readsPerCell, aggregateBarcodeFraction, undeclaredBarcodeShare, usableReadFraction warn/error), each defaulting to DEFAULT_LINES, and threads the built dict through _add/_qc_frame in place of the hardcoded default. qc_report.py gains --aggregate-iqr-multiplier, --aggregate-min-umi-threshold and --aggregate-top-n, defaulting to qc_measures.py's own constants -- this is the module that actually calls detect_aggregate_barcodes; top_n is now a parameter of detect_aggregate_barcodes/aggregate_barcode_fraction rather than a fixed module constant read inside their bodies.
verdict-args.lib.tengo restates each line's shipped default (mirroring qc_measures.py DEFAULT_LINES) and emits it on every build() call, so a status is always readable against the number that produced it. The nine parameter names are added to PARAMETER_NAMES, asserted absent from the per-sample mitool fan-out by fanout-inputs.test.tengo.
IDENTITY_SUMMARY_MAX_IDENTITIES 100 -> 10000, CELL_PUNCH_MAX_CELLS 200000 -> 2000000. Both gates skip an export and leave every verdict computed, so neither changes a value. The identity gate bounds the pivot's width and the cell gate its length; the old note conflated them and claimed the page reports the skip, which no page does.
model/src/types.ts and index.ts add nine optional line-threshold fields (BlockArgs/BlockData) alongside minAgreement/gateThreshold, and three optional aggregate-barcode-detection fields alongside combineColumn/minUmi -- the latter reach qc_report.py, which runs inside the per-sample mitool fan-out, not through verdict-args. main.tpl.tengo sets the nine line params on verdictParams (verdict stage) and adds the three knobs to extraInputs only when set (per- sample fan-out, mirroring limitInput's pattern so an unset knob never stalls a body waiting for a value). fanout-inputs.lib.tengo's EXTRA_INPUT_KEYS gains the three knob names. fb-downstream.tpl.tengo passes them to qc-report's exec only when present. ui/src/pages/MainPage.vue adds two PlAccordionSections in the Settings drawer: "Quality lines" (all nine thresholds, each tooltip stating the number is inherited from the field and uncalibrated for this assay) and "Aggregate-barcode detection" (the three knobs, with an alert stating that moving any of them changes what the 0.05 line judges).
…round fits by sample 320 requires doublets, a false-discovery rate and the empty-droplet share to be stated as absent rather than left to be noticed. They were written into qc_measures.py as comments and reached no reader. A footer states them, plus saturation, which is carried apart because it exists and the three do not. Not a status and not a measurement row: a permanent absence is a property of the method and 'not evaluated' is a property of one run. The fitted-background scatter now facets by sample. The fit runs per (tag, sample) and one panel pooled every sample's fits.
The three plot tabs were pinned to 480px, so each drew in roughly half the window. PlBlockPage's body is a flex column in a minmax(0, 1fr) grid row, so flex: 1 takes whatever the tabs and alerts leave.
…t reaches stored projects :default-options seeds a plot with no saved state and never overwrites one, so a reader who opened that tab before keeps the pooled single-panel chart. The other two plot states are left alone.
The nine inherited lines and the three aggregate-detection knobs are stored undefined until someone types one, and verdict-args.lib.tengo substitutes the shipped default on the command line. The settings fields rendered that state as an empty box, so a reader could not see the number the run was scored against. The fields now display the substituted value. args() still projects undefined, so a field showing 0.75 and a field holding 0.75 produce the same command line and the same args hash, and no stored project stales. QC_LINE_DEFAULTS and AGGREGATE_DETECTION_DEFAULTS replace the same numbers previously stated only as tooltip prose. test/src/qcDefaults.test.ts asserts all twelve against verdict-args.lib.tengo and qc_measures.py, which nothing else compared.
…page Two readings of the same table were harder than they needed to be. The measurements grid keyed its rows on the qcEntity axis, which carries an opaque key per level: a sampleId on the sample rows, a barcode on the tag rows, the literal "run" on the run row. Sample rows showed the raw id. They now show the label the sample list uses, and a key with no label renders as itself, which is the path the tag and run rows take. The undeclared-barcode table showed its status as the bare words OK and warn. It now uses the same status tag, in the same colours, as the sample list. Both go through PlAgDataTableV2's cellRendererSelector, which reaches the table as defaultColDef, so each selector claims its column by p-column name and returns undefined for the rest.
PaulNewling
marked this pull request as ready for review
August 25, 2026 16:07
|
Too many files changed for review (131 files, 100 file limit). Bypass the limit by tagging |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Greptile Summary
The PR adds clonotype-set antigen-binding profiling, associated quality measurements, workflow imports, configurable comparator/grouping rules, and a punchcard UI replacing the prior verdict grids. Important touched terms:
Confidence Score: 4/5
The PR should not merge until replacing a panel can no longer leave stale punchcard selections that silently hide an otherwise valid result table.
The new punchcard filter persists across panel replacement, and the model returns no table when all persisted identities are absent from the new panel even though valid new identity options exist.
Files Needing Attention: ui/src/pages/MainPage.vue, model/src/index.ts, ui/src/pages/PunchcardPage.vue
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Feature-barcode reads] --> B[Per-cell tag counts] P[Panel declarations] --> C[Tag-to-identity grouping] B --> C D[VDJ dataset and cell linker] --> E[Clonotype-set membership] C --> F[Comparator and admissibility checks] E --> F F --> G[Per-cell binding states] G --> H[Clonotype-set voting] H --> I[Four-state verdict columns] I --> J[Punchcard UI] F --> K[Quality measurements and run metadata]Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "MILAB-6496: point the settings comments ..." | Re-trigger Greptile