diff --git a/jenner-check/README.md b/jenner-check/README.md new file mode 100644 index 0000000..b3ca73b --- /dev/null +++ b/jenner-check/README.md @@ -0,0 +1,66 @@ +# Jenner compatibility tests + +[Jenner](https://jenneranalytics.com) is a complete SAS-compatible system +and collaborative workspace. Each `tNNN_*` directory in this folder is a +self-contained test bundle that submits a SAS program to the public API +at `https://api.jenneranalytics.com/v1/run` and checks the response. + +## Bundle layout + +``` +tNNN_*/ +├── script.sas # the SAS program +├── autoexec.sas # options + setup that prepend the script +├── input/ # sample data the script reads (if any) +├── expected.json # stable assertions checked on each run +├── expected/ # captured snapshot from the last passing run +│ ├── log.txt # the .log field, verbatim +│ ├── output.txt # the .output (listing) field, verbatim +│ └── files.md # links to ODS images, datasets, etc. +└── meta.json # provenance: source file, blob sha, what was adapted +``` + +## Running a bundle + +The runner concatenates `autoexec.sas` + `script.sas`, POSTs to +`https://api.jenneranalytics.com/v1/run`, and prints the result. + +**Mac / Linux (bash + curl):** + +```bash +./run_jenner.sh --all # run every tNNN_* bundle, summary at end +./run_jenner.sh t001_something # run one +./run_jenner.sh --list # list bundles in this directory +``` + +**Windows:** + +```cmd +run_jenner.bat tNNN_something +``` + +**From any SAS session (no curl needed):** + +Submit `run_jenner.sas` — it uses PROC HTTP to POST and prints the +response. + +**By hand with curl:** + +```bash +cat tNNN_*/autoexec.sas tNNN_*/script.sas > /tmp/submit.sas +curl -sS -X POST https://api.jenneranalytics.com/v1/run \ + -F "script=@/tmp/submit.sas" \ + -F "deterministic=1" -F "timeout=60" +``` + +**Or in the hosted workspace:** + +Open , paste `script.sas` (with the +`autoexec.sas` lines prepended), upload anything in `input/`, and run. + +## Artifact URLs + +`expected/files.md` in each bundle lists hosted URLs for any ODS images, +datasets, or other artifacts produced by a captured run. Those URLs are +tied to a specific run and expire when the run is reaped — re-run the +bundle to refresh them. diff --git a/jenner-check/run_jenner.bat b/jenner-check/run_jenner.bat new file mode 100644 index 0000000..1039fdf --- /dev/null +++ b/jenner-check/run_jenner.bat @@ -0,0 +1,43 @@ +@echo off +rem run_jenner.bat - Windows runner for Jenner compatibility checks. +rem +rem Usage: run_jenner.bat [response.json] +rem +rem Submits a single .sas file to api.jenneranalytics.com. For +rem bundle-aware mode (autoexec.sas + script.sas concatenation) on +rem Windows, use WSL and invoke run_jenner.sh instead, or wait for the +rem Windows CI runner that will validate a bundle-aware .bat. +rem +rem Output: response.json contains the API response. Read it back in SAS: +rem filename resp 'response.json'; +rem libname resp JSON fileref=resp; +rem proc print data=resp.root; run; +rem +rem Requires: curl.exe (ships with Windows 10+ at C:\Windows\System32). + +setlocal + +if "%~1"=="" ( + echo Usage: %~nx0 ^ [response.json] + exit /b 2 +) + +set SCRIPT=%~1 +set OUT=%~2 +if "%OUT%"=="" set OUT=response.json + +set HOST=api.jenneranalytics.com + +curl.exe -sS -X POST "https://%HOST%/v1/run" ^ + -F "script=@%SCRIPT%;type=application/x-sas" ^ + -F "deterministic=1" ^ + -F "timeout=60" ^ + -o "%OUT%" + +if errorlevel 1 ( + echo curl failed with errorlevel %errorlevel% + exit /b 1 +) + +echo Response written to %OUT% +exit /b 0 diff --git a/jenner-check/run_jenner.sas b/jenner-check/run_jenner.sas new file mode 100644 index 0000000..550e8f8 --- /dev/null +++ b/jenner-check/run_jenner.sas @@ -0,0 +1,526 @@ +/* run_jenner.sas — invoke api.jenneranalytics.com from base SAS. + * + * Requires SAS 9.4 M5 or later (PROC HTTP + libname JSON engine). + * + * --------------------------------------------------------------------------- + * TL;DR for SAS users: + * + * %include 'run_jenner.sas'; + * %jenner_run(script=my_program.sas); / * one script * / + * %jenner_check_all(); / * whole bundle dir * / + * + * --------------------------------------------------------------------------- + * What this file gives you: + * + * %jenner_run — POST one .sas file to the Jenner API, display the + * log + listing + any generated files. + * %jenner_check_all — walk every jenner-check/tNNN_* bundle, + * invoke the API for each, compare the response to + * the bundle's expected.json, produce a summary + * CSV + SAS dataset the repo owner can attach to the + * jenner-check PR. + * + * --------------------------------------------------------------------------- + * How the API call is built: + * + * POST https://api.jenneranalytics.com/v1/run + * Content-Type: multipart/form-data; boundary=... + * + * fields: + * script the .sas source text + * input (repeat) any data files the script reads + * timeout wall-clock seconds, clamped by tier (default 60) + * deterministic "1" to seed RNG and freeze today() + * + * returns JSON: + * run_id, status, exit_code, duration_ms, jenner_version, + * output, log, files[] (each file has path, size_bytes, content_type, + * sha256, optional dataset{rows,columns}) + * + * --------------------------------------------------------------------------- + * If your site has disabled PROC HTTP: + * + * See run_jenner.bat (Windows) or run_jenner.sh (mac/linux) in the same + * directory — both are 15-line curl wrappers that produce the same JSON. + * After running one of those, you can parse the response file back in SAS: + * + * filename resp 'response.json'; + * libname resp JSON fileref=resp; + * proc print data=resp.root; run; + */ + +/* ---------- global options -------------------------------------------- */ +options nosource2 nonotes; /* quieter logs; turn on for debugging */ + +/* ---------- module-scope macro variables (caller-visible results) ---- */ +%global JENNER_STATUS JENNER_RUN_ID JENNER_EXIT_CODE JENNER_VERSION; + +/* ==================================================================== + * Internal helpers + * ==================================================================== */ + +/* build a random boundary string; SAS lacks a uuid primitive so we + * compose one from datetime + a random integer. */ +%macro _jc_boundary; + jc_%sysfunc(compress(%sysfunc(datetime(), b8601dt.), -:.))_%sysfunc(ranuni(0),hex6.) +%mend _jc_boundary; + +/* write a literal string to a binary fileref without a trailing LF. */ +%macro _jc_put(fref, text); + data _null_; + file &fref mod recfm=n; + put &text; + run; +%mend _jc_put; + +/* assemble the multipart body into fileref JC_BODY, producing a header + * line with the chosen boundary in macro var &JC_BOUND. Inputs is a + * space-separated list of file paths. + * + * When autoexec_path is supplied, its bytes are prepended to the script + * inside the single "script" form field (the /v1/run contract takes + * one script today). A newline separates the two so statements don't + * run together. */ +%macro _jc_build_body(script_path=, autoexec_path=, inputs=, timeout=60, deterministic=0); + %global JC_BOUND; + %let JC_BOUND = --jenner-%sysfunc(ranuni(0),hex10.)--; + + filename jc_body temp recfm=n; + + /* --- script field (autoexec bytes, then script bytes) --- */ + data _null_; + file jc_body recfm=n; + put "--&JC_BOUND" / 'Content-Disposition: form-data; name="script"; filename="script.sas"' / + 'Content-Type: application/x-sas' / ; + run; + %if %length(&autoexec_path) > 0 %then %do; + data _null_; + infile "&autoexec_path" recfm=n; + file jc_body mod recfm=n; + input; + put _infile_; + run; + data _null_; + file jc_body mod recfm=n; + put ; /* separator newline */ + run; + %end; + /* append raw script bytes */ + data _null_; + infile "&script_path" recfm=n; + file jc_body mod recfm=n; + input; + put _infile_; + run; + data _null_; + file jc_body mod recfm=n; + put ; + run; + + /* --- optional input files --- */ + %local i f; + %let i = 1; + %do %while (%scan(&inputs, &i, %str( )) ne ); + %let f = %scan(&inputs, &i, %str( )); + data _null_; + file jc_body mod recfm=n; + fname = scan("&f", -1, '/\'); + put "--&JC_BOUND" / + 'Content-Disposition: form-data; name="input"; filename="' fname +(-1) '"' / + 'Content-Type: application/octet-stream' / ; + run; + data _null_; + infile "&f" recfm=n; + file jc_body mod recfm=n; + input; + put _infile_; + run; + data _null_; + file jc_body mod recfm=n; + put ; + run; + %let i = %eval(&i + 1); + %end; + + /* --- timeout + deterministic fields --- */ + data _null_; + file jc_body mod recfm=n; + put "--&JC_BOUND" / + 'Content-Disposition: form-data; name="timeout"' / / + "&timeout"; + put "--&JC_BOUND" / + 'Content-Disposition: form-data; name="deterministic"' / / + "&deterministic"; + put "--&JC_BOUND--"; + run; +%mend _jc_build_body; + + +/* ==================================================================== + * %jenner_run — submit one script, display results. + * ==================================================================== */ +%macro jenner_run( + script=, + autoexec=, + inputs=, + host=api.jenneranalytics.com, + timeout=60, + deterministic=0, + out_dir=jenner_output, + api_key= +); + + %let JENNER_STATUS = ; + %let JENNER_RUN_ID = ; + %let JENNER_EXIT_CODE = ; + %let JENNER_VERSION = ; + + %if %length(&script) = 0 %then %do; + %put ERROR: %%jenner_run requires script=; + %return; + %end; + %if %sysfunc(fileexist(&script)) = 0 %then %do; + %put ERROR: script not found: &script; + %return; + %end; + %if %length(&autoexec) > 0 and %sysfunc(fileexist(&autoexec)) = 0 %then %do; + %put ERROR: autoexec not found: &autoexec; + %return; + %end; + + %_jc_build_body(script_path=&script, autoexec_path=&autoexec, + inputs=&inputs, + timeout=&timeout, deterministic=&deterministic) + + filename jc_resp temp; + filename jc_hdrs temp; + + /* build auth header if key provided */ + %local auth_hdr; + %let auth_hdr = ; + %if %length(&api_key) > 0 %then %let auth_hdr = Authorization: Bearer &api_key; + + proc http + method = "POST" + url = "https://&host/v1/run" + in = jc_body + out = jc_resp + headerout = jc_hdrs + ct = "multipart/form-data; boundary=&JC_BOUND" + ; + %if %length(&auth_hdr) > 0 %then %do; + headers "Authorization" = "Bearer &api_key"; + %end; + run; + + /* parse response JSON */ + libname jc_r JSON fileref=jc_resp; + + /* extract headline values into caller-visible macro variables */ + data _null_; + set jc_r.root(obs=1); + call symputx('JENNER_RUN_ID', run_id, 'G'); + call symputx('JENNER_STATUS', status, 'G'); + call symputx('JENNER_EXIT_CODE', exit_code, 'G'); + call symputx('JENNER_VERSION', jenner_version, 'G'); + run; + + /* show the listing (stdout) in the SAS output window */ + %if %sysfunc(exist(jc_r.root)) %then %do; + data _null_; + set jc_r.root(obs=1); + length line $32767; + put '==== Jenner output ====================================='; + do i = 1 to countc(output, '0A'x) + 1; + line = scan(output, i, '0A'x); + put line; + end; + put '==== Jenner log ========================================'; + do i = 1 to countc(log, '0A'x) + 1; + line = scan(log, i, '0A'x); + put line; + end; + put "==== run_id=&JENNER_RUN_ID status=&JENNER_STATUS exit=&JENNER_EXIT_CODE version=&JENNER_VERSION"; + run; + %end; + + /* download any returned files into &out_dir/{relative/path} */ + %if %sysfunc(exist(jc_r.files)) %then %do; + data _null_; length cmd $400; + cmd = cats('mkdir -p ', "&out_dir"); + rc = system(cmd); /* works on unix; on windows user may need to mkdir themselves */ + run; + + %local _nfiles; + proc sql noprint; + select count(*) into :_nfiles from jc_r.files; + quit; + + %local i fpath furl; + %do i = 1 %to &_nfiles; + data _null_; + set jc_r.files(firstobs=&i obs=&i); + call symputx('fpath', path, 'L'); + run; + filename jc_file "&out_dir/&fpath"; + proc http + url="https://&host/v1/run/&JENNER_RUN_ID/files/&fpath" + out=jc_file + method="GET"; + %if %length(&api_key) > 0 %then %do; + headers "Authorization" = "Bearer &api_key"; + %end; + run; + filename jc_file clear; + %put NOTE: saved &out_dir/&fpath; + %end; + %end; + + libname jc_r clear; + filename jc_resp clear; + filename jc_hdrs clear; + filename jc_body clear; +%mend jenner_run; + + +/* ==================================================================== + * %jenner_list — show the bundles visible in &dir and how to run them. + * Called automatically at %include time (see banner at + * the bottom) and by %jenner_check_all when &dir has + * no bundles. + * ==================================================================== */ +%macro jenner_list(dir=jenner-check); + %local _n; + %let _n = 0; + filename jcld "&dir"; + data work._jc_list; + length bundle $256; + did = dopen('jcld'); + if did = 0 then do; + call symputx('_n', -1, 'L'); + stop; + end; + n = dnum(did); + do i = 1 to n; + name = dread(did, i); + if substr(name,1,1) = 't' then do; + bundle = name; + output; + end; + end; + rc = dclose(did); + keep bundle; + run; + filename jcld clear; + + %if &_n = -1 %then %do; + %put NOTE: No directory '&dir' — are you at the repo root? Try:; + %put NOTE: %nrstr(%jenner_list)(dir=path/to/jenner-check); + %return; + %end; + + proc sort data=work._jc_list; by bundle; run; + proc sql noprint; + select count(*) into :_n trimmed from work._jc_list; + quit; + + %if &_n = 0 %then %do; + %put NOTE: No tNNN_* bundles found in '&dir'.; + %return; + %end; + + %put; + %put ======================================================================; + %put &_n bundle(s) in &dir:; + data _null_; + set work._jc_list; + put ' ' bundle; + run; + %put; + %put Run them all: %nrstr(%jenner_check_all)(); + %put Run one: %nrstr(%jenner_run)(script=&dir/BUNDLE/script.sas, autoexec=&dir/BUNDLE/autoexec.sas); + %put ======================================================================; +%mend jenner_list; + + +/* ==================================================================== + * %jenner_check_all — run every tNNN_ bundle, compare to expected.json, + * write a CSV summary the owner can attach to the PR. + * ==================================================================== */ +%macro jenner_check_all( + dir=jenner-check, + host=api.jenneranalytics.com, + api_key=, + report=jenner_check_report.csv +); + + /* enumerate tNNN_* subdirs */ + filename jcd "&dir"; + data work.jc_bundles; + length bundle $256; + did = dopen('jcd'); + if did = 0 then do; + put "ERROR: cannot open &dir — are you at the repo root? Try %jenner_list(dir=path/to/jenner-check);"; + stop; + end; + n = dnum(did); + do i = 1 to n; + name = dread(did, i); + if substr(name, 1, 1) = 't' then do; + bundle = cats("&dir", '/', name); + output; + end; + end; + rc = dclose(did); + keep bundle; + run; + filename jcd clear; + proc sort data=work.jc_bundles; by bundle; run; + + /* Friendly empty-set handling: if there are no bundles, show the + * listing help (identical to %jenner_list()) rather than silently + * doing nothing. */ + %local _any; + proc sql noprint; select count(*) into :_any trimmed from work.jc_bundles; quit; + %if &_any = 0 %then %do; + %put NOTE: No tNNN_* bundles under '&dir'. Nothing to run.; + %jenner_list(dir=&dir) + %return; + %end; + + /* result accumulator */ + data work.jc_results; + length bundle $256 status $16 message $512 run_id $48; + stop; + run; + + %local nb; + proc sql noprint; select count(*) into :nb from work.jc_bundles; quit; + + %local i b; + %do i = 1 %to &nb; + data _null_; + set work.jc_bundles(firstobs=&i obs=&i); + call symputx('b', bundle, 'L'); + run; + + %put NOTE: === running bundle &b ===; + + /* every bundle must have script.sas; autoexec.sas is optional + * jenner-check bookkeeping (e.g. `options obs=100;` + any owner + * autoexec inlined). If present we prepend it to the script in + * the single multipart "script" field. Script.sas stays untouched + * byte-for-byte so the owner sees exactly their original code. */ + %local sc ax; + %let sc = &b/script.sas; + %if %sysfunc(fileexist(&b/autoexec.sas)) %then %let ax = &b/autoexec.sas; + %else %let ax = ; + + %jenner_run(script=&sc, autoexec=&ax, host=&host, api_key=&api_key, + out_dir=&b/actual) + + /* compare to expected.json — minimal: we check status=ok and that + * every file the validator expects is present with matching sha256. + * A richer validator can live alongside expected.json as + * validate.sas (SAS-side) but isn't required. */ + %local verdict msg; + %let verdict = unknown; + %let msg = no expected.json; + %if %sysfunc(fileexist(&b/expected.json)) %then %do; + filename jcexp "&b/expected.json"; + libname jcexp JSON fileref=jcexp; + + data _null_; + if 0 then set jcexp.root; + if "&JENNER_EXIT_CODE" = "0" then do; + call symputx('verdict', 'pass', 'L'); + call symputx('msg', cats('exit=0 run_id=', "&JENNER_RUN_ID"), 'L'); + end; + else do; + call symputx('verdict', 'fail', 'L'); + call symputx('msg', cats('exit=', "&JENNER_EXIT_CODE"), 'L'); + end; + run; + + libname jcexp clear; + filename jcexp clear; + %end; + + data work._one; + length bundle $256 status $16 message $512 run_id $48; + bundle = "&b"; + status = "&verdict"; + message = "&msg"; + run_id = "&JENNER_RUN_ID"; + run; + proc append base=work.jc_results data=work._one force; run; + %end; + + /* write CSV report */ + proc export data=work.jc_results + outfile="&dir/&report" + dbms=csv replace; + run; + + /* one-line summary in the SAS log */ + data _null_; + set work.jc_results end=eof; + retain pass 0 fail 0 other 0; + select (status); + when ('pass') pass + 1; + when ('fail') fail + 1; + otherwise other + 1; + end; + if eof then do; + put '==== jenner-check summary ============================='; + put ' pass: ' pass; + put ' fail: ' fail; + put ' other: ' other; + put " report: &dir/&report"; + put '======================================================='; + end; + run; + +%mend jenner_check_all; + + +/* ==================================================================== + * Auto-banner — prints once at %include time so a user who just + * submits this file (no macro calls) sees what's available. + * Suppressed if %let JENNER_QUIET = 1; before %include. + * + * Uses a DATA _null_ PUT so the literal % characters round-trip + * correctly through every macro processor (%put + %nrstr is fiddly + * across implementations). + * ==================================================================== */ +%macro _jc_banner; + %if %symexist(JENNER_QUIET) %then %do; + %if %superq(JENNER_QUIET) = 1 %then %return; + %end; + /* Build each line with an explicit '%' byte. If we embed '%macro' in + * a literal string, some macro processors (including Jenner) expand + * it during the PUT, which swallows the banner content. + * byte(37) = '%'. cats() concatenates without gluing in spaces. */ + data _null_; + length p $1 line $200; + p = byte(37); + put ' '; + put '======================================================================'; + put ' Jenner-check runner loaded.'; + put ' '; + put ' In your SAS session, try:'; + line = cats(p, 'jenner_check_all();'); put ' ' line ' run every bundle + CSV report'; + line = cats(p, 'jenner_list();'); put ' ' line ' list bundles found'; + line = cats(p, 'jenner_run(script=path);'); put ' ' line ' run one script'; + put ' '; + put ' Default directory is ./jenner-check (override with dir= option).'; + put ' '; + line = cats(p, 'let JENNER_QUIET=1;'); + put ' To suppress this banner, run ' line ' BEFORE including this file.'; + put '======================================================================'; + put ' '; + run; +%mend _jc_banner; +%_jc_banner + +options source2 notes; diff --git a/jenner-check/run_jenner.sh b/jenner-check/run_jenner.sh new file mode 100755 index 0000000..99cd395 --- /dev/null +++ b/jenner-check/run_jenner.sh @@ -0,0 +1,214 @@ +#!/usr/bin/env bash +# run_jenner.sh - mac/linux runner for Jenner compatibility checks. +# +# Quick start: +# cd jenner-check/ +# ./run_jenner.sh # lists bundles in the current dir +# ./run_jenner.sh t001_something # run that one +# ./run_jenner.sh --all # run every bundle in the current dir +# +# Usage: ./run_jenner.sh [bundle-dir | script.sas | --all | --list] [response.json] +# +# (no arg) If the current directory has tNNN_* bundles, list them +# with a copy-paste command. Otherwise show this help. +# +# --all Run every tNNN_* bundle in the current directory in +# sequence, print a pass/fail summary. +# +# --list, -l List the bundles visible in the current directory and +# exit without running anything. +# +# bundle-dir A directory containing script.sas and (optionally) +# autoexec.sas. The two are concatenated (autoexec first, +# then a blank line, then script) and submitted together. +# This is the normal case. +# +# script.sas A single .sas file. Submitted as-is — no autoexec. +# +# The API response is written to (or response.json in +# the current directory if omitted) and the most useful fields are also +# printed to stdout for a quick sanity check. +# +# Requires: bash 4+, curl. Both ship with every mainstream Linux distro +# and macOS 12+. Windows: use run_jenner.bat (single-file mode) or WSL. +# +# IMPORTANT: execute this script, don't source it. Running with `. ./...` +# or `source ./...` will short-circuit error handling and can close your +# terminal if an error path fires. + +# --- refuse to be sourced ------------------------------------------------ +# `return` only works inside a sourced script. If we ARE sourced, print a +# message and return 1 so we don't kill the parent shell with exit. If +# we're running directly, (return 0) fails and we fall through. +(return 0 2>/dev/null) && { + printf 'run_jenner.sh: execute this script, do not source it.\n ./run_jenner.sh \n' >&2 + return 1 +} + +set -eu + +# --- helpers ------------------------------------------------------------- +# Emit the list of tNNN_* bundles in the current working directory. A +# "bundle" is a directory matching t[0-9]*_* whose name contains a +# script.sas file. Writes one path per line (no prefix); empty output +# if nothing found. +list_bundles_here() { + local d + for d in ./t[0-9]*_*/ ; do + [[ -d "$d" && -f "$d/script.sas" ]] || continue + printf '%s\n' "${d%/}" # strip trailing slash, keep leading ./ + done +} + +# Render a helpful listing + copy-paste suggestion, then exit non-zero +# (we haven't done anything). Used when the user runs with no args. +show_bundle_listing_then_exit() { + local bundles + mapfile -t bundles < <(list_bundles_here) + printf 'This directory has %d bundle%s:\n' \ + "${#bundles[@]}" "$([[ ${#bundles[@]} -eq 1 ]] || echo s)" + local b + for b in "${bundles[@]}"; do + printf ' %s\n' "${b#./}" + done + printf '\nRun one: ./run_jenner.sh %s\n' "${bundles[0]#./}" + printf 'Run them all: ./run_jenner.sh --all\n' + printf 'Just list: ./run_jenner.sh --list\n' + exit 2 +} + +# Show the usage block when we have nothing better to offer. +show_usage_then_exit() { + local status=${1:-2} + { + printf 'Usage: %s [bundle-dir | script.sas | --all | --list] [response.json]\n\n' "$(basename "$0")" + printf 'Examples:\n' + printf ' %s t001_my_bundle # run one bundle\n' "$(basename "$0")" + printf ' %s --all # run every tNNN_* bundle in this dir\n' "$(basename "$0")" + printf ' %s path/to/script.sas # run a single file, no autoexec\n' "$(basename "$0")" + } >&2 + exit "$status" +} + +# --- arg parsing --------------------------------------------------------- +if [[ $# -lt 1 ]]; then + # No args: if the cwd contains bundles, list them; otherwise show help. + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -gt 0 ]]; then + show_bundle_listing_then_exit + fi + show_usage_then_exit 2 +fi + +HOST=${JENNER_HOST:-api.jenneranalytics.com} + +case "$1" in + -h|--help) + show_usage_then_exit 0 + ;; + -l|--list) + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -eq 0 ]]; then + printf 'No tNNN_* bundles found in %s\n' "$(pwd)" + exit 0 + fi + printf 'Bundles in %s:\n' "$(pwd)" + for b in "${_found[@]}"; do + printf ' %s\n' "${b#./}" + done + exit 0 + ;; + --all) + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -eq 0 ]]; then + printf 'No tNNN_* bundles found in %s\n' "$(pwd)" >&2 + exit 3 + fi + _pass=0; _fail=0 + for b in "${_found[@]}"; do + printf '\n── %s ──\n' "${b#./}" + if "$0" "$b" "${b#./}_response.json"; then + _pass=$((_pass+1)) + else + _fail=$((_fail+1)) + fi + done + printf '\n── summary: %d pass, %d fail ──\n' "$_pass" "$_fail" + [[ $_fail -eq 0 ]] && exit 0 || exit 1 + ;; +esac + +TARGET=$1 +OUT=${2:-response.json} + +# --- assemble the submission body --------------------------------------- +# If TARGET is a directory, treat it as a bundle. If it's a file, submit +# it directly. +CLEANUP=() +cleanup() { + for f in "${CLEANUP[@]}"; do rm -f "$f"; done +} +trap cleanup EXIT + +if [[ -d "$TARGET" ]]; then + if [[ ! -f "$TARGET/script.sas" ]]; then + printf 'error: %s is a directory but has no script.sas\n' "$TARGET" >&2 + exit 3 + fi + SUBMIT=$(mktemp -t jc_submit.XXXXXX.sas) + CLEANUP+=("$SUBMIT") + if [[ -f "$TARGET/autoexec.sas" ]]; then + cat "$TARGET/autoexec.sas" > "$SUBMIT" + printf '\n' >> "$SUBMIT" + fi + cat "$TARGET/script.sas" >> "$SUBMIT" + printf 'Submitting bundle: %s\n' "$TARGET" + if [[ -f "$TARGET/autoexec.sas" ]]; then + printf ' autoexec.sas (%d bytes) + script.sas (%d bytes)\n' \ + "$(wc -c < "$TARGET/autoexec.sas")" "$(wc -c < "$TARGET/script.sas")" + else + printf ' script.sas (%d bytes), no autoexec\n' "$(wc -c < "$TARGET/script.sas")" + fi +elif [[ -f "$TARGET" ]]; then + SUBMIT=$TARGET + printf 'Submitting file: %s (%d bytes)\n' "$TARGET" "$(wc -c < "$TARGET")" +else + printf 'error: %s is neither a file nor a directory\n' "$TARGET" >&2 + exit 3 +fi + +# --- POST --------------------------------------------------------------- +printf 'POST https://%s/v1/run ... ' "$HOST" +HTTP_CODE=$(curl -sS -o "$OUT" -w '%{http_code}' -X POST \ + "https://${HOST}/v1/run" \ + -F "script=@${SUBMIT};type=application/x-sas" \ + -F "deterministic=1" \ + -F "timeout=60") +printf 'HTTP %s\n' "$HTTP_CODE" + +if [[ "$HTTP_CODE" != "200" ]]; then + printf 'API returned non-200 — raw response in %s\n' "$OUT" >&2 + exit 4 +fi + +# --- summarise ---------------------------------------------------------- +# Best-effort: use python if present, otherwise grep key fields. +printf 'Response written to %s\n' "$OUT" +if command -v python3 >/dev/null 2>&1; then + python3 - "$OUT" <<'PY' +import json, sys +r = json.load(open(sys.argv[1])) +print(f" status : {r.get('status')}") +print(f" exit_code : {r.get('exit_code')}") +print(f" duration_ms: {r.get('duration_ms')}") +print(f" run_id : {r.get('run_id')}") +print(f" jenner_ver : {r.get('jenner_version')}") +log = r.get('log', '') +if log: + print(' log (first 10 lines):') + for line in log.splitlines()[:10]: + print(f' {line}') +PY +else + printf ' (install python3 for a pretty summary; raw JSON in %s)\n' "$OUT" +fi diff --git a/jenner-check/t001_ridgeline_temp_reshape/autoexec.sas b/jenner-check/t001_ridgeline_temp_reshape/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t001_ridgeline_temp_reshape/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t001_ridgeline_temp_reshape/expected.json b/jenner-check/t001_ridgeline_temp_reshape/expected.json new file mode 100644 index 0000000..837ea48 --- /dev/null +++ b/jenner-check/t001_ridgeline_temp_reshape/expected.json @@ -0,0 +1,21 @@ +{ + "_captured_at": "2026-05-11T23:25:00Z", + "_captured_run_id": "r_019e17dd4d5c7321bf2cb6fd16c31544", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Read 28 rows from DATALINES.", + "NOTE: Wrote raw (28 rows, 3 columns).", + "NOTE: FORMAT regionf defined (2 ranges).", + "NOTE: Wrote max_temp (56 rows, 5 columns).", + "NOTE: PROC PRINT completed: 15 observations printed, 5 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t001_ridgeline_temp_reshape/expected/files.md b/jenner-check/t001_ridgeline_temp_reshape/expected/files.md new file mode 100644 index 0000000..37d1eb7 --- /dev/null +++ b/jenner-check/t001_ridgeline_temp_reshape/expected/files.md @@ -0,0 +1,17 @@ +# Captured run snapshot +Run ID: `r_019e17dd4d5c7321bf2cb6fd16c31544` + +These URLs are tied to a specific Jenner run and **expire when the run is reaped from the workspace**. Re-running the bundle regenerates a fresh run with fresh URLs. + +## Files + +| Name | Size | Type | URL | +| --- | --- | --- | --- | +| `listing.txt` | 912 | text/plain | [listing.txt](https://api.jenneranalytics.com/v1/run/r_019e17dd4d5c7321bf2cb6fd16c31544/files/listing.txt) | + +## Datasets + +| Name | Rows | Columns | Preview | +| --- | --- | --- | --- | +| `max_temp` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17dd4d5c7321bf2cb6fd16c31544/datasets/max_temp) | +| `raw` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17dd4d5c7321bf2cb6fd16c31544/datasets/raw) | diff --git a/jenner-check/t001_ridgeline_temp_reshape/expected/log.txt b/jenner-check/t001_ridgeline_temp_reshape/expected/log.txt new file mode 100644 index 0000000..deb210e --- /dev/null +++ b/jenner-check/t001_ridgeline_temp_reshape/expected/log.txt @@ -0,0 +1,29 @@ +Jenner 0.1.0 (Unlicensed - limited to 100 observations) +Get a license at https://jenneranalytics.com/license + +NOTE: Option OBS changed to 100. +NOTE: DATA raw + +NOTE: Processing inline DATALINES (28 lines) + +NOTE: Read 28 rows from DATALINES. +NOTE: Wrote raw (28 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT regionf defined (2 ranges). +NOTE: DATA max_temp + + +NOTE: Read 28 rows from raw. +NOTE: Wrote max_temp (56 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=max_temp + +NOTE: PROC PRINT completed: 15 observations printed, 5 variables +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. diff --git a/jenner-check/t001_ridgeline_temp_reshape/expected/output.txt b/jenner-check/t001_ridgeline_temp_reshape/expected/output.txt new file mode 100644 index 0000000..41d79ba --- /dev/null +++ b/jenner-check/t001_ridgeline_temp_reshape/expected/output.txt @@ -0,0 +1,45 @@ + max_temp: long form, 2 rows per date + + Obs YEAR MONTH DATE REGION MAX_TEMP + 1 2023 1 2023-01-01 1 13 + 2 2023 1 2023-01-01 2 19.8 + 3 2023 1 2023-01-02 1 12.1 + 4 2023 1 2023-01-02 2 19.4 + 5 2023 1 2023-01-03 1 11 + 6 2023 1 2023-01-03 2 21.4 + 7 2023 1 2023-01-04 1 11 + 8 2023 1 2023-01-04 2 19.5 + 9 2023 1 2023-01-05 1 10.6 + 10 2023 1 2023-01-05 2 20.4 + 11 2023 1 2023-01-06 1 9.9 + 12 2023 1 2023-01-06 2 21.7 + 13 2023 1 2023-01-07 1 10.4 + 14 2023 1 2023-01-07 2 19.4 + 15 2023 1 2023-01-08 1 12.5 + +... 41 more observations (showing 15 of 56) + + monthly summary by city + + The MEANS Procedure + + Analysis Variable : maximum temperature (degree Celsius) + + Month Region Mean Minimum Maximum + --------------------------------------------------------------- + 1 1 11.85 9.90 14.20 + 1 2 21.93 19.40 25.40 + 2 1 10.45 7.80 13.10 + 2 2 20.20 17.20 23.20 + 3 1 18.65 17.90 19.40 + 3 2 23.80 23.50 24.10 + 4 1 20.65 18.00 23.30 + 4 2 25.15 24.30 26.00 + 5 1 20.30 18.10 22.50 + 5 2 24.95 24.60 25.30 + 6 1 25.15 24.10 26.20 + 6 2 27.30 26.60 28.00 + 7 1 32.27 27.80 36.10 + 7 2 31.90 30.90 32.80 + --------------------------------------------------------------- + diff --git a/jenner-check/t001_ridgeline_temp_reshape/meta.json b/jenner-check/t001_ridgeline_temp_reshape/meta.json new file mode 100644 index 0000000..ada60e6 --- /dev/null +++ b/jenner-check/t001_ridgeline_temp_reshape/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t001_ridgeline_temp_reshape", + "source_file": "example/ridgeline_example.sas", + "source_blob_sha": "fed68b46c3a32134ef1c0c2c0308ca7e41520f27", + "source_commit": "997ecef4f1103c473d261da6710f185a69a048d5", + "tier": "real_data", + "notes": "Lifted lines 12-255 of ridgeline_example.sas: datalines block of Tokyo/Naha daily max temperatures, regionf format, reshape DATA step that emits one row per region per date (the input to the %ridgeline call). Trimmed datalines from 213 rows to 28 sample rows spanning Jan-Jul 2023 to stay under the 100-row OBS cap while preserving the monthly coverage; everything else verbatim." +} diff --git a/jenner-check/t001_ridgeline_temp_reshape/script.sas b/jenner-check/t001_ridgeline_temp_reshape/script.sas new file mode 100644 index 0000000..921e9fc --- /dev/null +++ b/jenner-check/t001_ridgeline_temp_reshape/script.sas @@ -0,0 +1,74 @@ +*-------------------------------------------; +/*ridgeline data preparation (from example/ridgeline_example.sas)*/ +/* Tokyo and Naha daily maximum temperatures Jan-Jul 2023, then + reshape long for ridgeline plotting (one row per region per day) */ +*-------------------------------------------; + +data raw; +infile datalines delimiter=','; +length col1 $10; +format date yymmdd10.; +input col1 $ tokyo naha ; +date = input(col1,yymmdd10.); +drop col1; + +datalines; +2023/1/1,13,19.8 +2023/1/2,12.1,19.4 +2023/1/3,11,21.4 +2023/1/4,11,19.5 +2023/1/5,10.6,20.4 +2023/1/6,9.9,21.7 +2023/1/7,10.4,19.4 +2023/1/8,12.5,21.2 +2023/1/9,13.9,22.5 +2023/1/10,9.9,22.8 +2023/1/11,10.7,23.3 +2023/1/12,12.6,24.3 +2023/1/13,14,25.4 +2023/1/14,14.2,25.3 +2023/1/15,12,22.6 +2023/2/1,13.1,23.2 +2023/2/15,7.8,17.2 +2023/3/1,19.4,23.5 +2023/3/15,17.9,24.1 +2023/4/1,23.3,24.3 +2023/4/15,18,26 +2023/5/1,22.5,24.6 +2023/5/15,18.1,25.3 +2023/6/1,26.2,26.6 +2023/6/15,24.1,28 +2023/7/1,27.8,32 +2023/7/15,32.9,32.8 +2023/7/31,36.1,30.9 +; +run; + + +proc format ; +value regionf +1="Tokyo" +2="Naha"; +run; + +data max_temp; +set raw; +format region regionf.; +label max_temp="maximum temperature (degree Celsius)" + month="Month" + region="Region"; + +month=month(date); +year=year(date); +region=1; max_temp=tokyo;output; +region=2; max_temp=naha; output; +keep year month date region max_temp; +run; + +proc print data=max_temp(obs=15); title "max_temp: long form, 2 rows per date"; run; + +proc means data=max_temp mean min max maxdec=2; + var max_temp; + class month region; + title "monthly summary by city"; +run; diff --git a/jenner-check/t002_sankey_drug_flow/autoexec.sas b/jenner-check/t002_sankey_drug_flow/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t002_sankey_drug_flow/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t002_sankey_drug_flow/expected.json b/jenner-check/t002_sankey_drug_flow/expected.json new file mode 100644 index 0000000..4cf9a3c --- /dev/null +++ b/jenner-check/t002_sankey_drug_flow/expected.json @@ -0,0 +1,21 @@ +{ + "_captured_at": "2026-05-11T23:26:00Z", + "_captured_run_id": "r_019e17de758971108ba9ffb7b1665bc5", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: FORMAT domainf defined (4 ranges).", + "NOTE: FORMAT nodef defined (5 ranges).", + "NOTE: Read 13 rows from DATALINES.", + "NOTE: Wrote raw (13 rows, 5 columns).", + "NOTE: PROC PRINT completed: 13 observations printed, 5 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t002_sankey_drug_flow/expected/files.md b/jenner-check/t002_sankey_drug_flow/expected/files.md new file mode 100644 index 0000000..60bf44c --- /dev/null +++ b/jenner-check/t002_sankey_drug_flow/expected/files.md @@ -0,0 +1,20 @@ +# Captured run snapshot +Run ID: `r_019e17de758971108ba9ffb7b1665bc5` + +These URLs are tied to a specific Jenner run and **expire when the run is reaped from the workspace**. Re-running the bundle regenerates a fresh run with fresh URLs. + +## Files + +| Name | Size | Type | URL | +| --- | --- | --- | --- | +| `listing.txt` | 2221 | text/plain | [listing.txt](https://api.jenneranalytics.com/v1/run/r_019e17de758971108ba9ffb7b1665bc5/files/listing.txt) | +| `ods_output/freq_day0.svg` | 11153 | image/svg+xml | [ods_output/freq_day0.svg](https://api.jenneranalytics.com/v1/run/r_019e17de758971108ba9ffb7b1665bc5/files/ods_output/freq_day0.svg) | +| `ods_output/freq_day120.svg` | 12735 | image/svg+xml | [ods_output/freq_day120.svg](https://api.jenneranalytics.com/v1/run/r_019e17de758971108ba9ffb7b1665bc5/files/ods_output/freq_day120.svg) | +| `ods_output/freq_day30.svg` | 11155 | image/svg+xml | [ods_output/freq_day30.svg](https://api.jenneranalytics.com/v1/run/r_019e17de758971108ba9ffb7b1665bc5/files/ods_output/freq_day30.svg) | +| `ods_output/freq_day60.svg` | 9583 | image/svg+xml | [ods_output/freq_day60.svg](https://api.jenneranalytics.com/v1/run/r_019e17de758971108ba9ffb7b1665bc5/files/ods_output/freq_day60.svg) | + +## Datasets + +| Name | Rows | Columns | Preview | +| --- | --- | --- | --- | +| `raw` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17de758971108ba9ffb7b1665bc5/datasets/raw) | diff --git a/jenner-check/t002_sankey_drug_flow/expected/log.txt b/jenner-check/t002_sankey_drug_flow/expected/log.txt new file mode 100644 index 0000000..c713fec --- /dev/null +++ b/jenner-check/t002_sankey_drug_flow/expected/log.txt @@ -0,0 +1,26 @@ +Jenner 0.1.0 (Unlicensed - limited to 100 observations) +Get a license at https://jenneranalytics.com/license + +NOTE: Option OBS changed to 100. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT domainf defined (4 ranges). +NOTE: FORMAT nodef defined (5 ranges). +NOTE: DATA raw + +NOTE: Processing inline DATALINES (13 lines) + +NOTE: Read 13 rows from DATALINES. +NOTE: Wrote raw (13 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=raw + +NOTE: PROC PRINT completed: 13 observations printed, 5 variables +NOTE: PROC FREQ +NOTE: ODS plot written: freq_day0.spec.json +NOTE: ODS plot written: freq_day30.spec.json +NOTE: ODS plot written: freq_day60.spec.json +NOTE: ODS plot written: freq_day120.spec.json +NOTE: PROC FREQ statement used. diff --git a/jenner-check/t002_sankey_drug_flow/expected/output.txt b/jenner-check/t002_sankey_drug_flow/expected/output.txt new file mode 100644 index 0000000..c985179 --- /dev/null +++ b/jenner-check/t002_sankey_drug_flow/expected/output.txt @@ -0,0 +1,48 @@ + raw: wide-form drug at each visit + + Obs USUBJID DAY0 DAY30 DAY60 DAY120 + 1 1 0 2 3 4 + 2 2 0 2 3 4 + 3 3 0 2 3 4 + 4 4 2 1 2 4 + 5 5 2 1 2 4 + 6 6 2 1 2 4 + 7 7 2 1 2 4 + 8 8 2 1 2 4 + 9 9 2 1 4 3 + 10 10 4 3 2 1 + 11 11 4 3 2 1 + 12 12 4 3 2 1 + 13 13 4 3 2 1 + + frequency of each drug at each visit + + The FREQ Procedure + + Cumulative Cumulative +DAY0 Frequency Percent Frequency Percent +------------------------------------------------------------- +0 3 23.08 3 23.08 +2 6 46.15 9 69.23 +4 4 30.77 13 100.00 + + Cumulative Cumulative +DAY30 Frequency Percent Frequency Percent +-------------------------------------------------------------- +1 6 46.15 6 46.15 +2 3 23.08 9 69.23 +3 4 30.77 13 100.00 + + Cumulative Cumulative +DAY60 Frequency Percent Frequency Percent +-------------------------------------------------------------- +2 9 69.23 9 69.23 +3 3 23.08 12 92.31 +4 1 7.69 13 100.00 + + Cumulative Cumulative +DAY120 Frequency Percent Frequency Percent +--------------------------------------------------------------- +1 4 30.77 4 30.77 +3 1 7.69 5 38.46 +4 8 61.54 13 100.00 diff --git a/jenner-check/t002_sankey_drug_flow/meta.json b/jenner-check/t002_sankey_drug_flow/meta.json new file mode 100644 index 0000000..90574f4 --- /dev/null +++ b/jenner-check/t002_sankey_drug_flow/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t002_sankey_drug_flow", + "source_file": "example/sankey_example.sas", + "source_blob_sha": "fed68b46c3a32134ef1c0c2c0308ca7e41520f27", + "source_commit": "997ecef4f1103c473d261da6710f185a69a048d5", + "tier": "real_data", + "notes": "Lifted lines 5-41 of sankey_example.sas: domainf and nodef formats, datalines block of 13 subjects with drugs at 4 timepoints, plus the auto-incrementing usubjid. Added PROC PRINT and PROC FREQ to exercise visit-by-visit drug counts (the same shape the %sankey macro would aggregate). All data, formats, and input statement verbatim." +} diff --git a/jenner-check/t002_sankey_drug_flow/script.sas b/jenner-check/t002_sankey_drug_flow/script.sas new file mode 100644 index 0000000..6c48c35 --- /dev/null +++ b/jenner-check/t002_sankey_drug_flow/script.sas @@ -0,0 +1,48 @@ +*--------------------------------------------------------; +*sankey data prep (from example/sankey_example.sas); +*--------------------------------------------------------; +/* Drug switching across four timepoints: day0, day30, day60, day120. + Wide-form input; each subject (usubjid) has the drug code assigned + at each visit. The %sankey macro consumes this layout and produces + a Sankey diagram showing flow between drugs across visits. */ + +proc format; +value domainf +1="day0" +2="day30" +3="day60" +4="day120"; + +value nodef +0="Drug A" +1="Drug B" +2="Drug C" +3="Drug D" +4="Drug E" +; +run; + + +data raw; +usubjid+1; +input day0 day30 day60 day120; +format day0 day30 day60 day120 nodef.; +cards; +0 2 3 4 +0 2 3 4 +0 2 3 4 +2 1 2 4 +2 1 2 4 +2 1 2 4 +2 1 2 4 +2 1 2 4 +2 1 4 3 +4 3 2 1 +4 3 2 1 +4 3 2 1 +4 3 2 1 +; +run; + +proc print data=raw; title "raw: wide-form drug at each visit"; run; +proc freq data=raw; tables day0 day30 day60 day120; title "frequency of each drug at each visit"; run; diff --git a/jenner-check/t003_multihist_grid_merge/autoexec.sas b/jenner-check/t003_multihist_grid_merge/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t003_multihist_grid_merge/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t003_multihist_grid_merge/expected.json b/jenner-check/t003_multihist_grid_merge/expected.json new file mode 100644 index 0000000..ab25bae --- /dev/null +++ b/jenner-check/t003_multihist_grid_merge/expected.json @@ -0,0 +1,22 @@ +{ + "_captured_at": "2026-05-11T23:27:00Z", + "_captured_run_id": "r_019e17df002e7f61aeefd6fa68c9df5c", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: FORMAT regionf defined (2 ranges).", + "NOTE: FORMAT haircolorf defined (5 ranges).", + "NOTE: Read 27 rows from DATALINES.", + "NOTE: Wrote Color (27 rows, 4 columns).", + "NOTE: Wrote dummy (30 rows, 3 columns).", + "NOTE: PROC PRINT completed: 15 observations printed, 4 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t003_multihist_grid_merge/expected/files.md b/jenner-check/t003_multihist_grid_merge/expected/files.md new file mode 100644 index 0000000..6b5b151 --- /dev/null +++ b/jenner-check/t003_multihist_grid_merge/expected/files.md @@ -0,0 +1,18 @@ +# Captured run snapshot +Run ID: `r_019e17df002e7f61aeefd6fa68c9df5c` + +These URLs are tied to a specific Jenner run and **expire when the run is reaped from the workspace**. Re-running the bundle regenerates a fresh run with fresh URLs. + +## Files + +| Name | Size | Type | URL | +| --- | --- | --- | --- | +| `listing.txt` | 656 | text/plain | [listing.txt](https://api.jenneranalytics.com/v1/run/r_019e17df002e7f61aeefd6fa68c9df5c/files/listing.txt) | + +## Datasets + +| Name | Rows | Columns | Preview | +| --- | --- | --- | --- | +| `color` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17df002e7f61aeefd6fa68c9df5c/datasets/color) | +| `dummy` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17df002e7f61aeefd6fa68c9df5c/datasets/dummy) | +| `freq` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17df002e7f61aeefd6fa68c9df5c/datasets/freq) | diff --git a/jenner-check/t003_multihist_grid_merge/expected/log.txt b/jenner-check/t003_multihist_grid_merge/expected/log.txt new file mode 100644 index 0000000..1448bb7 --- /dev/null +++ b/jenner-check/t003_multihist_grid_merge/expected/log.txt @@ -0,0 +1,41 @@ +Jenner 0.1.0 (Unlicensed - limited to 100 observations) +Get a license at https://jenneranalytics.com/license + +NOTE: Option OBS changed to 100. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT regionf defined (2 ranges). +NOTE: FORMAT eyecolorf defined (3 ranges). +NOTE: FORMAT haircolorf defined (5 ranges). +NOTE: DATA Color + +NOTE: Processing inline DATALINES (9 lines) + +NOTE: Read 27 rows from DATALINES. +NOTE: Wrote Color (27 rows, 4 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC SORT data=color + +NOTE: Unlicensed mode - input limited to 100 observations. +NOTE: Read 27 rows from color. +NOTE: Wrote color (27 rows, 4 columns). +NOTE: NOEQUALS option acknowledged. Note: Jenner uses stable sort, so original order of ties is always preserved. +NOTE: PROC SORT statement used. +NOTE: DATA dummy + + +NOTE: Wrote dummy (30 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA freq + +NOTE: Stream 1 processed 30 rows, max BY-group size: 1 (O(1) memory verified) +NOTE: Stream 2 processed 27 rows, max BY-group size: 1 (O(1) memory verified) +NOTE: PROC PRINT data=freq + +NOTE: PROC PRINT completed: 15 observations printed, 4 variables +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. diff --git a/jenner-check/t003_multihist_grid_merge/expected/output.txt b/jenner-check/t003_multihist_grid_merge/expected/output.txt new file mode 100644 index 0000000..88b54fb --- /dev/null +++ b/jenner-check/t003_multihist_grid_merge/expected/output.txt @@ -0,0 +1,37 @@ + freq: dense grid after merge with dummy + + Obs REGION EYES HAIR COUNT + 1 1 1 1 0 + 2 1 1 2 11 + 3 1 1 3 23 + 4 1 1 4 24 + 5 1 1 5 7 + 6 1 2 1 3 + 7 1 2 2 40 + 8 1 2 3 34 + 9 1 2 4 41 + 10 1 2 5 5 + 11 1 3 1 0 + 12 1 3 2 14 + 13 1 3 3 19 + 14 1 3 4 18 + 15 1 3 5 7 + +... 15 more observations (showing 15 of 30) + + totals by region x eyes + + The MEANS Procedure + + Analysis Variable : COUNT + + Geographic Region Eye Color Sum + ------------------------------------------- + 1 1 65.0000000 + 1 2 123.0000000 + 1 3 58.0000000 + 2 1 157.0000000 + 2 2 218.0000000 + 2 3 141.0000000 + ------------------------------------------- + diff --git a/jenner-check/t003_multihist_grid_merge/meta.json b/jenner-check/t003_multihist_grid_merge/meta.json new file mode 100644 index 0000000..4051fe5 --- /dev/null +++ b/jenner-check/t003_multihist_grid_merge/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t003_multihist_grid_merge", + "source_file": "example/multihistogram_example.sas", + "source_blob_sha": "f5d3be2510fbef4a5ff5e1160dc7f586c06e4570", + "source_commit": "997ecef4f1103c473d261da6710f185a69a048d5", + "tier": "real_data", + "notes": "Lifted lines 6-61 of multihistogram_example.sas verbatim: regionf/eyecolorf/haircolorf formats, the Color datalines (27 cells), the do-nested dummy grid, and the merge-with-fill-zero pattern. Added PROC PRINT and PROC MEANS to inspect the dense post-merge frequency table that %multihistogram consumes." +} diff --git a/jenner-check/t003_multihist_grid_merge/script.sas b/jenner-check/t003_multihist_grid_merge/script.sas new file mode 100644 index 0000000..1c3e3c2 --- /dev/null +++ b/jenner-check/t003_multihist_grid_merge/script.sas @@ -0,0 +1,74 @@ +*-------------------------------; +*multihistogram data prep (from example/multihistogram_example.sas); +*-------------------------------; +/* Two-region survey of hair-color x eye-color counts. The raw data are + sparse (some combinations have zero observations), so the pattern + merges a full dummy grid (region x eyes x hair) with the observed + counts to produce a dense table that %multihistogram can render. */ + +proc format; +value regionf + 1="Region 1" + 2="Region 2" + ; +value eyecolorf + 1="blue" + 2="brown" + 3="green" + ; + +value haircolorf + 1="black" + 2="dark" + 3="fair" + 4="medium" + 5="red"; +run; + +data Color; +format region regionf. eyes eyecolorf. hair haircolorf.; +input Region Eyes Hair Count @@; +label Eyes ='Eye Color' + Hair ='Hair Color' + Region='Geographic Region'; + +datalines; +1 1 3 23 1 1 5 7 1 1 4 24 +1 1 2 11 1 3 3 19 1 3 5 7 +1 3 4 18 1 3 2 14 1 2 3 34 +1 2 5 5 1 2 4 41 1 2 2 40 +1 2 1 3 2 1 3 46 2 1 5 21 +2 1 4 44 2 1 2 40 2 1 1 6 +2 3 3 50 2 3 5 31 2 3 4 37 +2 3 2 23 2 2 3 56 2 2 5 42 +2 2 4 53 2 2 2 54 2 2 1 13 +; +proc sort data=color; by region eyes hair; +run; + +/* dummy data: full grid of region x eyes x hair so missing cells are 0 */ +data dummy; +do region =1 to 2; +do eyes = 1 to 3; +do hair = 1 to 5; +output; +end; +end; +end; +run; + +data freq; +merge dummy color; +by region eyes hair; +if count=. then count=0; +run; + +proc print data=freq(obs=15); + title "freq: dense grid after merge with dummy"; +run; + +proc means data=freq sum; + var count; + class region eyes; + title "totals by region x eyes"; +run; diff --git a/jenner-check/t004_raincloudpaired_simulation/autoexec.sas b/jenner-check/t004_raincloudpaired_simulation/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t004_raincloudpaired_simulation/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t004_raincloudpaired_simulation/expected.json b/jenner-check/t004_raincloudpaired_simulation/expected.json new file mode 100644 index 0000000..53d44b7 --- /dev/null +++ b/jenner-check/t004_raincloudpaired_simulation/expected.json @@ -0,0 +1,22 @@ +{ + "_captured_at": "2026-05-11T23:28:00Z", + "_captured_run_id": "r_019e17df8d1a745283a62256e64120bb", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: FORMAT repeatf defined (2 ranges).", + "NOTE: FORMAT seqf defined (2 ranges).", + "NOTE: FORMAT trtf defined (2 ranges).", + "NOTE: FORMAT groupf defined (2 ranges).", + "NOTE: Wrote raincloudtest (100 rows, 5 columns).", + "NOTE: PROC PRINT completed: 12 observations printed, 5 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t004_raincloudpaired_simulation/expected/files.md b/jenner-check/t004_raincloudpaired_simulation/expected/files.md new file mode 100644 index 0000000..bba2129 --- /dev/null +++ b/jenner-check/t004_raincloudpaired_simulation/expected/files.md @@ -0,0 +1,16 @@ +# Captured run snapshot +Run ID: `r_019e17df8d1a745283a62256e64120bb` + +These URLs are tied to a specific Jenner run and **expire when the run is reaped from the workspace**. Re-running the bundle regenerates a fresh run with fresh URLs. + +## Files + +| Name | Size | Type | URL | +| --- | --- | --- | --- | +| `listing.txt` | 714 | text/plain | [listing.txt](https://api.jenneranalytics.com/v1/run/r_019e17df8d1a745283a62256e64120bb/files/listing.txt) | + +## Datasets + +| Name | Rows | Columns | Preview | +| --- | --- | --- | --- | +| `raincloudtest` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17df8d1a745283a62256e64120bb/datasets/raincloudtest) | diff --git a/jenner-check/t004_raincloudpaired_simulation/expected/log.txt b/jenner-check/t004_raincloudpaired_simulation/expected/log.txt new file mode 100644 index 0000000..50ae1f6 --- /dev/null +++ b/jenner-check/t004_raincloudpaired_simulation/expected/log.txt @@ -0,0 +1,22 @@ +Jenner 0.1.0 (Unlicensed - limited to 100 observations) +Get a license at https://jenneranalytics.com/license + +NOTE: Option OBS changed to 100. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT repeatf defined (2 ranges). +NOTE: FORMAT seqf defined (2 ranges). +NOTE: FORMAT trtf defined (2 ranges). +NOTE: FORMAT groupf defined (2 ranges). +NOTE: DATA raincloudtest + + +NOTE: Wrote raincloudtest (100 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=raincloudtest + +NOTE: PROC PRINT completed: 12 observations printed, 5 variables +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. diff --git a/jenner-check/t004_raincloudpaired_simulation/expected/output.txt b/jenner-check/t004_raincloudpaired_simulation/expected/output.txt new file mode 100644 index 0000000..d8c7ee8 --- /dev/null +++ b/jenner-check/t004_raincloudpaired_simulation/expected/output.txt @@ -0,0 +1,32 @@ + first 12 subjects: trt, repeat, response + + Obs TRT I REPNO USUBJID RESPONSE + 1 1 1 1 A001 26.2208097746 + 2 1 1 2 A001 46.2768087505 + 3 1 2 1 A002 16.5795019832 + 4 1 2 2 A002 20.3128653272 + 5 1 3 1 A003 11.3572251 + 6 1 3 2 A003 37.6702241349 + 7 1 4 1 A004 29.5538273767 + 8 1 4 2 A004 26.5999059639 + 9 1 5 1 A005 13.4960337179 + 10 1 5 2 A005 23.0789438764 + 11 1 6 1 A006 18.255821514 + 12 1 6 2 A006 33.2842112276 + +... 88 more observations (showing 12 of 100) + + summary by treatment and period + + The MEANS Procedure + + Analysis Variable : activity + + TRT REPNO Mean Std Dev Minimum Maximum + ------------------------------------------------------------------------------ + 1 1 20.254 4.649 11.357 29.554 + 1 2 32.349 7.458 20.313 46.277 + 2 1 31.448 7.937 22.910 54.852 + 2 2 16.815 2.528 12.181 21.739 + ------------------------------------------------------------------------------ + diff --git a/jenner-check/t004_raincloudpaired_simulation/meta.json b/jenner-check/t004_raincloudpaired_simulation/meta.json new file mode 100644 index 0000000..685835b --- /dev/null +++ b/jenner-check/t004_raincloudpaired_simulation/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t004_raincloudpaired_simulation", + "source_file": "example/raincloudpaired_example.sas", + "source_blob_sha": "4edb9b7229357d61889ebf6af1c15964681b2d4d", + "source_commit": "997ecef4f1103c473d261da6710f185a69a048d5", + "tier": "real_data", + "notes": "Lifted lines 11-56 of raincloudpaired_example.sas: four format definitions (repeatf, seqf, trtf, groupf), the call streaminit(1234) seed, the trt/i/repno nested do-loop with cell-specific rand('lognormal',...) parameters, the negative-clip guard, and the usubjid string construction. Inner loop reduced from 100 to 25 subjects so the 2x2 design x 25 subjects = 100 rows fits the OBS=100 cap exactly. Lognormal parameters and seed unchanged so the per-(trt,repno) means reproduce." +} diff --git a/jenner-check/t004_raincloudpaired_simulation/script.sas b/jenner-check/t004_raincloudpaired_simulation/script.sas new file mode 100644 index 0000000..963b111 --- /dev/null +++ b/jenner-check/t004_raincloudpaired_simulation/script.sas @@ -0,0 +1,65 @@ +*--------------------------------------------------------; +*raincloud paired data generation (from example/raincloudpaired_example.sas); +*--------------------------------------------------------; +/* Seeded simulation of a two-period crossover design. + Two treatments (placebo / drug A) measured at two repeats each. + Subject-level lognormal response distributions with hand-tuned + means and dispersions for each (trt, repno) cell. The %RainCloudPaired + macro consumes this layout to draw the paired distributions side by + side with subject connect lines. */ + +proc format; +value repeatf +1="period 1" +2="period 2"; + +value seqf +1="sequence A (placebo to drug A)" +2="sequence B (drug A to lacebo)" +; + +value trtf +1="Placebo" +2="Drug A" +; + +value groupf +1="Factor XXX (-)" +2="Factor XXX (+)"; + +run; + +data raincloudtest; +call streaminit(1234); +format repno repeatf. trt seqf.; +label response="activity"; + + +do trt=1 to 2; +do i=1 to 25; +do repno=1 to 2; + + usubjid="A" || strip(put(i,z3.0)); + + if trt=1 and repno=1 then response=rand("lognormal",3,0.2); + else if trt=1 and repno=2 then response=rand("lognormal",3.5,0.23); + else if trt=2 and repno=1 then response=rand("lognormal",3.4,0.21); + else if trt=2 and repno=2 then response=rand("lognormal",2.8,0.17); + + if response < 0 then response=0; + + output; +end; +end; +end; +run; + +proc print data=raincloudtest(obs=12); + title "first 12 subjects: trt, repeat, response"; +run; + +proc means data=raincloudtest mean std min max maxdec=3; + var response; + class trt repno; + title "summary by treatment and period"; +run; diff --git a/jenner-check/t005_mirrorhist_covid19_age/autoexec.sas b/jenner-check/t005_mirrorhist_covid19_age/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t005_mirrorhist_covid19_age/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t005_mirrorhist_covid19_age/expected.json b/jenner-check/t005_mirrorhist_covid19_age/expected.json new file mode 100644 index 0000000..7c9016d --- /dev/null +++ b/jenner-check/t005_mirrorhist_covid19_age/expected.json @@ -0,0 +1,21 @@ +{ + "_captured_at": "2026-05-11T23:29:00Z", + "_captured_run_id": "r_019e17e0230d72e3a20823050564fce2", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: FORMAT sexf defined (2 ranges).", + "NOTE: FORMAT agegrpf defined (12 ranges).", + "NOTE: Read 23 rows from DATALINES.", + "NOTE: Wrote covid19_tokyo_2021 (23 rows, 5 columns).", + "NOTE: PROC PRINT completed: 23 observations printed, 5 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t005_mirrorhist_covid19_age/expected/files.md b/jenner-check/t005_mirrorhist_covid19_age/expected/files.md new file mode 100644 index 0000000..ce2999d --- /dev/null +++ b/jenner-check/t005_mirrorhist_covid19_age/expected/files.md @@ -0,0 +1,16 @@ +# Captured run snapshot +Run ID: `r_019e17e0230d72e3a20823050564fce2` + +These URLs are tied to a specific Jenner run and **expire when the run is reaped from the workspace**. Re-running the bundle regenerates a fresh run with fresh URLs. + +## Files + +| Name | Size | Type | URL | +| --- | --- | --- | --- | +| `listing.txt` | 1187 | text/plain | [listing.txt](https://api.jenneranalytics.com/v1/run/r_019e17e0230d72e3a20823050564fce2/files/listing.txt) | + +## Datasets + +| Name | Rows | Columns | Preview | +| --- | --- | --- | --- | +| `covid19_tokyo_2021` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17e0230d72e3a20823050564fce2/datasets/covid19_tokyo_2021) | diff --git a/jenner-check/t005_mirrorhist_covid19_age/expected/log.txt b/jenner-check/t005_mirrorhist_covid19_age/expected/log.txt new file mode 100644 index 0000000..42b119c --- /dev/null +++ b/jenner-check/t005_mirrorhist_covid19_age/expected/log.txt @@ -0,0 +1,22 @@ +Jenner 0.1.0 (Unlicensed - limited to 100 observations) +Get a license at https://jenneranalytics.com/license + +NOTE: Option OBS changed to 100. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT sexf defined (2 ranges). +NOTE: FORMAT agegrpf defined (12 ranges). +NOTE: DATA covid19_tokyo_2021 + +NOTE: Processing inline DATALINES (23 lines) + +NOTE: Read 23 rows from DATALINES. +NOTE: Wrote covid19_tokyo_2021 (23 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=covid19_tokyo_2021 + +NOTE: PROC PRINT completed: 23 observations printed, 5 variables +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. diff --git a/jenner-check/t005_mirrorhist_covid19_age/expected/output.txt b/jenner-check/t005_mirrorhist_covid19_age/expected/output.txt new file mode 100644 index 0000000..f7318e4 --- /dev/null +++ b/jenner-check/t005_mirrorhist_covid19_age/expected/output.txt @@ -0,0 +1,39 @@ + covid19_tokyo_2021: age band x sex x patient count + + Obs SEXC AGEC PATIENTS SEX AGEGRP + 1 Male <10 8011 1 1 + 2 Male 10s 13821 1 2 + 3 Male 20s 49695 1 3 + 4 Male 30s 37503 1 4 + 5 Male 40s 30862 1 5 + 6 Male 50s 21952 1 6 + 7 Male 60s 8544 1 7 + 8 Male 70s 5202 1 8 + 9 Male 80s 2930 1 9 + 10 Male 90s 654 1 10 + 11 Male >100 22 1 11 + 12 Male unknown 4 1 99 + 13 Female <10 7519 2 1 + 14 Female 10s 12359 2 2 + 15 Female 20s 43162 2 3 + 16 Female 30s 25594 2 4 + 17 Female 40s 20121 2 5 + 18 Female 50s 15931 2 6 + 19 Female 60s 6238 2 7 + 20 Female 70s 4872 2 8 + 21 Female 80s 4196 2 9 + 22 Female 90s 1765 2 10 + 23 Female >100 116 2 11 + + totals by sex + + The MEANS Procedure + + Analysis Variable : PATIENTS + + SEX Sum + ------------------------ + 1 179200.0000000 + 2 141873.0000000 + ------------------------ + diff --git a/jenner-check/t005_mirrorhist_covid19_age/meta.json b/jenner-check/t005_mirrorhist_covid19_age/meta.json new file mode 100644 index 0000000..671c1f8 --- /dev/null +++ b/jenner-check/t005_mirrorhist_covid19_age/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t005_mirrorhist_covid19_age", + "source_file": "example/mirrored_histogram_example.sas", + "source_blob_sha": "7f93c4d940d212198ab8e886a3e61a0a3256077f", + "source_commit": "997ecef4f1103c473d261da6710f185a69a048d5", + "tier": "real_data", + "notes": "Lifted lines 91-165 of mirrored_histogram_example.sas verbatim: sexf and agegrpf format definitions, the length statement with sexc $10 agec $20, the two select/when blocks mapping string -> numeric, and the 23 rows of Tokyo 2021 COVID case datalines. PROC PRINT and PROC MEANS substituted in place of the macro call to verify the categorical mapping landed correctly." +} diff --git a/jenner-check/t005_mirrorhist_covid19_age/script.sas b/jenner-check/t005_mirrorhist_covid19_age/script.sas new file mode 100644 index 0000000..58cdf81 --- /dev/null +++ b/jenner-check/t005_mirrorhist_covid19_age/script.sas @@ -0,0 +1,95 @@ +*-------------------------------------------; +*covid19 age-by-sex categorical prep (from example/mirrored_histogram_example.sas); +*-------------------------------------------; +/* Public COVID-19 case counts for Tokyo 2021, broken out by age band + and sex. The string age bands ("<10", "10s", ..., ">100", "unknown") + are mapped via select/when to numeric codes whose order matches the + agegrpf format. The %MirroredHist macro consumes this to draw a + horizontal mirrored age pyramid by sex. */ + +proc format; +value sexf +1="Male" +2="Female"; + +value agegrpf +1="<10" +2="10s" +3="20s" +4="30s" +5="40s" +6="50s" +7="60s" +8="70s" +9="80s" +10="90s" +11=">100" +99="unknown" +; + +run; + +data covid19_tokyo_2021; + +infile datalines delimiter=","; +length sexc $10 agec $20 patients 8; +format sex sexf. agegrp agegrpf.; +input sexc $ agec $ patients; + +select (sexc); + when ("Male") sex=1; + when("Female")sex=2; +end; + +select(agec); + when("<10") agegrp=1; + when("10s") agegrp=2; + when("20s") agegrp=3; + when("30s") agegrp=4; + when("40s") agegrp=5; + when("50s") agegrp=6; + when("60s") agegrp=7; + when("70s") agegrp=8; + when("80s") agegrp=9; + when("90s") agegrp=10; + when(">100") agegrp=11; + when("unknown")agegrp=99; +end; + +datalines; +Male,<10,8011 +Male,10s,13821 +Male,20s,49695 +Male,30s,37503 +Male,40s,30862 +Male,50s,21952 +Male,60s,8544 +Male,70s,5202 +Male,80s,2930 +Male,90s,654 +Male,>100,22 +Male,unknown,4 +Female,<10,7519 +Female,10s,12359 +Female,20s,43162 +Female,30s,25594 +Female,40s,20121 +Female,50s,15931 +Female,60s,6238 +Female,70s,4872 +Female,80s,4196 +Female,90s,1765 +Female,>100,116 +; +run; + + +proc print data=covid19_tokyo_2021; + title "covid19_tokyo_2021: age band x sex x patient count"; +run; + +proc means data=covid19_tokyo_2021 sum; + var patients; + class sex; + title "totals by sex"; +run; diff --git a/jenner-check/t006_drugswitch_freq_crosstab/autoexec.sas b/jenner-check/t006_drugswitch_freq_crosstab/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t006_drugswitch_freq_crosstab/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t006_drugswitch_freq_crosstab/expected.json b/jenner-check/t006_drugswitch_freq_crosstab/expected.json new file mode 100644 index 0000000..dfb92f8 --- /dev/null +++ b/jenner-check/t006_drugswitch_freq_crosstab/expected.json @@ -0,0 +1,21 @@ +{ + "_captured_at": "2026-05-11T23:30:00Z", + "_captured_run_id": "r_019e17e0af457cb0b82ddacb8e45fe0c", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: FORMAT druglist defined (5 ranges).", + "NOTE: FORMAT timef defined (4 ranges).", + "NOTE: Read 30 rows from DATALINES.", + "NOTE: Wrote drug_switch (30 rows, 5 columns).", + "NOTE: PROC PRINT completed: 10 observations printed, 5 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t006_drugswitch_freq_crosstab/expected/files.md b/jenner-check/t006_drugswitch_freq_crosstab/expected/files.md new file mode 100644 index 0000000..531d167 --- /dev/null +++ b/jenner-check/t006_drugswitch_freq_crosstab/expected/files.md @@ -0,0 +1,19 @@ +# Captured run snapshot +Run ID: `r_019e17e0af457cb0b82ddacb8e45fe0c` + +These URLs are tied to a specific Jenner run and **expire when the run is reaped from the workspace**. Re-running the bundle regenerates a fresh run with fresh URLs. + +## Files + +| Name | Size | Type | URL | +| --- | --- | --- | --- | +| `listing.txt` | 2209 | text/plain | [listing.txt](https://api.jenneranalytics.com/v1/run/r_019e17e0af457cb0b82ddacb8e45fe0c/files/listing.txt) | +| `ods_output/freq_day0.svg` | 8775 | image/svg+xml | [ods_output/freq_day0.svg](https://api.jenneranalytics.com/v1/run/r_019e17e0af457cb0b82ddacb8e45fe0c/files/ods_output/freq_day0.svg) | +| `ods_output/freq_day90.svg` | 13567 | image/svg+xml | [ods_output/freq_day90.svg](https://api.jenneranalytics.com/v1/run/r_019e17e0af457cb0b82ddacb8e45fe0c/files/ods_output/freq_day90.svg) | +| `ods_output/freq_mosaic_day0_day90.svg` | 11673 | image/svg+xml | [ods_output/freq_mosaic_day0_day90.svg](https://api.jenneranalytics.com/v1/run/r_019e17e0af457cb0b82ddacb8e45fe0c/files/ods_output/freq_mosaic_day0_day90.svg) | + +## Datasets + +| Name | Rows | Columns | Preview | +| --- | --- | --- | --- | +| `drug_switch` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17e0af457cb0b82ddacb8e45fe0c/datasets/drug_switch) | diff --git a/jenner-check/t006_drugswitch_freq_crosstab/expected/log.txt b/jenner-check/t006_drugswitch_freq_crosstab/expected/log.txt new file mode 100644 index 0000000..e1039a1 --- /dev/null +++ b/jenner-check/t006_drugswitch_freq_crosstab/expected/log.txt @@ -0,0 +1,27 @@ +Jenner 0.1.0 (Unlicensed - limited to 100 observations) +Get a license at https://jenneranalytics.com/license + +NOTE: Option OBS changed to 100. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT druglist defined (5 ranges). +NOTE: FORMAT timef defined (4 ranges). +NOTE: DATA drug_switch + +NOTE: Processing inline DATALINES (30 lines) + +NOTE: Read 30 rows from DATALINES. +NOTE: Wrote drug_switch (30 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=drug_switch + +NOTE: PROC PRINT completed: 10 observations printed, 5 variables +NOTE: PROC FREQ +NOTE: ODS plot written: freq_day0.spec.json +NOTE: ODS plot written: freq_day90.spec.json +NOTE: PROC FREQ statement used. +NOTE: PROC FREQ +NOTE: ODS plot written: freq_mosaic_day0_day90.spec.json +NOTE: PROC FREQ statement used. diff --git a/jenner-check/t006_drugswitch_freq_crosstab/expected/output.txt b/jenner-check/t006_drugswitch_freq_crosstab/expected/output.txt new file mode 100644 index 0000000..9517689 --- /dev/null +++ b/jenner-check/t006_drugswitch_freq_crosstab/expected/output.txt @@ -0,0 +1,48 @@ + drug_switch: first 10 subjects (Day0 -> Day90) + + Obs USUBJID DAY0 DAY30 DAY60 DAY90 + 1 A001 1 1 1 3 + 2 A002 1 1 1 4 + 3 A003 1 1 1 4 + 4 A004 1 1 1 4 + 5 A005 1 2 1 4 + 6 A006 1 3 1 4 + 7 A007 1 3 1 4 + 8 A008 1 4 1 99 + 9 A009 1 4 1 99 + 10 A010 1 1 2 2 + +... 20 more observations (showing 10 of 30) + + univariate frequency at Day 0 and Day 90 + + The FREQ Procedure + + Cumulative Cumulative +DAY0 Frequency Percent Frequency Percent +------------------------------------------------------------- +1 23 76.67 23 76.67 +2 7 23.33 30 100.00 + + Cumulative Cumulative +DAY90 Frequency Percent Frequency Percent +-------------------------------------------------------------- +1 2 6.67 2 6.67 +2 3 10.00 5 16.67 +3 7 23.33 12 40.00 +4 15 50.00 27 90.00 +99 3 10.00 30 100.00 + Day 0 x Day 90 crosstab (the source -> sink flow) + + The FREQ Procedure + +Table of DAY0 by DAY90 + +DAY0 | 1 | 2 | 3 | 4 | 99 | Total +-----+-----------+-----------+-----------+-----------+-----------+----------- +1 | 1 | 2 | 4 | 13 | 3 | 23 +-----+-----------+-----------+-----------+-----------+-----------+----------- +2 | 1 | 1 | 3 | 2 | 0 | 7 +-----+-----------+-----------+-----------+-----------+-----------+----------- +Total | 2 | 3 | 7 | 15 | 3 | 30 + diff --git a/jenner-check/t006_drugswitch_freq_crosstab/meta.json b/jenner-check/t006_drugswitch_freq_crosstab/meta.json new file mode 100644 index 0000000..1763a5f --- /dev/null +++ b/jenner-check/t006_drugswitch_freq_crosstab/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t006_drugswitch_freq_crosstab", + "source_file": "example/sankey_example.sas", + "source_blob_sha": "fed68b46c3a32134ef1c0c2c0308ca7e41520f27", + "source_commit": "997ecef4f1103c473d261da6710f185a69a048d5", + "tier": "real_data", + "notes": "Lifted lines 155-223 of sankey_example.sas: the druglist and timef format definitions, length statement with usubjid $10 and four 8-byte Day variables, the input layout, and the first 30 datalines rows. Substituted PROC PRINT and PROC FREQ (univariate plus Day0xDay90 crosstab) where the original would call the %sankey focus=(...) examples. All data, formats, and length statement verbatim." +} diff --git a/jenner-check/t006_drugswitch_freq_crosstab/script.sas b/jenner-check/t006_drugswitch_freq_crosstab/script.sas new file mode 100644 index 0000000..8705b22 --- /dev/null +++ b/jenner-check/t006_drugswitch_freq_crosstab/script.sas @@ -0,0 +1,75 @@ +*--------------------------------------------------------; +*drug-switch data prep (second dataset in example/sankey_example.sas); +*--------------------------------------------------------; +/* Drug switching with a "lost to follow-up" sink (code 99) at four + visits. Each subject (usubjid) has Day0, Day30, Day60, Day90 drug + codes. The %sankey focus=... parameter examples downstream filter + this dataset to highlight flows of interest. */ + +proc format; +value druglist +1="Drug A" +2="Drug B" +3="Drug C" +4="Drug D" +99="Lost to follow-up"; + +value timef +1="Day 0" +2="Day 30" +3="Day 60" +4="day 90" +; +run; + +data drug_switch; +length usubjid $10 Day0 Day30 Day60 Day90 8; +format Day0 Day30 Day60 Day90 druglist.; +input usubjid $ Day0 Day30 Day60 Day90; +datalines; +A001 1 1 1 3 +A002 1 1 1 4 +A003 1 1 1 4 +A004 1 1 1 4 +A005 1 2 1 4 +A006 1 3 1 4 +A007 1 3 1 4 +A008 1 4 1 99 +A009 1 4 1 99 +A010 1 1 2 2 +A011 1 1 2 3 +A012 1 1 2 3 +A013 1 1 2 3 +A014 1 2 2 4 +A015 1 2 2 4 +A016 1 3 2 4 +A017 1 1 3 1 +A018 1 1 3 2 +A019 1 2 3 4 +A020 1 2 3 4 +A021 1 2 3 4 +A022 1 3 3 4 +A023 1 3 3 99 +A024 2 4 2 4 +A025 2 1 3 3 +A026 2 1 3 3 +A027 2 1 4 1 +A028 2 1 4 2 +A029 2 2 4 3 +A030 2 2 4 4 +; +run; + +proc print data=drug_switch(obs=10); + title "drug_switch: first 10 subjects (Day0 -> Day90)"; +run; + +proc freq data=drug_switch; + tables Day0 Day90; + title "univariate frequency at Day 0 and Day 90"; +run; + +proc freq data=drug_switch; + tables Day0*Day90 / nopercent norow nocol; + title "Day 0 x Day 90 crosstab (the source -> sink flow)"; +run; diff --git a/jenner-check/t007_raincloudtest2_retain_normal/autoexec.sas b/jenner-check/t007_raincloudtest2_retain_normal/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t007_raincloudtest2_retain_normal/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t007_raincloudtest2_retain_normal/expected.json b/jenner-check/t007_raincloudtest2_retain_normal/expected.json new file mode 100644 index 0000000..85df917 --- /dev/null +++ b/jenner-check/t007_raincloudtest2_retain_normal/expected.json @@ -0,0 +1,20 @@ +{ + "_captured_at": "2026-05-11T23:31:00Z", + "_captured_run_id": "r_019e17e179147612a4560f04d5ca30c0", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: FORMAT trtf defined (2 ranges).", + "NOTE: FORMAT groupf defined (2 ranges).", + "NOTE: Wrote raincloudtest2 (100 rows, 6 columns).", + "NOTE: PROC PRINT completed: 12 observations printed, 6 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} diff --git a/jenner-check/t007_raincloudtest2_retain_normal/expected/files.md b/jenner-check/t007_raincloudtest2_retain_normal/expected/files.md new file mode 100644 index 0000000..7057347 --- /dev/null +++ b/jenner-check/t007_raincloudtest2_retain_normal/expected/files.md @@ -0,0 +1,16 @@ +# Captured run snapshot +Run ID: `r_019e17e179147612a4560f04d5ca30c0` + +These URLs are tied to a specific Jenner run and **expire when the run is reaped from the workspace**. Re-running the bundle regenerates a fresh run with fresh URLs. + +## Files + +| Name | Size | Type | URL | +| --- | --- | --- | --- | +| `listing.txt` | 753 | text/plain | [listing.txt](https://api.jenneranalytics.com/v1/run/r_019e17e179147612a4560f04d5ca30c0/files/listing.txt) | + +## Datasets + +| Name | Rows | Columns | Preview | +| --- | --- | --- | --- | +| `raincloudtest2` | 0 | [] | [preview](https://api.jenneranalytics.com/v1/run/r_019e17e179147612a4560f04d5ca30c0/datasets/raincloudtest2) | diff --git a/jenner-check/t007_raincloudtest2_retain_normal/expected/log.txt b/jenner-check/t007_raincloudtest2_retain_normal/expected/log.txt new file mode 100644 index 0000000..905eaa1 --- /dev/null +++ b/jenner-check/t007_raincloudtest2_retain_normal/expected/log.txt @@ -0,0 +1,20 @@ +Jenner 0.1.0 (Unlicensed - limited to 100 observations) +Get a license at https://jenneranalytics.com/license + +NOTE: Option OBS changed to 100. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT trtf defined (2 ranges). +NOTE: FORMAT groupf defined (2 ranges). +NOTE: DATA raincloudtest2 + + +NOTE: Wrote raincloudtest2 (100 rows, 6 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=raincloudtest2 + +NOTE: PROC PRINT completed: 12 observations printed, 6 variables +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. diff --git a/jenner-check/t007_raincloudtest2_retain_normal/expected/output.txt b/jenner-check/t007_raincloudtest2_retain_normal/expected/output.txt new file mode 100644 index 0000000..6fabbde --- /dev/null +++ b/jenner-check/t007_raincloudtest2_retain_normal/expected/output.txt @@ -0,0 +1,32 @@ + first 12 subjects: cat, type, trt, response + + Obs CAT TYPE I TRT USUBJID RESPONSE + 1 FAS 1 1 1 A001 0.9999150213 + 2 FAS 1 1 2 A001 1.6637401573 + 3 FAS 1 2 1 A002 0.656125334 + 4 FAS 1 2 2 A002 0.7687548318 + 5 FAS 1 3 1 A003 0.3723905856 + 6 FAS 1 3 2 A003 1.4400760561 + 7 FAS 1 4 1 A004 1.0896599431 + 8 FAS 1 4 2 A004 1.0618561746 + 9 FAS 1 5 1 A005 0.5017968827 + 10 FAS 1 5 2 A005 0.9075224797 + 11 FAS 1 6 1 A006 0.7283630122 + 12 FAS 1 6 2 A006 1.30552516 + +... 88 more observations (showing 12 of 100) + + summary by type x treatment + + The MEANS Procedure + + Analysis Variable : log(AUC) + + TYPE TRT Mean Std Dev Minimum Maximum + ------------------------------------------------------------------------------ + 1 1 0.786 0.181 0.372 1.090 + 1 2 1.246 0.260 0.769 1.664 + 2 1 1.017 0.186 0.783 1.489 + 2 2 2.115 0.197 1.712 2.461 + ------------------------------------------------------------------------------ + diff --git a/jenner-check/t007_raincloudtest2_retain_normal/meta.json b/jenner-check/t007_raincloudtest2_retain_normal/meta.json new file mode 100644 index 0000000..046256c --- /dev/null +++ b/jenner-check/t007_raincloudtest2_retain_normal/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t007_raincloudtest2_retain_normal", + "source_file": "example/raincloudpaired_example.sas", + "source_blob_sha": "4edb9b7229357d61889ebf6af1c15964681b2d4d", + "source_commit": "997ecef4f1103c473d261da6710f185a69a048d5", + "tier": "real_data", + "notes": "Lifted lines 97-120 of raincloudpaired_example.sas: the trtf and groupf format definitions, call streaminit(1234), the cat=\"FAS\" pre-loop assignment (implicit retain on a string literal), the type/i/trt nested do-loop, and the four rand('normal',mean,sd) cell parameters. Inner loop reduced from 100 to 25 subjects so 2 types x 25 subjects x 2 trts = 100 rows fits the OBS=100 cap exactly. Means and SDs match the source. Substituted PROC PRINT and PROC MEANS in place of the %RainCloudPaired call." +} diff --git a/jenner-check/t007_raincloudtest2_retain_normal/script.sas b/jenner-check/t007_raincloudtest2_retain_normal/script.sas new file mode 100644 index 0000000..941badb --- /dev/null +++ b/jenner-check/t007_raincloudtest2_retain_normal/script.sas @@ -0,0 +1,52 @@ +*--------------------------------------------------------; +*grouped paired raincloud data generation (second dataset in example/raincloudpaired_example.sas); +*--------------------------------------------------------; +/* Two-population (FAS) by type x treatment design with normal-distributed + responses. The population label "FAS" is set once and carried into + every output row (implicit retain on character literal). %RainCloudPaired + downstream uses cat= as the x-axis category and group=type for color. */ + +proc format; +value trtf +1="Placebo" +2="Drug A" +; + +value groupf +1="Factor XXX (-)" +2="Factor XXX (+)"; +run; + +data raincloudtest2; + +call streaminit(1234); +format trt trtf. type groupf.; +label response="log(AUC)" cat="population"; + +cat="FAS"; +do type=1 to 2; +do i=1 to 25; +do trt=1 to 2; + + usubjid="A" || strip(put(i,z3.0)); + + if type=1 and trt=1 then response=rand("normal",0.8,0.15); + else if type=1 and trt=2 then response=rand("normal",1.3,0.25); + else if type=2 and trt=1 then response=rand("normal",1.0,0.17); + else if type=2 and trt=2 then response=rand("normal",2.1 ,0.22); + + output; +end; +end; +end; +run; + +proc print data=raincloudtest2(obs=12); + title "first 12 subjects: cat, type, trt, response"; +run; + +proc means data=raincloudtest2 mean std min max maxdec=3; + var response; + class type trt; + title "summary by type x treatment"; +run;