open.gismo · design session · direction: guided setup
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.
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.
lbstresn". The gsm vocabulary is never shown to the person answering. It appears once, in the file the wizard writes at the end.LBSTNRHI". Measured: that swap fixes the one case names and arithmetic both get wrong.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 measured | Result | What 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. |
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.
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.
| Rung | Files | Cols | What turns on |
|---|---|---|---|
| 0 · Nothing runs | participants only | 15 | 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 + labs | 29 | 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 (kri0008–kri0011 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.
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.
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.
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:
| adsl | lab | shared | |
|---|---|---|---|
| SUBJID | SUBJECT | 765 | proposed |
| TRTSDT | LBDTC | 88 | decoy — two date columns |
| USUBJID | SUBJECT | 0 | what 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.
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.
Now the lab file. The product has already profiled it, so each question arrives with a proposal, three sample values, and the reason:
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.LBORNRLO / LBORNRHI, asked as one question because they are one fact.LBTEST, with its 16 distinct values listed.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.
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.
What is not working is now shown, priced in the product's own units rather than in column names:
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.
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.
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.
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.
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.
| Decision | Who | Why that side of the line |
|---|---|---|
| File encoding, delimiter, quoting, header row | product | Wrong is immediately visible — the preview is garbage. |
| Column types, date formats, numeric coercion | product | Wrong is visible as an unparseable count, which the preview shows. |
| Which file is participants, which is measurements | ask | Cheap to ask, and everything downstream branches on it. Question 1. |
| The join key between files | ask | Wrong yields zero rows, silently. Question 2, before anything runs. |
| Result / lower limit / upper limit | ask | M3. The profiler produced a perfect-scoring wrong answer on this exact triple. Questions 3–4. |
| Analyte column and visit column | ask | Questions 5–6. Cheap, and they anchor the two charts people look at first. |
| Units, visit number, study id, site, sex, race, age, arm, durations | product | Inferred and listed, never asked. Each is individually correctable, none is individually load-bearing. |
| Which of my test names is ALT / AST / bilirubin / ALP | ask | Zero matched automatically. But only asked once the hepatic chart is the thing being turned on — never up front. |
| Whether an absent domain is acceptable | ask | M4. Costs a written acknowledgement, not a click. |
| Whether an absent column is acceptable | named, not asked | Listed in the Ledger with its consequence. Blocking on each of six absences would defeat the whole direction. |
gsm.mapping::ApplySpec() — source_col is the entire column-rename seam and it already works. The wizard's output is a spec file; nothing new executes it. Verified end to end in this session.og_init() already copies 44 workflow YAMLs into the project, so the mapping specs the wizard edits are the user's own files. No new storage, no new format.config/data-config.yaml — already routes domain to file path. The wizard writes it instead of the user.safetyCharts::meta_* — nine tables that already publish a plain-English role vocabulary and ADaM/SDTM spellings per role, including analyte values for the hepatic explorer. This is the single most valuable unclaimed asset in the portfolio for this problem, and it is sitting in the ancestor package.| Piece | Size | Note |
|---|---|---|
| The profiler. Per-column type, cardinality, value-shape and date summary. | small | Prototyped and run this session; ~60 lines of R. |
| The structural ranker. Candidate triples ranked by whether each analyte carries its own range. | small | Prototyped and run; this is the piece that fixes M3. |
| The overlap sweep. Every character column against every other, plus constant-affix repair. | small | Prototyped 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. | medium | Handwritten, ~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. | medium | The site is already a SPA; this is a new route, not a new app. |
| The Ledger. Absences resolved to named consequences. | medium | Needs 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(). | small | It 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 ours | The hepatic analyte question has nowhere to write its answer. Patching a module YAML is the only option inside this program's boundary. |
Five, ordered by how much they should count against it. The first is the one I would lose the argument on.
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.
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.
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.
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.
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.
og_validate() and in the run, and it is independent of every question about surfaces.