open.gismo · data loading · direction C · 2026-08-27

Read their data. Report back.

Hand open.gismo v0.2.0 a real CRO delivery today — six files, 60,938 rows, a SAS transport and three lab vendors — and it replies with twelve lines of file not found and no mention of a single thing you gave it. This direction inverts that. The product reads whatever is in the folder, and answers with one document: here is what I found, here is what I can already build from it, here is what I cannot use and exactly what that costs you.

Direction C · read and report

The front door is not a wizard, an uploader, or a mapping grid. It is a report the product writes about your data, and every correction is made by answering a question inside it.

One command, og_read(), which never fails and always produces an artifact. Three verdicts per thing, never a pass/fail. Every gap priced in the charts it turns off. No guessing — proposals carry their evidence and wait for a human.

Why this direction and not a wizard

What open.gismo says today, verbatim

Six real files were built from demo-301’s own study data, re-shaped the way a sponsor actually receives it — an SDTM-ish demographics transport, an EDC adverse-event extract, and one liver panel split across three lab vendors. They were copied into a fresh project’s input/, which is what the README tells a user to do. This is the reply, unedited.

What was in the folderRowsColsFormatShape
dm.xpt1,00511SAS transportSDTM-ish subject file — USUBJID, SITEID, ARMCD
ae.csv2,58311CSVEDC events — coded, no toxicity grade
lab_central.csv28,60011CSVCentral chemistry — short analyte codes, ranges present
lab_haem.csv21,4506CSVHaematology — site-prefixed IDs, no reference ranges
lab_specialty.csv7,1507CSVBilirubin and GGT — SI units, own vocabulary
site_list.csv1504CSVCTMS site export
> og_validate("~/brightstar")

open.gismo validation — open.gismo Demo Study
/private/tmp/ogdesign/brightstar/study

    Raw_AE        input/Raw_AE.csv        file not found
    Raw_DATACHG   input/Raw_DATACHG.csv   file not found
    Raw_DATAENT   input/Raw_DATAENT.csv   file not found
    Raw_ENROLL    input/Raw_ENROLL.csv    file not found
    Raw_LB        input/Raw_LB.csv        file not found
    Raw_PD        input/Raw_PD.csv        file not found
    Raw_QUERY     input/Raw_QUERY.csv     file not found
    Raw_SDRGCOMP  input/Raw_SDRGCOMP.csv  file not found
    Raw_SITE      input/Raw_SITE.csv      file not found
    Raw_STUDCOMP  input/Raw_STUDCOMP.csv  file not found
    Raw_STUDY     input/Raw_STUDY.csv     file not found
    Raw_SUBJ      input/Raw_SUBJ.csv      file not found

   0 ready    12 need attention

Fix these before og_run():
  • Raw_AE — file not found
  • Raw_DATACHG — file not found …
✗ The whole argument, in one screen

Four megabytes are sitting in that directory. 1,005 participants, 2,583 adverse events, 57,200 lab results across three vendors. The report names none of them. It does not say “I see six files”; it does not say “dm.xpt looks like your subject file”; it does not say “I cannot read .xpt” — it in fact could, since haven is already an installed dependency of the stack, just never called. Twelve lines about the absence of files nobody was told to create, and silence about the files that are there.

Every other direction in this session fixes this by giving the user somewhere to type the mapping. This one fixes it by making the product look first.

The four detectors

Each was written and run against the delivery above. The numbers are measurements, not estimates. Order matters — they are listed in the order they earn their keep, which is not the order a mapping surface would run them.

1 · Inventory

Read every file in the folder, whatever the extension, and profile every column: distinct values, missing count, three examples, an inferred type. Never look for our filenames.

Measured: 6 of 6 files read, including the SAS transport. 50 columns profiled. This is the step the product does not currently take at all.

2 · Key graph

Take every column that could be an identifier and intersect its values against every candidate in every other file. Report the ones that join. Then re-test the ones that do not, after stripping a leading prefix — a zero overlap that becomes a total overlap once 0301- comes off is not a coincidence, it is a defect.

Measured: 15 candidate keys, 12 relationships found, 4 flagged as broken-by-prefix, no false positives. This runs before anything is mapped and before anything is run.

3 · Vocabulary

For every categorical column, read the distinct values and compare them against the literal strings the charts require. The hepatic explorer’s measure_values block names four analytes by full text; the outlier explorer needs grades "3" and "4" as characters.

Measured: the delivery contains 15 distinct analyte names and shares 0 of the 4 the eDISH chart asks for. Every column-name check on this file passes.

4 · Derivation

When nothing carries a required column but everything it is made of is present, propose the derivation rather than asking for the column. Days-on-study from two dates. A CTCAE grade from a result and its upper limit.

Measured: 5 such columns, unlocking 10 of the 39 charts and metrics. Each carries a stated assumption the report records — because two of these derivations are not facts.

What comes out: three verdicts, and a price on each

The unit of the report is not a column. It is a chart. This is the part no other direction gives you, and it is the part that required real work: a resolver that chains every metric and module spec back through the mapping workflows to the raw columns that feed it, so a gap can be quoted in what it costs rather than in what it is.

3
✔ Renders, complete
Everything present, nothing ambiguous, no decision needed. Two country-level enrolment metrics and the QTL report.
6
⚠ Renders, wrong
Reachable on names alone and therefore invisible to every name-based check — and each would produce output that looks fine and is not. The hepatic explorer and Hy’s Law are here.
10
◐ One answer away
Blocked only by a column nothing carries but everything is present to derive. Both AE timeline charts and the Grade 3+ Lab Abnormality Rate.
20
✖ No data at all
Seven domains are simply not in the folder — queries, data entry lag, data changes, protocol deviations, disposition, ECG, eligibility. No surface can fix this. The report says so and stops asking.

Four states, thirty-nine displays, each counted once. The second column is the point of this direction: nine displays are reachable from column names alone, and six of those nine would render wrongly — so a surface that stops at reconciling names reports nine successes and ships six silent failures. An earlier draft of this page carried the nine and the six in separate columns and did not add up; the arithmetic error is noted here rather than quietly corrected.

Counted on 2026-08-27 against demo-301/workflows/: 29 metric workflows and 10 report modules, resolved through the 15 mapping workflows to 56 distinct Raw_* columns that a chart or metric actually consumes. The mapping specs declare far more columns than that; three quarters of them feed labels and reporting, not analysis. Twelve spec entries resolve to no raw column at all because they are derived inside the mapping SQL — the report must call those unknown-provenance rather than satisfied, or it will over-promise.

The hard case, all the way through

The liver panel from three labs. This is the case that defeats every naming surface, and it takes down hep-explorer — the portfolio’s differentiator. Six steps. What the user does, what the product does, and what they see when it goes wrong.

Step 1 The user runs one command 0 questions asked

og_read("~/brightstar"). There is no upload, no dialog, no format requirement. The folder is pointed at, and the product reads. Fifteen seconds later there is a file at intake/report.html and a line in the console: six files, 60,938 rows, 50 columns. Of 39 displays: 3 ready, 6 will render wrongly, 10 need one answer, 20 need data you did not send. Open the report.

The first thing the report shows is not a problem. It is the inventory — every file, every column, three example values. Before a single complaint, the user can see that the product understood what they sent.

Step 2 The first finding is the one that would have destroyed the study cost: 4 displays

The key graph puts this at the top, above everything about column names, because it is the most expensive and the least visible.

✖ BROKEN KEY  lab_haem.csv.SUBJID does not join to anything

  1,005 subject identifiers in dm.xpt        e.g. S1000 · S1001 · S1002
  1,005 subject identifiers in lab_haem.csv  e.g. 0301-S1000 · 0301-S1001 · 0301-S1002
  overlap as delivered ....... 0.00   (0 of 1,005 match)
  overlap after removing "0301-" 1.00   (1,005 of 1,005 match)

  The mapping joins labs to participants with an inner join. As delivered,
  that join returns 0 rows. Every check on this file passes: the column is
  present, typed, complete. You would find out from an empty chart.

  → Apply "strip leading 0301-" to lab_haem.csv.SUBJID?   [ apply ]  [ not this ]

Today this is undetectable before a run, and after a run it is indistinguishable from a study with no lab data. The detector costs one pass over two columns.

Step 3 The moment of highest friction: the chart that is ready and produces nothing cost: 2 displays

The hepatic explorer clears every check a mapping surface can run. Its nine required columns all resolve. It is in the ready-now column. And it would render an empty canvas, because the chart does not ask for a column — it asks for four literal strings inside one. The same four strings are the default lMeasureValues of gsm.safety::Input_HysLaw, so the Hy’s Law Candidate metric fails in exactly the same way at exactly the same moment, and for the same invisible reason.

◐ VOCABULARY MISMATCH  hep_explorer will render 0 of 4 series

  the chart names, in workflows/4_modules/hep_explorer.yaml:
    ALT → "Alanine Aminotransferase"     AST → "Aspartate Aminotransferase"
    TB  → "Bilirubin"                    ALP → "Alkaline Phosphatase"

  your analyte columns hold 15 distinct values across 3 files:
    lab_central.csv TEST      ALB ALKP ALT AST CREAT GLUC K
    lab_specialty.csv ANALYTE TBILI GGT
    lab_haem.csv LBTEST       Hematocrit Hemoglobin Lymphocytes Neutrophils …

  matched: 0 of 4. Every column check on these files passes.
  → ALT = "ALT"?  AST = "AST"?  TB = "TBILI"?  ALP = "ALKP"?   [ confirm all 4 ]

   and the units do not agree: TBILI is umol/L, the chart's cut-points
    are multiples of the upper limit, so this one is safe. GGT is ukat/L
    with a ukat/L limit — also safe. Recorded, not corrected.

This is the highest-friction moment in the whole flow, and it is friction the user does not know they have. There is no error, no warning, no missing column. The only signal available anywhere in the product today is an eDISH plot with no points on it, and a person seeing that will reasonably conclude the tool is broken rather than that their lab vendor uses abbreviations.

Reading values instead of names is the entire reason this direction exists. Note what the report does not do: it does not auto-apply the match. TBILI to Bilirubin is a clinical assertion — total versus direct bilirubin is a real distinction — so it is proposed with the evidence attached and a human confirms it. Honest about gaps means an unconfirmed guess stays visibly unconfirmed.

Step 4 Three files, one domain required by all 6

The panel arrives split. Central chemistry, haematology, and a specialty vendor for bilirubin and GGT. Nothing in open.gismo today can express “Raw_LB is these three files” — data-config.yaml maps one domain to one path.

The report handles this as a finding, not a form. It has already noticed that three files share a column shape — an identifier, an analyte name, a numeric result, a visit — and proposes the union, showing exactly what the stacked domain would contain and which of the three contributes each required column. The reference-range columns come from two of the three files; the haematology file has none, so the union carries nulls there, and the report says which analytes will therefore have no normal range rather than discovering it later.

Step 5 Five columns nothing carries, that everything is present to derive unlocks 10 displays

This class is invisible to a mapping grid, because there is no source column to point at. The report proposes the derivation and records the assumption in the same breath.

WantedDerivation offeredCoverage measuredWhat the report warns
Raw_SUBJ.timeonstudylast dated record minus RFSTDTC765 / 765 dosed◐ assumption RFENDTC is empty for all 1,005 rows — there is no exit date in this delivery, so “last dated record” stands in for it. Median 28 days, max 119. If a participant left the study, this overstates nothing but assumes they did not.
Raw_LB.toxgrg_nsvCTCAE v5 bands from result ÷ upper limit3,575 ALT rows◐ partial works for chemistry, where ranges are present. The haematology file has no reference range at all, so those analytes get no grade and the report says which.
Raw_AE.aetoxgrfrom AESEV2,583 / 2,583✖ lossy severity is not toxicity grade. Mild/moderate/severe has three levels; CTCAE has five. The report offers it, labels it a substitution, and stamps the substitution onto every display that uses it.
Raw_AE.aest_dyAESTDTC minus RFSTDTC2,583 / 2,583✔ clean both dates present and joinable for every row.
Raw_AE.aeen_dyAEENDTC minus RFSTDTC2,583 / 2,583✔ clean ongoing events carry no end date and stay null, as they should.

The lossy one is the interesting design case. A wizard would either refuse it or silently accept it. The report does neither: it accepts it, marks it, and carries the mark forward, so that when the AE grade distribution appears on the study site it arrives with a note saying it was substituted from severity. That is the honest-about-gaps property applied past the front door.

Step 6 Twenty displays that are not coming, and the report stops asking no action available

Seven domains are absent — protocol deviations, query management, data entry lag, data change rate, study and drug completion, ECG, eligibility. Twenty of the 39 displays depend on them. No mapping, derivation or vocabulary work can conjure them.

The report’s job here is to be a shopping list, not a nag. It groups them by the extract that would supply them, prices each group in displays, and offers one action: acknowledge. Acknowledging writes the decision into the project config, so the next run does not re-raise it, and the study site prints “not supplied” in the place the metric would have appeared instead of silently omitting the tile.

Missing extractDisplaysWhat it would unlock
EDC audit trail (query, data entry, data change)8Query rate and age, data entry lag, data change rate — site and country levels
Protocol deviations4Important and total PD rates
Disposition (study and study drug completion)5Completion rates plus one quality tolerance limit
ECG2The QT explorer and the QTc metric
Eligibility criteria1The eligibility-violation tolerance limit
◐ Where the study ends up

After the three confirmations in steps 2, 3 and 5 — one prefix strip, four analyte confirmations, five derivations with two of them marked — the delivery runs. Nineteen of 39 displays render, including the full hepatic explorer with all four series and both AE timeline charts. Twenty are dark, each labelled on the site with the extract that would light it.

Total questions put to the user: three screens, ten confirmations. Total columns the user typed: zero.

Automatic versus asked

Done without asking

  • Reading every file, in any format the R stack can already open — CSV, SAS transport, SAS7BDAT, Excel, Parquet. haven and arrow are installed in this stack today and never called.
  • Profiling all 50 columns: type, cardinality, completeness, examples.
  • Building the key graph and testing the prefix hypothesis on every non-joining pair.
  • Resolving the full dependency chain from each of the 39 displays back to the raw columns it needs, so every finding has a price attached.
  • Matching column names through the alias table, and reporting the confidence with the evidence that produced it.
  • Detecting the three-files-one-domain shape and the vocabulary mismatch.
  • Writing the report, and writing nothing else. The folder is not modified until a human answers something.

Always asked

  • Every column match, however confident. A high-confidence match is pre-selected and one click confirms the batch; it is never applied silently.
  • Every value-level assertion, because these are clinical claims. TBILI is total bilirubin only if the person says so.
  • Every derivation, with its assumption stated in the question, not in a footnote.
  • Every lossy substitution, twice — once to accept it, and once more as a mark that follows it onto the site.
  • Going without a domain. This is a decision that gets recorded, not a warning that gets thrown.
✗ Naming the friction, since a design that hides it has not engaged

The moment of highest friction is not the first screen. It is step 3 — the chart that passes every check and draws nothing. It is worst precisely because the product currently gives the user no reason to look. The direction’s answer is that the vocabulary detector runs unconditionally, on every categorical column named by any chart’s settings block, and its findings are ranked by the number of displays affected rather than by how confident the matcher is. A zero-of-four match on the flagship outranks a ninety-percent-confident column rename, every time.

The second-worst moment is the one this direction is most exposed on, and it is honest to say so here rather than in the weaknesses section alone: the first report on a real study is long. Fifty columns, thirty-nine displays, twelve key relationships. See W1.

What it costs to build

Leans on, does not rebuild

New, in rough order of size

PieceSizeNotes
The cost resolver — chart and metric specs chained back to raw columns◐ mediumThe load-bearing new capability, and the one nothing else in the ecosystem has. Roughly 80 lines of graph-walking plus the two special cases below. Written and run for this design.
The report writer — HTML plus a machine-readable findings.yaml◐ mediumThe HTML is a template over the findings file. The findings file is the real interface and is what makes the report diffable.
Detectors 1–4◐ mediumAll four exist as working prototypes from this session. The key graph and the vocabulary check are each under 40 lines.
The answer writer — turning a confirmed finding into YAML in the project✔ smallFor column renames, one source_col: line. For file routing, one data-config.yaml entry. For value-level and derivation, see W2 — this is where it gets expensive.
The alias sidecar table◐ medium, and ongoinggsm.mapping has no standard-to-source dictionary. safetyGraphics has one — safetyCharts::meta_* carries standard_adam and standard_sdtm per field, which is why its detector can pre-fill. Ours has to be built and maintained. Lives in open.gismo first; upstream later.
Format readers at the door✔ smallhaven and arrow are installed but undeclared. Every read in the package today is utils::read.csv.
The og_validate() blindness fix✔ smallIt never reads source_col, so a correctly mapped project can never reach green. Worth fixing before any surface is designed, whichever direction wins.

What could go wrong

Where this direction is weakest

Five, and the first two are serious enough that a reader who wants to reject this direction should reject it on one of them.

W1 · The first report is a wall

Fifty columns, thirty-nine displays, twelve key relationships, five derivations, seven absent domains. A wizard hands you one decision at a time and gets you to a chart in four screens. This hands you a document and asks you to read it. For a person who has been given a folder and told to have something by Thursday, that is a worse first ten minutes.

Partial answer: rank findings by displays-unlocked, surface the top three, collapse everything else, and put the ready-now count in the console line so the first thing seen is a number of working charts and not a list of problems. It does not fully close the gap. A wizard genuinely beats this on time-to-first-chart, and if that is the metric that matters, this direction loses.

W2 · The answers have nowhere to go

A gsm.mapping column spec accepts exactly two keys, type and source_col. That covers renaming and nothing else. Everything this direction is proudest of detecting — the prefix strip, the analyte vocabulary, the CTCAE derivation — has no declarative home. Writing them means generating a steps: entry with literal SQL or an R call into the study’s own mapping YAML, which is a much bigger promise than “we add one line”: the product would be generating code on the user’s behalf, and that code has to be reviewable, re-generatable and not clobber hand edits.

This needs a recode: or transform: key in the gsm.mapping spec, which is outside this program’s write scope. The proposal should be drafted and handed to @jwildfire to file upstream, not filed by an agent. Until it exists, this direction can detect far more than it can fix, and a report that names problems it cannot resolve is exactly the “tells you what is wrong and gives you nowhere to fix it” failure it was built to escape.

W3 · Reading and reporting is passive when the user wants a picture

“Three of thirty-nine displays are ready” is an honest sentence and a demoralising one. The obvious fix is to run what does work and put it behind the report, so the first artifact is charts rather than prose. That is an og_run() change, and a partial run walks straight into the site-risk-score denominator problem above.

Probably the right move anyway, and it should be scoped with this direction rather than after it — but it makes the build materially larger and couples it to a defect that is not this feature’s fault.

W4 · The alias table is a dependency this direction does not control

The 66% name-match rate measured above is with a hand-written alias table of about sixty entries. Without it the rate is near zero, because the gsm raw vocabulary is neither SDTM nor ADaM — a perfectly conformant CDISC package matches on almost no column name. That table has to be built, curated, and kept current against a moving target of CRO conventions, and its quality decides whether the automatic half of this design works at all.

True of every direction in this session, so it is not a reason to prefer another one — but it is a real, unowned, ongoing cost and this design leans on it harder than a direction that just asks the user to point at columns.

W5 · The detectors cost a full read

The key graph hashes the distinct values of every candidate identifier in every file. On 60,938 rows that is instant. On a four-million-row lab file with thirty candidate columns it is not, and sampling breaks the guarantee — a sampled overlap can miss a partial key break, which is the exact failure the detector exists to catch.

Needs a stated performance envelope, which the platform gap analysis already flags as missing across the whole portfolio (row D10). Honest position: sample by default, report that the check was sampled, and offer a full pass.

Decisions this direction needs

C1

Does the front door produce an artifact or a session?

This direction says the report is a committed file in the project, versioned alongside the snapshots, that a colleague can be sent. The alternative is a live surface in the app that leaves no trace. The whole direction rests on this answer.

C2

Never auto-apply, even at high confidence?

This direction commits to never applying a match without a human answer, including obvious ones like USUBJID to subjid. That costs clicks and buys the honest-about-gaps property. The opposite position — auto-apply above a threshold and show what was applied — is faster and is what safetyGraphics does.

C3

Do lossy substitutions get carried onto the study site?

Severity standing in for toxicity grade is defensible, and a chart built on it should probably say so where it appears. That is a change to the site renderer, not the intake surface, and it is the most distinctive thing in this direction.

C4

Should a broken key refuse the run, or warn?

Today a missing domain warns and continues. A zero-overlap join is worse than a missing domain, because it looks like success. This direction says refuse, and let the user override with a recorded decision. That is a behaviour change to og_run() and is worth doing regardless of which direction wins.

C5

Who drafts the recode: proposal to gsm.mapping?

Per W2, this is the one piece reaching outside the portfolio, and this program does not write to Gilead-BioStats. The proposal should be drafted here and handed over for @jwildfire to file.

How the numbers on this page were produced

Direction C of four, for the open.gismo data-loading design session. Extends row D1 of the platform gap analysis — the data-mapping surface — and shares its brief with the four-screen fit direction and the bring-your-own-study direction, which cover the competitor landscape and the engine-seam evidence this page deliberately does not repeat.

Nothing in open.gismo, demo-301 or any upstream repository was modified. The fixture and prototypes live outside every repository, in a scratch directory.


Drafted by Claude Code using Opus 5 and reviewed by @jwildfire