open.gismo · design session · direction: guided setup

Six questions, asked over your own data

A person with a CRO delivery and no idea what an analysis domain is should reach a real chart from their own study in six answers. This is the design for that path — and the four measurements that forced it into an uncomfortable shape.

01The claim

The data-loading problem is not a mapping problem. It is a vocabulary problem: everything a person knows about their study is expressed in values — site codes, analyte names, units, visit labels — and everything the pipeline needs is expressed in schema. Guided setup is the direction that refuses to translate upward into schema and instead brings every question down into values. It asks six things, all of them answerable by looking at your own rows, and infers the rest.

That is the easy half to say. The hard half is that I built the inference engine, ran it, and it produced a confidently wrong answer on the study that ships with the product — with a better confidence score than the right answer on the study it got right. So this design cannot be "detect and pre-fill", which is what the state of the art does. It has to be something narrower and more disciplined.

C1
Questions name roles, never columns
"Which column holds the result of the lab test?" — never "map lbstresn". The gsm vocabulary is never shown to the person answering. It appears once, in the file the wizard writes at the end.
C2
Inference orders candidates; it never fills a blank
Every question is pre-selected but not pre-answered, and the page shows the runner-up that lost. No confidence percentage is ever displayed, because I measured it going the wrong way.
C3
Structure beats arithmetic, and both beat names
The test that ranks candidates is "does every analyte carry its own reference range", not "does the value fall inside the range" and not "is this column called LBSTNRHI". Measured: that swap fixes the one case names and arithmetic both get wrong.
C4
A staircase, not a form
The wizard never shows 126 columns. Two files and 29 columns reach six of the twelve report modules. It stops at the first real chart and offers the next rung with a named prize.
C5
The join is a question, asked before anything runs
The published detector's pick joins 57,200 lab rows to zero. The overlap sweep finds the right key in one pass. Nobody should discover this from an empty chart.
C6
Skipping is priced, in the product's own units
A half-mapped study does not produce half a risk score. It produces a doubled one. Declining a domain therefore costs a sentence of acknowledgement written into the project, not a click.

02The four measurements

Each of these was executed against real files, not reasoned about. Method, code and full output are on the evidence page; the summary is here because the design is downstream of all four.

What was measuredResultWhat it forced
M1 · The published name detector. safetyGraphics::detectStandard() against a CRO-shaped central-lab extract and an ADSL, using safetyCharts::meta_labs / meta_dm. 3 of 9 lab roles found (33%), and it announced "SDTM" anyway. 5 of 7 on ADSL (71%). 0 of 4 hepatic analyte values matched. Detection cannot carry a guided path on the file that matters most. And a partial detection that names a standard is worse than none, because it invites a confirm-and-continue.
M2 · A name-blind value profiler written for this session and pointed at the same lab extract. 9 of 9 roles correct, including all four the name detector missed — result, both reference limits, units. The evidence a guided setup needs is in the values, not the headers. This is the direction's engine.
M3 · The same profiler on demo-301's own Raw_LB.csv, the study open.gismo ships. Confidently wrong. It chose visnum as the lab result, toxgrg_nsv as the lower limit and lb_dy as the upper — scoring 1.000, above the 0.849 it scored on the answer it got right. C2 and C3. No auto-fill, no percentage on screen, and a different ranking test. Re-ranking on structure puts the correct triple first on both files and demotes this one to second with a score of zero.
M4 · The price of a partial map. gsm.kri::CalculateRiskScore() run twice on identical site data, once with twelve KRIs mapped and once with six. Same site, same single red flag: Site Risk Score 8.3 at twelve, 16.7 at six. Exactly double. No warning is emitted at any level. C6. Skipping a domain is not a smaller answer, it is a different one, and the surface that lets you skip has to say so.
The finding that shaped everything else

M3 is the reason this design is not the obvious one. The obvious guided setup detects, pre-fills, and asks you to confirm — and a person who has never seen gsm has no basis on which to refuse a pre-fill. On demo-301 they would have confirmed visnum as the lab result and drawn a histogram of visit numbers. It would have looked completely normal.

03The staircase

Computed by walking every workflow in demo-301/workflows/ and resolving each metric and module through its Mapped_* dependencies back to raw domains. The shape of that graph is the shape of the interview.

RungFilesColsWhat turns on
0 · Nothing runs participants only15 Not one metric and not one chart. Raw_SUBJ alone feeds Mapped_SUBJ, Mapped_COUNTRY and Mapped_STUDY and stops. This is why "upload your demographics" is a bad first screen.
1 · First light participants + labs29 6 of 12 report modules — histogram, shift plot, results over time, delta-delta, outlier explorer, hepatic explorer — plus kri0005, cou0005, saf0001. Half the visible product from two files.
2 · Events + adverse events+16 AE explorer, AE timelines, and five metrics (kri0001, kri0002, cou0001, cou0002, saf0003).
3 · Site oversight + deviations, disposition+12 Seven more metrics — kri0003, kri0004, kri0006 and their country twins, plus qtl0002. The site risk score starts to mean something, because its denominator stops being a rump.
4 · Data quality + queries, entry lag, changes+13 Eight operational metrics (kri0008kri0011 and their country twins). These are the domains a sponsor rarely has outside a CTMS, and the ones a guided setup should be least insistent about.

Rung 1 is the design's whole bet. The interview's success condition is not a complete mapping — it is one chart, drawn from the user's own rows, inside six answers. Everything after that is an offer, not a requirement, and every offer names its prize.

04The hard case, walked all the way

A CRO ships two files: an ADaM-shaped adsl.csv (1,005 rows, 16 columns) and a central-lab extract lab_central.csv (57,200 rows, 11 columns). No define.xml. Nothing is named the way gsm names it, the two files disagree about what a subject identifier is, three columns the pipeline wants do not exist, and the analytes are spelled by three different vendors. Every number below is measured. The clickable version is here.

  1. Drop the folder. The product reads, it does not ask.

    Two files, 1,005 and 57,200 rows. The product profiles every column — type, cardinality, value shape, date-parseability — and says what it thinks each file is: "one row per person" and "many rows per person, with a measurement." It asks the user to confirm the kind of file, not the schema. That is question 1 and it is answerable by anybody.

  2. The join, before anything else.

    The product sweeps every character column in file A against every character column in file B and ranks by shared distinct values. The result, run:

    adsllabshared
    SUBJIDSUBJECT765proposed
    TRTSDTLBDTC88decoy — two date columns
    USUBJIDSUBJECT0what the name detector picks

    Question 2 is: "These two files agree on 765 people using SUBJID and SUBJECT. Is that the right pairing?" — with the zero-overlap runner-up shown underneath, so the person can see the alternative was tested and lost. If the user picks USUBJID anyway, the product refuses and offers the repair it found: a single constant prefix, AA-AA-000-0000, whose removal takes the overlap from 0 to 765.

    Why this is question two and not question ten

    LB.yaml and AE.yaml both end in an inner join against the participant table. Getting this wrong does not produce an error. It produces zero rows out of 57,200, and six charts quietly stop existing. Today's og_validate() reads one file at a time and cannot see it.

  3. Four questions over real values.

    Now the lab file. The product has already profiled it, so each question arrives with a proposal, three sample values, and the reason:

    • Which column is the result?LBORRES. Reason shown: every one of the 16 test names carries its own pair of limits, and this is the number that sits between them.
    • Which columns are the normal range?LBORNRLO / LBORNRHI, asked as one question because they are one fact.
    • Which column names the test?LBTEST, with its 16 distinct values listed.
    • Which column is the visit?VISIT, with Baseline, Week 1, … shown.

    Six answers total, counting the file kinds and the join. Units, dates, visit number, study id, site, sex, race, age, arm and treatment duration are all inferred and listed in a collapsed "also matched" block, never asked about.

  4. First light — the wizard's success condition.

    The product writes the two mapping specs, runs them, and draws a histogram of the user's own ALT values. This step was executed for real: the emitted spec fed through gsm.mapping::ApplySpec() and the study's inner join yields 57,200 rows over 765 participants, with lbstresn numeric and lb_dt a Date.

    The interview should end here. Not because the mapping is complete — it is not — but because this is the moment the person acquires the only thing that lets them judge everything after it: a picture of their own study.

  5. The Ledger, and the moment of highest friction.

    What is not working is now shown, priced in the product's own units rather than in column names:

    Six safety charts are drawing, over 765 peopleworking
    Nothing here looks like a toxicity gradeGrade 3+ Lab KRI off
    Nothing here marks which visit is baselineshift plot uses visit 1
    The hepatic explorer cannot find its four analytes1 chart off
    Site Risk Score is computed over 1 of 12 KRIsnot comparable

    The fourth and fifth rows are where this direction is under the most strain, and they are different problems. The hepatic one is answerable — keep reading. The risk score one is not answerable by any question, because the only fix is more data.

    The absent column is where a guided setup is structurally weak

    A wizard is excellent at "which of these?" and has nothing to offer for "you need a thing you do not have." There is no candidate to click. The best this direction can do is name the absence in the user's language, name the consequence in the product's, and refuse to let it pass silently — which is strictly better than today, where the column is dropped by a purrr::keep filter with no message at any log level, and strictly worse than a direction that could offer to derive the grade from the result and the limits.

  6. The one question that is about values, not columns.

    The hepatic explorer needs four analytes by name. Its module YAML carries them as literals: ALT: Alanine Aminotransferase and three more. The user's file spells them ALT, SGOT, TBILI, Alk Phos. Matched: zero of four.

    Guided setup handles this well as an interaction — a person who has never seen gsm can absolutely answer "which of your 16 test names is the liver enzyme ALT?" from a dropdown of their own values, and safetyCharts::meta_hepExplorer already publishes both the ADaM and SDTM spellings to seed the guesses. It handles it badly as an artefact: there is nowhere declarative to write the answer. The spec accepts exactly two keys, type and source_col, and neither expresses a value. The wizard has to patch a module YAML.

  7. Emit, and get out of the way.

    Three files are written into the project folder: config/data-config.yaml pointing at the two CSVs where they already sit, workflows/1_mappings/SUBJ.yaml and LB.yaml carrying source_col: lines, and a new config/setup-log.md recording each question, the answer, the evidence the product offered, and the acknowledged absences in the user's own words.

    The interview is over and does not run again. Which is the beginning of this direction's real problem — see §07.

05What it does silently, and what it asks

The line is drawn by one rule: the product decides anything a wrong answer would make obvious, and asks about anything a wrong answer would hide. A mis-detected unit column is visible the moment a chart draws. A mis-detected join key is invisible forever.

DecisionWhoWhy that side of the line
File encoding, delimiter, quoting, header rowproductWrong is immediately visible — the preview is garbage.
Column types, date formats, numeric coercionproductWrong is visible as an unparseable count, which the preview shows.
Which file is participants, which is measurementsaskCheap to ask, and everything downstream branches on it. Question 1.
The join key between filesaskWrong yields zero rows, silently. Question 2, before anything runs.
Result / lower limit / upper limitaskM3. The profiler produced a perfect-scoring wrong answer on this exact triple. Questions 3–4.
Analyte column and visit columnaskQuestions 5–6. Cheap, and they anchor the two charts people look at first.
Units, visit number, study id, site, sex, race, age, arm, durationsproductInferred and listed, never asked. Each is individually correctable, none is individually load-bearing.
Which of my test names is ALT / AST / bilirubin / ALPaskZero matched automatically. But only asked once the hepatic chart is the thing being turned on — never up front.
Whether an absent domain is acceptableaskM4. Costs a written acknowledgement, not a click.
Whether an absent column is acceptablenamed, not askedListed in the Ledger with its consequence. Blocking on each of six absences would defeat the whole direction.

06What it costs to build

Leans on, unchanged

Has to be built

PieceSizeNote
The profiler. Per-column type, cardinality, value-shape and date summary.smallPrototyped and run this session; ~60 lines of R.
The structural ranker. Candidate triples ranked by whether each analyte carries its own range.smallPrototyped and run; this is the piece that fixes M3.
The overlap sweep. Every character column against every other, plus constant-affix repair.smallPrototyped and run. Should ship regardless of which direction wins — see the recommendation below.
The role table for gsm. A sidecar mapping each gsm column to a role, a plain-English question and candidate spellings.mediumHandwritten, ~90 rows for the four rung-1 and rung-2 domains. Lives in open.gismo first; upstreaming to gsm.mapping is a separate conversation with a repo this program cannot write to.
The interview UI. Six screens, each a question over a live preview.mediumThe site is already a SPA; this is a new route, not a new app.
The Ledger. Absences resolved to named consequences.mediumNeeds the workflow dependency graph computed at run time. The graph is already in the project's own YAMLs; nothing reads it yet.
A fix to og_validate().smallIt derives required columns from the spec's target names and never reads source_col, so a correctly-mapped project can never reach green. Worth doing before any screen is designed.
A value-mapping seam.large / not oursThe hepatic analyte question has nowhere to write its answer. Patching a module YAML is the only option inside this program's boundary.

What could go wrong

07Where this direction is weakest

Five, ordered by how much they should count against it. The first is the one I would lose the argument on.

W1 — It optimises the first hour and is dead weight on the fortieth refresh

Study data arrives repeatedly. A wizard is a one-time interview, so by design it runs once and then leaves behind an artefact in a vocabulary the person never learned. Visit two, they open LB.yaml to fix a column and are reading lbstnrhi and toxgrg_nsv — names the interview worked hard to never show them. The direction's core commitment, C1, is precisely what makes its output illegible to the person who produced it.

The honest mitigations are both partial: the setup-log.md keeps the plain English next to the decisions, and the wizard can re-enter on a diff ("this file gained a column since you set it up"). Neither closes the gap. A direction built around the file as the primary surface rather than the interview does not have this problem at all.

W2 — It cannot ask about what is not there

Three of the fourteen columns the two rung-1 domains want — the toxicity grade, the baseline flag, the study day — simply do not exist in the delivery. A question needs candidates and there are none. This is the highest-friction moment in the whole flow and the direction's answer is only to name it well, which is a reporting improvement rather than a solution. Two of the three are derivable from what is present, and a derivation surface is a different design.

W3 — The value-level question has no home

The interaction is the easy part; the persistence is impossible inside the spec. Every value-level fact — which string means ALT, which visit is baseline, whether the grade is "3" or 3, what the units are in — has to be written somewhere that is not the mapping spec, because the mapping spec accepts two keys and neither of them is about values. Whatever the wizard writes is a private convention until gsm.mapping grows a seam, and that is a repo outside this program's write boundary.

W4 — It taxes the expert to serve the novice, and hedging destroys it

Someone who knows gsm wants to paste a YAML and be done. Every question is friction for them. The obvious fix — an expert mode — means building two surfaces and maintaining agreement between them, and the moment expert mode exists it becomes the one the docs show, because it is the one that fits in a code block. This direction only pays off if the novice path is the only path for a while, which is a real cost to accept deliberately rather than a detail.

W5 — Inference quality is the whole product, and I measured it as unstable

9 of 9 on one file, confidently wrong on the next, with the confidence score pointing the wrong way. The structural ranker fixes the case I found; I have no basis for claiming it fixes the ones I did not. A direction whose value proposition is "the product does the inferring" is exposed in a way that a direction built on explicit declaration is not, and the failure mode is silent-wrong rather than loud-broken — the house failure mode this program already knows it has.

08What I would ask @jwildfire