The commitment
The user this design serves is a clinical data manager. They have had the study's data in their hands for eighteen months. They know that the local labs write ALT (SGPT), that site 4275 puts its number in front of the subject ID, and that nobody ever collected CTCAE grades on this protocol. They do not need a tool to discover any of that. They need a tool that will let them say it, quickly, once, in a form the machine will act on and a colleague can review.
So the bench optimises for the expert, and it accepts the consequences: it is dense, it is one screen rather than four steps, and it will not pre-fill a single guess. Its promise is not this will be easy. Its promise is a different one: nothing happens that you did not decide, and you see what each decision costs before you make it.
Everything downstream of the mapping in this portfolio is already committed to that bargain. Workflows are declarative YAML in the study's own folder. Provenance traces a number back to its snapshot and its package pins. Requirements link to published evidence. The mapping surface is the last place where the answer is trust us — and it happens to be the place where the wrong answer is least visible.
Why the loading step needs a surface at all
open.gismo v0.2.0 runs a plain project folder with no GitHub, no Actions and no server, and reads the results back as a study site. The unit is already right. The question is how a person's own data gets into one, and the honest answer today is one line of README — put files called Raw_AE.csv in input/ — plus og_validate(), which reports what is wrong and gives you nowhere to fix it.
The gap is not "rename some columns". gsm's raw vocabulary is neither SDTM nor ADaM: the findings domains are lower-case SDTM-adjacent (lbstresn, lbstnrhi, aeser), while subject, site and enrolment are operational-EDC shaped (invid, agerep, subject_nsv, mincreated_dts), and two columns the pipeline requires — toxgrg_nsv and mdrsoc_nsv on an uncoded study — have no source in either standard. A sponsor with immaculate CDISC still has to map.
The mapping surface is the front door for everybody, not a concession to messy data.
Against the surveyed field this is table stakes rather than differentiation. Row D1 of the gap analysis counted a mapping surface on at least four of thirteen platforms; the companion session's closer reading corrects the membership — tidyCDISC has none and rejects the upload outright, Medidata, Veeva and Empirica all have one and were not credited — but the count stands. What is not on any of them is a mapping surface whose output is a file in the user's own repository. That part is ours to take.
The design, in one screen
Fourteen domains, 126 columns, in a collapsible grid. Each row is one target column: its name, its declared type, whether it is a key, what it is bound to, two real values from the user's file, and what names it downstream. A drawer on the right answers what does this cost, do the keys line up, are the values recognised, and what is being written to my files.
The bench is clickable — go work it, then come back for the argument.
Four dispositions, not two
A binary mapped/unmapped grid is where these screens go wrong. It gives an expert nowhere to put what they know. Every row here is exactly one of four states, and each writes something different into the project:
| Disposition | Means | What it writes |
|---|---|---|
| Bound | This target comes from that column of that file. | source_col: on the spec entry — the key gsm.mapping already has and open.gismo never exposed. |
| Derived | It is computable from columns that are delivered. | A generated gsm.core::RunQuery step in the same mapping workflow — the same mechanism demo-301's LB.yaml already uses by hand. |
| Constant | The study has one answer and the delivery does not carry it. | A literal in the generated step, recorded as a decision rather than as data. |
| Declined | It does not exist for this study, and I know what that costs. | A commented entry carrying the person's reason, so the judgement travels with the study. |
Declining is the disposition that makes the direction work. It is what lets an expert say I know, and I am choosing, and it is what turns 126 intimidating rows into a finite piece of work. The bench prints the price — the eight workflows that stop when timeonstudy goes, the fifteen that stop when the seven undelivered domains are declined together — and then records the reason.
Where the four dispositions land
That last box is the direction's central engineering claim. gsm.mapping's spec accepts exactly two keys, type and source_col (ApplySpec.R:42), and the mapping workflow accepts arbitrary steps: running DuckDB SQL. Between them they can express everything above. The bench needs nothing from Gilead-BioStats. A design that proposes a new recode: key upstream is proposing a dependency on another organisation's release cycle for a feature that can ship this month against a seam that already exists — and the generated steps are the evidence a later upstream proposal should be built on.
What it does automatically, and what it asks
Automatic, always
- Profiles every delivered file: header, row count, distinct values, blank count and two real samples per column.
- Ranks candidate columns and prints the reason — identical name, alias list, name contains.
- Computes the downstream cost of every column and every domain from the project's own workflows.
- Measures key overlap after a run, rather than assuming it from names.
- Inventories the distinct values of any column a module filters on literal strings.
- Writes and rewrites the project's mapping YAML.
Always asks
- Which delivered file is which domain. This one it will not even propose without printing how confident it is.
- Every non-identical binding.
- Every derivation's expression.
- Every constant's value.
- Every decline's reason.
- Whether to run.
Why the file-to-domain assignment is asked and not taken
A coverage-based file proposer was built and run against the delivery on the bench page. It gets seven of fourteen right, correctly abstains on one, and is confidently, plausibly wrong on six: it proposes the liver-laboratory file for Raw_DATACHG at 75% name coverage, for Raw_DATAENT at 75%, for Raw_STUDCOMP at 50% and for Raw_SDRGCOMP at 40%, and demographics for Raw_IE at 50%. It scores that well because nine of the fourteen domains want a studyid, a subject identifier and a date, so every file matches every domain.
Those are exactly the wrong-in-the-worst-direction mistakes: a wrong file that mostly maps produces a domain full of plausible nonsense that passes every type check the product runs. The bench prints the coverage number, shows the proposal, and makes a person accept it — and the number it prints is the argument for making them.
The moment of highest friction
It is not matching columns. On the constructed delivery, with the seven files a person would actually assign, 27 of 126 targets match by identical name and another 39 have a plausible candidate. Those 66 rows take a competent person about ten minutes.
The friction is the other 60: the rows with nothing to put in them. Six sit in domains that were delivered — toxgrg_nsv, because this EDC never captured CTCAE grades; deemedimportant, because the deviation extract has no importance flag; mincreated_dts, because the CRO dropped audit timestamps. The other 54 are worse: they belong to the seven of fourteen domains that have no candidate file at all, because an EDC export simply does not contain data-change rates, data-entry lags or study-completion records.
This is where a person closes the tab. A grid of empty boxes with a red required beside each one reads as you cannot use this product, and the honest response — most of it will work fine — is invisible.
The bench's answer is Decline, priced. An empty row is not a failure state; it is one of four legitimate answers, and the expensive ones are labelled. Decline Raw_SUBJ.timeonstudy and eight workflows go. Decline the whole Raw_DATACHG domain and four go. Decline all seven undelivered domains in one action and the bench says what is left standing, computed from the project's own workflows: 26 of the 41 data-driven workflows still run — all eight safety charts including the Hepatic Safety Explorer, the Hy's Law candidate metric, the AE listing, the adverse-event, deviation, Grade 3+ lab, query-age and enrolment metrics at both site and country level, and both reporting workflows. Fifteen stop: study completion, study-drug completion, data change, data entry, both quality tolerance limits, the ECG chart and the QTL report.
That is a completely different message from 67 columns missing, and it is the same fact.
There is one consequence the screen can warn about and cannot prevent, and it is worth naming because it is the portfolio's known silent-failure mode arriving through this door. Five workflows name no raw column at all — srs0001 (the Site Risk Score), the Bounds and Metrics reporting steps, and the two KRI report modules. srs0001 stacks every kri* that produced results and re-normalises against their weights, so declining a domain does not make it fail; it moves its denominator. The bench says so in the cost panel. Fixing it belongs upstream.
What it costs to build
| Piece | Leans on | Size | Note |
|---|---|---|---|
1. Teach og_validate() to read source_col |
og_validate.R:201 |
1 day | Prerequisite for everything. Today the readiness check and the engine disagree about the same project — executed proof. |
2. Key-overlap check inside og_run() |
DuckDB; the joins the mapping YAML already declares | 3 days | ✔ Ship this first, alone Highest value per day on the list, and it helps every user immediately, with or without a bench. |
| 3. File profiler | og_validate.R's existing sampling |
2 days | Header, rows, distinct count, blank count, two samples. Must stay cheap on a 171k-row query extract. |
| 4. The grid, four dispositions, the drawer | the existing SPA | ~2 weeks | The bulk. The mockup is a working sketch of the whole thing. |
| 5. The spec writer | yaml, and a managed-region marker |
~1 week | The real engineering. R's yaml does not round-trip comments, so the bench owns a marked region and never touches anything outside it. |
6. Value inventory and the CASE WHEN generator |
module lSettings.measure_values |
~1 week | Reads the targets out of the module YAML rather than hard-coding them. |
| 7. The alias table | — | 3 days | Optional The one asset that does not exist. safetyGraphics has safetyCharts::meta_* with standard_adam / standard_sdtm per field, which is what lets detectStandard() pre-fill. gsm.mapping has no equivalent. |
Total for a usable first version: items 1, 2, 3 and 4, or roughly three weeks. Nothing on the list requires a change to gsm.mapping, gsm.core or any Gilead-BioStats repository.
What could go wrong
- The bench clobbers a hand edit. Generated SQL sits in a file the user is invited to edit. The managed-region marker bounds the damage but does not eliminate it, and one lost hand-written join destroys the trust the whole direction runs on.
- The overlap check is a join per key pair per run. Bounded and cheap on demo-301; unmeasured on a 500,000-row study with fourteen domains. It needs a row cap and a sampling story before it ships.
- A spec using the
_allwildcard has no enumerable target list, so the grid has nothing to draw. No mapping workflow uses it today;Bounds.yamldoes, and nothing stops a mapping from adopting it. - Adding a domain is still not the bench's problem. A study needing a domain gsm.mapping does not ship must file an issue upstream and wait. The bench cannot help with that and should not pretend to.
- CSV is the only format the package reads anywhere — no
haven, noarrow, noDBI. A bench over a folder of transports needs a reader before it needs a grid.
Where this direction is weakest
Six objections, in the order I would attack it.
- It is the first thing in open.gismo that genuinely needs an app. The product's whole pitch is a plain project folder that runs anywhere — no GitHub, no Actions, no server. A folder-and-YAML direction stays true to that; this one does not. If the answer to how do I load my data is launch the app and work a grid, the product has quietly become a different product. This is the sharpest strategic objection and it does not have a comfortable answer.
- It gives a novice nothing. The bench does not degrade; it just presents 126 rows. A medical monitor who wants to look at one study's charts this afternoon is worse off here than with a guided four-step wizard that gets 70% right and asks about the rest. If the real first user is not a data manager, this direction is simply the wrong one.
- No reuse. A per-study worksheet produces a per-study artefact. Importing a previous project's mapping is bolted on, and the second study of the same EDC costs nearly as much as the first until an alias corpus exists — which is exactly the spine of an adapter-library direction.
- Structural mismatch is unrepresentable. Long-versus-wide labs, one file feeding two domains, two files that must be unioned into one, a domain that needs a pivot: a column grid cannot express any of them. My answer is hand-edit the SQL in the same file, which degrades the surface to a text editor at precisely the hardest moment. Roughly one delivery in three will hit this.
- It generates code the user is accountable for. A derived CTCAE grade is a clinical decision expressed as SQL. The bench records who wrote it; it cannot judge it. And a data manager who does not read SQL is blocked inside a surface sold on control — the Derive tab is where this direction's promise runs out.
- The suggester starts empty. Day one on a study with no prior is 126 manual bindings. The alias table on the mockup and its delivery were written by the same person, so the hit rate shown there is an upper bound, not a measurement.
The strongest defence. Four of the six are arguments for a different first user, not against the bench existing. Whoever wins this round, the study that arrives as three pooled laboratory files with site-prefixed identifiers and no reference ranges still needs a place for a human being to say so. Every direction eventually grows a bench; the question is whether it is the front door or the escape hatch.
Decisions
| # | Question | Recommendation |
|---|---|---|
| D-B1 | Does the bench live inside the running app, or as a standalone page over a project folder that needs no server? | Standalone page first. It keeps the no-server promise intact and it is how the mockup already works. |
| D-B2 | Does the bench write into the project's existing mapping YAML, or own a separate file? | Into the existing file, inside a marked managed region. The whole differentiator is that the output is a reviewable diff in the user's repository. |
| D-B3 | Is the key-overlap check a bench feature, or a check inside og_run() for everybody? | Inside og_run(), and shipped first, alone. It is three days and it is the single highest-value item on the whole list. |
| D-B4 | Must a decline carry a reason before it is accepted? | Yes. A reasonless decline is a blank with extra steps, and the reason is the artefact that survives the session. |
| D-B5 | Value mapping: generate SQL steps, or propose a recode: key to gsm.mapping? | Generate. Ship this month against a seam that exists; propose upstream later, evidenced by what studies actually generated. |
| D-B6 | What formats does the door accept? | CSV plus .xpt via haven. One dependency, and transports are half of real deliveries. |
| D-B7 | Does the bench learn — remembering accepted bindings across projects on the same machine? | Not in v1. It is the obvious second feature and the obvious first way to leak one sponsor's column names into another's project. |
Boundaries respected
- Nothing was built. No change to open.gismo, demo-301, gsm.mapping or any other repository's source. No issue, no branch, no pull request.
- No writes outside
jwildfire. gsm.mapping and gsm.core are Gilead-BioStats repositories; they were read, and the D-B5 recommendation is deliberately one that needs nothing from them. - Read-only competitor context. The landscape work is the companion session's; this page cites it rather than repeating it, and the one correction it carries is theirs.
- The delivery is constructed and labelled as such on every page it appears. Everything on the contract side — domains, columns, types, consumer counts, the four silences, the
og_validate()behaviour — was read from source or executed.
Known limitations
- One study. The 14 domains and 126 columns are demo-301's. A study drawing on gsm.mapping's wider set — VISIT, Death, Randomization, OverallResponse, PK, Consents, AntiCancer — has more, and the grid gets longer, which sharpens objection 2.
- The build estimates are one person's judgement and have no spike behind them. Item 5, the spec writer, is the one most likely to be wrong, and it is wrong in the expensive direction.
- The bench's cost model resolves a workflow's columns back to the raw layer through the participant join. Columns arriving on
Mapped_AEandMapped_LBfromMapped_SUBJare attributed toRaw_SUBJ. That is right for demo-301's mappings and would need re-deriving for a study that joins differently. - The eDISH reach figures come from the constructed delivery, executed. They are a real measurement of an invented study, which is the strongest claim available without a real sponsor's data.
Drafted by Claude Code using Opus 5 for review by @jwildfire · one of four competing directions on the same brief. Companions: the landscape and four-screen session and the evidence session, both 2026-08-27.