open.gismo · data-loading design · page 1 of 3

The Mapping Bench

One direction of four for getting a real study into open.gismo, and it is the unfashionable one: put every column the pipeline needs beside every column the study delivered, on one dense screen, and let a person who knows their own data connect them. No wizard. No guessing. Every decision priced while it is being made.

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:

DispositionMeansWhat 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

How each disposition reaches the engine Bound writes a source_col key into the spec block. Derived and Constant write a generated query step. Declined writes a commented entry with a reason. All four land in the project's own mapping YAML, which gsm.mapping's ApplySpec and the study's own steps then execute. No change is required to any package. THE BENCH Bound Derived Constant Declined workflows/1_mappings/LB.yaml spec: … source_col: LBSTRESN the key that already exists steps: gsm.core::RunQuery generated SQL, in your file # toxgrg_nsv: DECLINED — … the reason, kept UNCHANGED gsm.mapping::ApplySpec gsm.core::RunQuery workr no upstream change needed

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

PieceLeans onSizeNote
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

Where this direction is weakest

Six objections, in the order I would attack it.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

#QuestionRecommendation
D-B1Does 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-B2Does 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-B3Is 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-B4Must 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-B5Value 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-B6What formats does the door accept?CSV plus .xpt via haven. One dependency, and transports are half of real deliveries.
D-B7Does 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

Known limitations

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.