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.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_make_formats/autoexec.sas b/jenner-check/t001_make_formats/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t001_make_formats/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t001_make_formats/expected.json b/jenner-check/t001_make_formats/expected.json new file mode 100644 index 0000000..6ef1732 --- /dev/null +++ b/jenner-check/t001_make_formats/expected.json @@ -0,0 +1,19 @@ +{ + "_captured_at": "2026-06-17T13:07:26.863017Z", + "_captured_run_id": "r_019ed5a6c0cb72c18aeb740c0957845a", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "FORMAT $nspnbhd defined (18 ranges).", + "Wrote parcel_labels (6 rows, 4 columns).", + "PROC PRINT completed: 6 observations printed, 4 variables" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} \ No newline at end of file diff --git a/jenner-check/t001_make_formats/expected/files.md b/jenner-check/t001_make_formats/expected/files.md new file mode 100644 index 0000000..ab19699 --- /dev/null +++ b/jenner-check/t001_make_formats/expected/files.md @@ -0,0 +1,14 @@ +These URLs point to artifacts from a specific Jenner run and expire when that run is reaped; re-running the bundle regenerates them. + + +## Files + +| name | content_type | size_bytes | url | +|------|--------------|-----------|-----| +| listing.txt | text/plain | 374 | https://api.jenneranalytics.com/v1/run/r_019ed5a6c0cb72c18aeb740c0957845a/files/listing.txt?token=4e7e7428b9df416c8c03549864830805 | + +## Datasets + +| name | rows | columns | preview_url | +|------|------|---------|-------------| +| parcel_labels | 6 | 4 | https://api.jenneranalytics.com/v1/run/r_019ed5a6c0cb72c18aeb740c0957845a/datasets/parcel_labels?token=4e7e7428b9df416c8c03549864830805 | diff --git a/jenner-check/t001_make_formats/expected/log.txt b/jenner-check/t001_make_formats/expected/log.txt new file mode 100644 index 0000000..fe7a2ee --- /dev/null +++ b/jenner-check/t001_make_formats/expected/log.txt @@ -0,0 +1,21 @@ +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 $newptyp defined (6 ranges). +NOTE: FORMAT $newptyb defined (6 ranges). +NOTE: FORMAT $nspnbhd defined (18 ranges). +NOTE: DATA parcel_labels + +NOTE: Processing inline DATALINES (6 lines) + +NOTE: Read 6 rows from DATALINES. +NOTE: Wrote parcel_labels (6 rows, 4 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=parcel_labels + +NOTE: PROC PRINT completed: 6 observations printed, 4 variables diff --git a/jenner-check/t001_make_formats/expected/output.txt b/jenner-check/t001_make_formats/expected/output.txt new file mode 100644 index 0000000..d966102 --- /dev/null +++ b/jenner-check/t001_make_formats/expected/output.txt @@ -0,0 +1,10 @@ + DHCD parcel recodes via $nspnbhd. and $newptyp. + + geo2000 neighborhood proptype property_type +11001007503 Anacostia 10 10 +11001007803 Deanwood 13 13 +11001007901 Trinidad 14 14 +11001008500 Trinidad 15 15 +11001009904 Deanwood 11 11 +99999999999 12 12 + diff --git a/jenner-check/t001_make_formats/meta.json b/jenner-check/t001_make_formats/meta.json new file mode 100644 index 0000000..89501e2 --- /dev/null +++ b/jenner-check/t001_make_formats/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t001_make_formats", + "source_file": "Prog/Make_formats.sas", + "source_blob_sha": "0ab21b0b938693442b4d85b04124cc7deb760542", + "source_commit": "ac221b855c1daf69fc24fe67326506b65c76799a", + "tier": "real_data", + "notes": "PROC FORMAT VALUE statements ($nspnbhd, $newptyp, $newptyb) verbatim from source; library=DHCD dropped so formats build in WORK; recodes shown via PUT() in a DATA step + PROC PRINT (same PUT pattern the repo uses in NSP_forecl_address.sas)." +} diff --git a/jenner-check/t001_make_formats/script.sas b/jenner-check/t001_make_formats/script.sas new file mode 100644 index 0000000..ad3954f --- /dev/null +++ b/jenner-check/t001_make_formats/script.sas @@ -0,0 +1,73 @@ +/************************************************************************** + Bundle: t001_make_formats + Source: Prog/Make_formats.sas (NeighborhoodInfoDC/DHCD) + Original: P. Tatian, NeighborhoodInfo DC, 06/17/09 + + The PROC FORMAT VALUE statements below are reproduced verbatim from the + repository's Make_formats.sas. The only adaptation: the original writes + the formats to a permanent catalog (library=DHCD, defined by the DCData + macro framework); here they build in the default (WORK) catalog so the + program is self-contained. + + A short DATA step applies the $newptyp and $nspnbhd formats with PUT() -- + the same recoding pattern the repository uses elsewhere (e.g. + Prog/NSP_forecl_address.sas: `nbrhd = put( geo2000, $nbrhd. );`) -- so the + neighborhood and property-type labels appear directly in the output. +**************************************************************************/ + +proc format; + value $newptyp + 10 = 'Single-family homes' + 11 = 'Condominiums' + 12 = 'Cooperative buildings' + 13 = 'Rental buildings' + 14 = 'Rental buildings (< 5 apts.)' + 15 = 'Rental buildings (5+ apts.)'; + value $newptyb + 10 = 'Single-family homes' + 11 = 'Condominium buildings' + 12 = 'Cooperative buildings' + 13 = 'Rental buildings' + 14 = 'Rental buildings (< 5 apts.)' + 15 = 'Rental buildings (5+ apts.)'; + value $nspnbhd + '11001007503' = 'Anacostia' + '11001007504' = 'Anacostia' + '11001007601' = 'Anacostia' + '11001007803' = 'Deanwood' + '11001007806' = 'Deanwood' + '11001007807' = 'Deanwood' + '11001007808' = 'Deanwood' + '11001007809' = 'Deanwood' + '11001009904' = 'Deanwood' + '11001009905' = 'Deanwood' + '11001009906' = 'Deanwood' + '11001007901' = 'Trinidad' + '11001007903' = 'Trinidad' + '11001008500' = 'Trinidad' + '11001008802' = 'Trinidad' + '11001008803' = 'Trinidad' + '11001008904' = 'Trinidad' + other = ' '; +run; + +** Apply the repository formats to a few sample parcels so the recodes show **; +data parcel_labels; + length geo2000 $ 11 proptype $ 2 neighborhood $ 40 property_type $ 40; + input geo2000 $ proptype $; + neighborhood = put( geo2000, $nspnbhd. ); + property_type = put( proptype, $newptyp. ); + datalines; +11001007503 10 +11001007803 13 +11001007901 14 +11001008500 15 +11001009904 11 +99999999999 12 +; +run; + +proc print data=parcel_labels noobs; + var geo2000 neighborhood proptype property_type; + title 'DHCD parcel recodes via $nspnbhd. and $newptyp.'; +run; diff --git a/jenner-check/t002_nsp_forecl_prop_type/autoexec.sas b/jenner-check/t002_nsp_forecl_prop_type/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t002_nsp_forecl_prop_type/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t002_nsp_forecl_prop_type/expected.json b/jenner-check/t002_nsp_forecl_prop_type/expected.json new file mode 100644 index 0000000..c238256 --- /dev/null +++ b/jenner-check/t002_nsp_forecl_prop_type/expected.json @@ -0,0 +1,19 @@ +{ + "_captured_at": "2026-06-17T13:07:26.880167Z", + "_captured_run_id": "r_019ed5a9fc1a734394937d02c67f269b", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Wrote NSP_forecl_prop_type (10 rows, 6 columns).", + "FORMAT $nbrhd defined (20 ranges).", + "PROC TABULATE" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} \ No newline at end of file diff --git a/jenner-check/t002_nsp_forecl_prop_type/expected/files.md b/jenner-check/t002_nsp_forecl_prop_type/expected/files.md new file mode 100644 index 0000000..ea69e2c --- /dev/null +++ b/jenner-check/t002_nsp_forecl_prop_type/expected/files.md @@ -0,0 +1,15 @@ +These URLs point to artifacts from a specific Jenner run and expire when that run is reaped; re-running the bundle regenerates them. + + +## Files + +| name | content_type | size_bytes | url | +|------|--------------|-----------|-----| +| listing.txt | text/plain | 996 | https://api.jenneranalytics.com/v1/run/r_019ed5a9fc1a734394937d02c67f269b/files/listing.txt?token=1a58cb930bd34a37bf37e7677a944bcd | + +## Datasets + +| name | rows | columns | preview_url | +|------|------|---------|-------------| +| nsp_forecl_prop_type | 10 | 6 | https://api.jenneranalytics.com/v1/run/r_019ed5a9fc1a734394937d02c67f269b/datasets/nsp_forecl_prop_type?token=1a58cb930bd34a37bf37e7677a944bcd | +| nsp_input | 10 | 5 | https://api.jenneranalytics.com/v1/run/r_019ed5a9fc1a734394937d02c67f269b/datasets/nsp_input?token=1a58cb930bd34a37bf37e7677a944bcd | diff --git a/jenner-check/t002_nsp_forecl_prop_type/expected/log.txt b/jenner-check/t002_nsp_forecl_prop_type/expected/log.txt new file mode 100644 index 0000000..606722d --- /dev/null +++ b/jenner-check/t002_nsp_forecl_prop_type/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: DATA NSP_input + +NOTE: Processing inline DATALINES (10 lines) + +NOTE: Read 10 rows from DATALINES. +NOTE: Wrote NSP_input (10 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA NSP_forecl_prop_type + + +NOTE: Read 10 rows from NSP_input. +NOTE: Wrote NSP_forecl_prop_type (10 rows, 6 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT $newptyp defined (5 ranges). +NOTE: FORMAT $nbrhd defined (20 ranges). +NOTE: PROC TABULATE diff --git a/jenner-check/t002_nsp_forecl_prop_type/expected/output.txt b/jenner-check/t002_nsp_forecl_prop_type/expected/output.txt new file mode 100644 index 0000000..9c9f0f4 --- /dev/null +++ b/jenner-check/t002_nsp_forecl_prop_type/expected/output.txt @@ -0,0 +1,13 @@ +---------------------------------------------------------------------------------- +| | Properties in Foreclosure, End of 2009-Q1 | +| |-----------------------------------------------------------------------| +| | Properties | % | Properties | % | Properties | % | +|--------+------------+----------+------------+----------+------------+----------| +|Total | 2| 100.0| 4| 100.0| 3| 100.0| +|10 | .| 0.0| 1| 25.0| .| 0.0| +|11 | .| 0.0| .| 0.0| 1| 33.3| +|12 | .| 0.0| 1| 25.0| .| 0.0| +|14 | 1| 50.0| 1| 25.0| 1| 33.3| +|15 | 1| 50.0| 1| 25.0| 1| 33.3| +---------------------------------------------------------------------------------- + diff --git a/jenner-check/t002_nsp_forecl_prop_type/meta.json b/jenner-check/t002_nsp_forecl_prop_type/meta.json new file mode 100644 index 0000000..3b7e3ba --- /dev/null +++ b/jenner-check/t002_nsp_forecl_prop_type/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t002_nsp_forecl_prop_type", + "source_file": "Prog/NSP_forecl_prop_type.sas", + "source_blob_sha": "0e32a96b9b282b099544ddff4f749a44d430748e", + "source_commit": "ac221b855c1daf69fc24fe67326506b65c76799a", + "tier": "real_data", + "notes": "DATA-step proptype recode (if/then, usecode in-list) + PROC TABULATE crosstab verbatim; external HsngMon.Foreclosures_qtr_2009_2 replaced by inline NSP_input sample with the columns read; $nbrhd quoted-key format and WHERE put() filter intact." +} diff --git a/jenner-check/t002_nsp_forecl_prop_type/script.sas b/jenner-check/t002_nsp_forecl_prop_type/script.sas new file mode 100644 index 0000000..1aa8095 --- /dev/null +++ b/jenner-check/t002_nsp_forecl_prop_type/script.sas @@ -0,0 +1,92 @@ +/************************************************************************** + Bundle: t002_nsp_forecl_prop_type + Source: Prog/NSP_forecl_prop_type.sas (NeighborhoodInfoDC/DHCD) + Original: P. Tatian, NeighborhoodInfo DC, 05/19/09 + + Description (verbatim from source): Foreclosures by property type for + NSP census tracts. + + Adaptation: the original reads HsngMon.Foreclosures_qtr_2009_2 -- an + external SAS library defined by the DCData macro framework. Here that + input is supplied as a small inline sample (NSP_input) with the columns + the program reads: ui_proptype, usecode, report_dt, in_foreclosure_end, + and geo2000 (an 11-character Census-tract GEOID). The DATA-step recode + (rental buildings split into < 5 / 5+ apts. by usecode) and the + PROC TABULATE crosstab are reproduced exactly as written. +**************************************************************************/ + +** Sample standing in for HsngMon.Foreclosures_qtr_2009_2 **; +data NSP_input; + length geo2000 $ 11 ui_proptype usecode $ 3; + input geo2000 $ ui_proptype $ usecode $ in_foreclosure_end report_dt :date9.; + format report_dt date9.; + datalines; +11001007503 13 023 1 01jan2009 +11001007503 13 016 1 01jan2009 +11001007803 10 011 1 01jan2009 +11001007901 13 024 1 01jan2009 +11001008500 11 012 1 01jan2009 +11001008500 13 016 1 01jan2009 +11001009904 12 013 1 01jan2009 +11001007806 13 016 1 01jan2009 +11001007503 10 011 0 01jan2009 +11001007803 13 023 1 01jan2009 +; +run; + +data NSP_forecl_prop_type; + + set NSP_input; + + length new_proptype $ 2; + + if ui_proptype = '13' then do; + if usecode in ( '023', '024' ) then new_proptype = '14'; + else new_proptype = '15'; + end; + else new_proptype = ui_proptype; + +run; + +proc format; + value $newptyp + 10 = 'Single-family homes' + 11 = 'Condominium' + 12 = 'Cooperative building' + 14 = 'Rental building (< 5 apts.)' + 15 = 'Rental building (5+ apts.)'; + value $nbrhd + '11001007503' = 'Anacostia' + '11001007504' = 'Anacostia' + '11001007601' = 'Anacostia' + '11001007803' = 'Deanwood' + '11001007806' = 'Deanwood' + '11001007807' = 'Deanwood' + '11001007808' = 'Deanwood' + '11001007809' = 'Deanwood' + '11001009904' = 'Deanwood' + '11001009905' = 'Deanwood' + '11001009906' = 'Deanwood' + '11001007901' = 'Trinidad' + '11001007903' = 'Trinidad' + '11001008500' = 'Trinidad' + '11001008802' = 'Trinidad' + '11001008803' = 'Trinidad' + '11001008804' = 'Trinidad' + '11001008903' = 'Trinidad' + '11001008904' = 'Trinidad' + other = ' '; + +proc tabulate data=NSP_forecl_prop_type format=comma10.0 noseps missing; + where report_dt = '01jan2009'd and in_foreclosure_end and + put( geo2000, $nbrhd. ) ~= ''; + class new_proptype geo2000; + table + /** Rows **/ + all='Total' new_proptype=' ', + /** Columns **/ + geo2000='Properties in Foreclosure, End of 2009-Q1' * ( n='Properties' colpctn='%'*f=comma10.1 ) + ; + format new_proptype $newptyp. geo2000 $nbrhd.; + +run; diff --git a/jenner-check/t003_rc_descriptive_tab/autoexec.sas b/jenner-check/t003_rc_descriptive_tab/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t003_rc_descriptive_tab/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t003_rc_descriptive_tab/expected.json b/jenner-check/t003_rc_descriptive_tab/expected.json new file mode 100644 index 0000000..1859336 --- /dev/null +++ b/jenner-check/t003_rc_descriptive_tab/expected.json @@ -0,0 +1,19 @@ +{ + "_captured_at": "2026-06-17T13:07:26.898378Z", + "_captured_run_id": "r_019ed5ab5a9b7fa397ebd1e77ba53a3c", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Wrote rent_control_database (14 rows, 3 columns).", + "FORMAT $ward defined (9 ranges).", + "PROC TABULATE" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} \ No newline at end of file diff --git a/jenner-check/t003_rc_descriptive_tab/expected/files.md b/jenner-check/t003_rc_descriptive_tab/expected/files.md new file mode 100644 index 0000000..c8d1760 --- /dev/null +++ b/jenner-check/t003_rc_descriptive_tab/expected/files.md @@ -0,0 +1,14 @@ +These URLs point to artifacts from a specific Jenner run and expire when that run is reaped; re-running the bundle regenerates them. + + +## Files + +| name | content_type | size_bytes | url | +|------|--------------|-----------|-----| +| listing.txt | text/plain | 2016 | https://api.jenneranalytics.com/v1/run/r_019ed5ab5a9b7fa397ebd1e77ba53a3c/files/listing.txt?token=f4177af0179a4d8488f72b6afded95c6 | + +## Datasets + +| name | rows | columns | preview_url | +|------|------|---------|-------------| +| rent_control_database | 14 | 3 | https://api.jenneranalytics.com/v1/run/r_019ed5ab5a9b7fa397ebd1e77ba53a3c/datasets/rent_control_database?token=f4177af0179a4d8488f72b6afded95c6 | diff --git a/jenner-check/t003_rc_descriptive_tab/expected/log.txt b/jenner-check/t003_rc_descriptive_tab/expected/log.txt new file mode 100644 index 0000000..200e2c5 --- /dev/null +++ b/jenner-check/t003_rc_descriptive_tab/expected/log.txt @@ -0,0 +1,23 @@ +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 rent_control_database + +NOTE: Processing inline DATALINES (14 lines) + +NOTE: Read 14 rows from DATALINES. +NOTE: Wrote rent_control_database (14 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC SORT data=rent_control_database + +NOTE: Unlicensed mode - input limited to 100 observations. +NOTE: Read 14 rows from rent_control_database. +NOTE: Wrote rent_control_database (14 rows, 3 columns). +NOTE: PROC SORT statement used. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT $ward defined (9 ranges). +NOTE: PROC TABULATE diff --git a/jenner-check/t003_rc_descriptive_tab/expected/output.txt b/jenner-check/t003_rc_descriptive_tab/expected/output.txt new file mode 100644 index 0000000..64f91fe --- /dev/null +++ b/jenner-check/t003_rc_descriptive_tab/expected/output.txt @@ -0,0 +1,22 @@ +----------------------------------------------------------------------------------------------- +| | Number of properties | Number of properties | Pct. units | Pct. units | +|---------------------+----------------------+----------------------+------------+------------| +|Ward | | | | | +|---------------------| | | | | +|Total Rent Controlled| 12| 100.0| 189| 100.0| +|---------------------+----------------------+----------------------+------------+------------| +|Ward 1 | 2| 16.7| 20| 10.6| +|---------------------+----------------------+----------------------+------------+------------| +|Ward 2 | 1| 8.3| 30| 15.9| +|---------------------+----------------------+----------------------+------------+------------| +|Ward 4 | 2| 16.7| 22| 11.6| +|---------------------+----------------------+----------------------+------------+------------| +|Ward 5 | 2| 16.7| 36| 19.0| +|---------------------+----------------------+----------------------+------------+------------| +|Ward 6 | 2| 16.7| 58| 30.7| +|---------------------+----------------------+----------------------+------------+------------| +|Ward 7 | 1| 8.3| 3| 1.6| +|---------------------+----------------------+----------------------+------------+------------| +|Ward 8 | 2| 16.7| 20| 10.6| +----------------------------------------------------------------------------------------------- + diff --git a/jenner-check/t003_rc_descriptive_tab/meta.json b/jenner-check/t003_rc_descriptive_tab/meta.json new file mode 100644 index 0000000..630c932 --- /dev/null +++ b/jenner-check/t003_rc_descriptive_tab/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t003_rc_descriptive_tab", + "source_file": "Prog/RC_Descriptive_tables.sas", + "source_blob_sha": "8ab240505b70781e1c1bf5bfdf6efdd4c0ec1b09", + "source_commit": "ac221b855c1daf69fc24fe67326506b65c76799a", + "tier": "real_data", + "notes": "By-ward PROC TABULATE (single CLASS row dim, N/PCTN/SUM/PCTSUM) with PROC SORT and $ward format, verbatim; external DHCD.rent_control_database replaced by inline sample (ward2002, rent_controlled, adj_unit_count); WHERE rent_controlled=1 intact." +} diff --git a/jenner-check/t003_rc_descriptive_tab/script.sas b/jenner-check/t003_rc_descriptive_tab/script.sas new file mode 100644 index 0000000..4ab506d --- /dev/null +++ b/jenner-check/t003_rc_descriptive_tab/script.sas @@ -0,0 +1,72 @@ +/************************************************************************** + Bundle: t003_rc_descriptive_tab + Source: Prog/RC_Descriptive_tables.sas (NeighborhoodInfoDC/DHCD) + Original: A. Williams, DC Rent Control Properties, 12/27/09 + + Description (from source): Creates tables based on the rent control + database. This bundle reproduces the "By ward" table -- rent-controlled + properties and units cross-classified by 2002 ward -- with the four + statistic columns the source defines: number of properties (N), + percent of properties (PCTN), number of units (SUM of adj_unit_count), + and percent of units (PCTSUM). + + Adaptation: the original reads DHCD.rent_control_database from an + external SAS library (DCData framework). Here that input is supplied as + a small inline sample with ward2002, rent_controlled, and adj_unit_count. + The PROC SORT, the $ward. PROC FORMAT, and the PROC TABULATE TABLE + specification (including the WHERE on rent_controlled) are reproduced + exactly as written in the source. +**************************************************************************/ + +** Sample standing in for DHCD.rent_control_database **; +data rent_control_database; + length ward2002 $ 1; + input ward2002 $ rent_controlled adj_unit_count; + datalines; +1 1 12 +1 1 8 +2 1 30 +3 0 5 +4 1 16 +4 1 6 +5 1 22 +5 1 14 +6 1 40 +7 1 3 +8 1 9 +8 1 11 +1 0 7 +6 1 18 +; +run; + +proc sort data=rent_control_database; +by ward2002; +run; + +proc format; +value $ward +"1"= " Ward 1" +"2"= " Ward 2" +"3"= " Ward 3" +"4"= " Ward 4" +"5"= " Ward 5" +"6"= " Ward 6" +"7"= " Ward 7" +"8"= " Ward 8" +" "=" Missing"; +run; + +proc tabulate data=rent_control_database missing format=comma12.0; +class rent_controlled ward2002; +var adj_unit_count ; +table (ALL="Total Rent Controlled") Ward2002="Ward" , + (ALL="Number of properties")*N="" + (ALL="Pct. properties")*(PCTN="")*f=comma12.1 + (adj_unit_count="Number of units")*SUM="" + (adj_unit_count="Pct. units")*(pctsum="")*f=comma12.1 ; +where rent_controlled=1; +format ward2002 $ward.; +title 'Rent Controlled Properties by Ward'; +footnote1 "Note: The figures in these tables are based on a preliminary enumeration of rent-controlled housing in the District of Columbia."; +run; diff --git a/jenner-check/t004_cpmp_needs_tab/autoexec.sas b/jenner-check/t004_cpmp_needs_tab/autoexec.sas new file mode 100644 index 0000000..f50fa62 --- /dev/null +++ b/jenner-check/t004_cpmp_needs_tab/autoexec.sas @@ -0,0 +1,9 @@ +options obs=100; + +/* The DCData framework provides %fdate, which sets the &fdate macro var + to a formatted run date used in report footnotes. A deterministic stub + is defined here so the bundle output is reproducible. */ +%macro fdate; + %global fdate; + %let fdate = (run date); +%mend fdate; diff --git a/jenner-check/t004_cpmp_needs_tab/expected.json b/jenner-check/t004_cpmp_needs_tab/expected.json new file mode 100644 index 0000000..b2c2f05 --- /dev/null +++ b/jenner-check/t004_cpmp_needs_tab/expected.json @@ -0,0 +1,19 @@ +{ + "_captured_at": "2026-06-17T13:07:26.915224Z", + "_captured_run_id": "r_019ed5abe8dd7fd190353d3c4e88ffb7", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Wrote CPMP_2008 (10 rows, 9 columns).", + "FORMAT raceeth defined (5 ranges).", + "PROC TABULATE" + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} \ No newline at end of file diff --git a/jenner-check/t004_cpmp_needs_tab/expected/files.md b/jenner-check/t004_cpmp_needs_tab/expected/files.md new file mode 100644 index 0000000..90a33d0 --- /dev/null +++ b/jenner-check/t004_cpmp_needs_tab/expected/files.md @@ -0,0 +1,14 @@ +These URLs point to artifacts from a specific Jenner run and expire when that run is reaped; re-running the bundle regenerates them. + + +## Files + +| name | content_type | size_bytes | url | +|------|--------------|-----------|-----| +| listing.txt | text/plain | 2520 | https://api.jenneranalytics.com/v1/run/r_019ed5abe8dd7fd190353d3c4e88ffb7/files/listing.txt?token=14b029f357374160a6c9ad5481512a23 | + +## Datasets + +| name | rows | columns | preview_url | +|------|------|---------|-------------| +| cpmp_2008 | 10 | 9 | https://api.jenneranalytics.com/v1/run/r_019ed5abe8dd7fd190353d3c4e88ffb7/datasets/cpmp_2008?token=14b029f357374160a6c9ad5481512a23 | diff --git a/jenner-check/t004_cpmp_needs_tab/expected/log.txt b/jenner-check/t004_cpmp_needs_tab/expected/log.txt new file mode 100644 index 0000000..2b62b5f --- /dev/null +++ b/jenner-check/t004_cpmp_needs_tab/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: DATA CPMP_2008 + +NOTE: Processing inline DATALINES (10 lines) + +NOTE: Read 10 rows from DATALINES. +NOTE: Wrote CPMP_2008 (10 rows, 9 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT hudinc defined (3 ranges). +NOTE: FORMAT ownershp defined (2 ranges). +NOTE: FORMAT hhtype defined (4 ranges). +NOTE: FORMAT raceeth defined (5 ranges). +NOTE: PROC TABULATE diff --git a/jenner-check/t004_cpmp_needs_tab/expected/output.txt b/jenner-check/t004_cpmp_needs_tab/expected/output.txt new file mode 100644 index 0000000..f28aad3 --- /dev/null +++ b/jenner-check/t004_cpmp_needs_tab/expected/output.txt @@ -0,0 +1,25 @@ +-------------------------------------------------------------------------------------------------------- +|All Households, 2008 | Current % | Current Number | Sample obs. | +|----------------------------------------------------------+------------+----------------+-------------| +|Non-Hisp. black Number of households | 40.00| 390| 390| +| Any housing problems | 40.00| 390| 390| +| Cost burden > 30% | 40.00| 260| 390| +| Cost burden > 50% | 40.00| 0| 390| +|Non-Hisp. white Number of households | 20.00| 245| 245| +| Any housing problems | 20.00| 95| 245| +| Cost burden > 30% | 20.00| 0| 245| +| Cost burden > 50% | 20.00| 0| 245| +|Hispanic/Latino Number of households | 20.00| 230| 230| +| Any housing problems | 20.00| 90| 230| +| Cost burden > 30% | 20.00| 230| 230| +| Cost burden > 50% | 20.00| 230| 230| +|Non-Hisp. Asian Number of households | 10.00| 110| 110| +| Any housing problems | 10.00| 110| 110| +| Cost burden > 30% | 10.00| 110| 110| +| Cost burden > 50% | 10.00| 110| 110| +|Non-Hisp. other race Number of households | 10.00| 70| 70| +| Any housing problems | 10.00| 0| 70| +| Cost burden > 30% | 10.00| 70| 70| +| Cost burden > 50% | 10.00| 0| 70| +-------------------------------------------------------------------------------------------------------- + diff --git a/jenner-check/t004_cpmp_needs_tab/meta.json b/jenner-check/t004_cpmp_needs_tab/meta.json new file mode 100644 index 0000000..7d3454b --- /dev/null +++ b/jenner-check/t004_cpmp_needs_tab/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t004_cpmp_needs_tab", + "source_file": "Prog/CPMP_2008_needs_tbl2.sas", + "source_blob_sha": "30234b2328a5b0e51975a228b90c8865662356ba", + "source_commit": "ac221b855c1daf69fc24fe67326506b65c76799a", + "tier": "real_data", + "notes": "Weighted PROC TABULATE (race/ethnicity needs table) with WEIGHT hhwt, preloadfmt/order=data, numeric classification formats, verbatim; external DHCD.CPMP_2008 replaced by inline ACS-shaped sample; ODS HTML local-path wrapper omitted; %fdate stubbed deterministically in autoexec." +} diff --git a/jenner-check/t004_cpmp_needs_tab/script.sas b/jenner-check/t004_cpmp_needs_tab/script.sas new file mode 100644 index 0000000..11f2fb8 --- /dev/null +++ b/jenner-check/t004_cpmp_needs_tab/script.sas @@ -0,0 +1,83 @@ +/************************************************************************** + Bundle: t004_cpmp_needs_tab + Source: Prog/CPMP_2008_needs_tbl2.sas (NeighborhoodInfoDC/DHCD) + Original: P. Tatian, NeighborhoodInfo DC, 02/26/10 + + Description (from source): Produce data for the CPMP tool Needs table + (Needs.xls) for the DC ConPlan. This bundle reproduces the first table -- + housing needs by race/ethnicity -- a WEIGHTed PROC TABULATE that reports + weighted sums and percents plus an unweighted sample-obs count, using the + hhwt household weight and the program's user-defined classification + formats. + + Adaptation: the original reads DHCD.CPMP_2008 from an external SAS + library (DCData framework) and writes to an ODS HTML/XLS file on a local + path; here the input is a small inline sample with the columns the table + reads, and the ODS HTML wrapper (a local D:\ path) is omitted so the + table renders to the listing. The PROC FORMAT block, the CLASS / VAR / + WEIGHT / TABLE specification, and the formats are reproduced exactly as + written in the source. +**************************************************************************/ + +** Sample standing in for DHCD.CPMP_2008 (ACS 2008 IPUMS extract) **; +data CPMP_2008; + input hud_inc ownershp household_type race_eth + total housing_problems cost_burden_30 cost_burden_50 hhwt; + datalines; +1 2 1 2 1 1 1 0 120 +1 2 2 1 1 1 0 0 95 +2 1 3 5 1 0 1 1 140 +3 2 4 2 1 1 1 0 80 +1 2 1 3 1 1 1 1 110 +2 2 2 2 1 1 1 0 60 +3 1 3 1 1 0 0 0 150 +1 2 4 5 1 1 1 1 90 +2 2 1 2 1 1 0 0 130 +3 2 2 4 1 0 1 0 70 +; +run; + +proc format; + value hudinc + 1 = '<=30% MFI' + 2 = '>30 to <=50% MFI' + 3 = '>50 to <=80% MFI'; + value ownershp (notsorted) + 2 = 'Renter' + 1 = 'Owner'; + value hhtype + 1 = 'Elderly' + 2 = 'Small related' + 3 = 'Large related' + 4 = 'All other'; + value raceeth (notsorted) + 2 = 'Non-Hisp. black' + 1 = 'Non-Hisp. white' + 5 = 'Hispanic/Latino' + 3 = 'Non-Hisp. Asian' + 4 = 'Non-Hisp. other race'; +run; + +%fdate() + +proc tabulate data=CPMP_2008 format=comma12.0 noseps missing; + class hud_inc ownershp household_type race_eth / preloadfmt order=data; + var total housing_problems cost_burden_30 cost_burden_50; + weight hhwt; + table + /** Rows **/ + race_eth=' ' * + ( total='Number of households' + housing_problems='Any housing problems' + cost_burden_30='Cost burden > 30%' + cost_burden_50='Cost burden > 50%' ), + /** Columns **/ + pctsum='Current %'*f=comma10.2 sum='Current Number' n='Sample obs.' + / rts=60 box="All Households, 2008" + ; + format hud_inc hudinc. ownershp ownershp. household_type hhtype. race_eth raceeth.; + title2 "HOUSING NEEDS BY RACE/ETHNICITY"; + footnote1 "Source: ACS 2008 IPUMS file."; + footnote2 "Prepared for DC DHCD by NeighborhoodInfo DC (&fdate)"; + +run; diff --git a/jenner-check/t005_units_regression/autoexec.sas b/jenner-check/t005_units_regression/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t005_units_regression/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t005_units_regression/expected.json b/jenner-check/t005_units_regression/expected.json new file mode 100644 index 0000000..de7279b --- /dev/null +++ b/jenner-check/t005_units_regression/expected.json @@ -0,0 +1,19 @@ +{ + "_captured_at": "2026-06-17T13:07:26.933236Z", + "_captured_run_id": "r_019ed5acb4107a4297958207d7bb1a86", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Wrote reg1 (12 rows, 13 columns).", + "PROC REG data=reg1", + "PROC MEANS statement used." + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} \ No newline at end of file diff --git a/jenner-check/t005_units_regression/expected/files.md b/jenner-check/t005_units_regression/expected/files.md new file mode 100644 index 0000000..41ed64e --- /dev/null +++ b/jenner-check/t005_units_regression/expected/files.md @@ -0,0 +1,26 @@ +These URLs point to artifacts from a specific Jenner run and expire when that run is reaped; re-running the bundle regenerates them. + + +## Files + +| name | content_type | size_bytes | url | +|------|--------------|-----------|-----| +| listing.txt | text/plain | 1633 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/listing.txt?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_cooksd.png | image/png | 30243 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_cooksd.png?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_cooksd.svg | image/svg+xml | 14236 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_cooksd.svg?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_qq_plot.png | image/png | 48215 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_qq_plot.png?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_qq_plot.svg | image/svg+xml | 16858 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_qq_plot.svg?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_residual_histogram_panel.png | image/png | 43008 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_residual_histogram_panel.png?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_residual_histogram_panel.svg | image/svg+xml | 15477 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_residual_histogram_panel.svg?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_residuals_vs_obs_order.png | image/png | 28983 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_residuals_vs_obs_order.png?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_residuals_vs_obs_order.svg | image/svg+xml | 14735 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_residuals_vs_obs_order.svg?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_residuals_vs_predicted.png | image/png | 28868 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_residuals_vs_predicted.png?token=87b55210bcd549768b72818e2cf0822b | +| ods_output/reg_residuals_vs_predicted.svg | image/svg+xml | 17160 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/files/ods_output/reg_residuals_vs_predicted.svg?token=87b55210bcd549768b72818e2cf0822b | + +## Datasets + +| name | rows | columns | preview_url | +|------|------|---------|-------------| +| parcel_unit_geo_merge | 12 | 7 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/datasets/parcel_unit_geo_merge?token=87b55210bcd549768b72818e2cf0822b | +| reg1 | 12 | 13 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/datasets/reg1?token=87b55210bcd549768b72818e2cf0822b | +| reg1_nogeo_results | 12 | 15 | https://api.jenneranalytics.com/v1/run/r_019ed5acb4107a4297958207d7bb1a86/datasets/reg1_nogeo_results?token=87b55210bcd549768b72818e2cf0822b | diff --git a/jenner-check/t005_units_regression/expected/log.txt b/jenner-check/t005_units_regression/expected/log.txt new file mode 100644 index 0000000..aeb3b36 --- /dev/null +++ b/jenner-check/t005_units_regression/expected/log.txt @@ -0,0 +1,38 @@ +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 parcel_unit_geo_merge + +NOTE: Processing inline DATALINES (12 lines) + +NOTE: Read 12 rows from DATALINES. +NOTE: Wrote parcel_unit_geo_merge (12 rows, 7 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA reg1 + + +NOTE: Read 12 rows from parcel_unit_geo_merge. +NOTE: Wrote reg1 (12 rows, 13 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +SYMBOLGEN: Macro variable VARLST resolves to dusecode021 dusecode022 dusecode023 dusecode024 dusecode025 +SYMBOLGEN: Macro variable VAREXCLUDELST resolves to dusecode029 +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. +SYMBOLGEN: Macro variable VARLST resolves to dusecode021 dusecode022 dusecode023 dusecode024 dusecode025 +NOTE: PROC REG data=reg1 + +NOTE: PROC REG: processed 1 job(s) in batch +NOTE: Output dataset reg1_nogeo_results written. +NOTE: ODS plot written: reg_residuals_vs_predicted.spec.json +NOTE: ODS plot written: reg_residual_histogram_panel.spec.json +NOTE: ODS plot written: reg_residuals_vs_obs_order.spec.json +NOTE: ODS plot written: reg_qq_plot.spec.json +NOTE: ODS plot written: reg_cooksd.spec.json +NOTE: PROC REG ODS Graphics generated. +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. diff --git a/jenner-check/t005_units_regression/expected/output.txt b/jenner-check/t005_units_regression/expected/output.txt new file mode 100644 index 0000000..475ac24 --- /dev/null +++ b/jenner-check/t005_units_regression/expected/output.txt @@ -0,0 +1,46 @@ + The MEANS Procedure + + Variable N Mean Std Dev Minimum Maximum + -------------------------------------------------------------------------------- + dusecode021 12 0.2500000 0.4522670 0.0000000 1.0000000 + dusecode022 12 0.2500000 0.4522670 0.0000000 1.0000000 + dusecode023 12 0.1666667 0.3892495 0.0000000 1.0000000 + dusecode024 12 0.1666667 0.3892495 0.0000000 1.0000000 + dusecode025 12 0.1666667 0.3892495 0.0000000 1.0000000 + dusecode029 12 0.0000000 0.0000000 0.0000000 0.0000000 + -------------------------------------------------------------------------------- + + The REG Procedure + Model: MODEL1 + Dependent Variable: active_res + +Source DF Sum of Squares Mean Square F Value Pr > F +--------------- -------- -------------- ----------- -------- -------- +Model 6 1321.25957 220.20993 664.44 <.0001 +Error 5 1.65710 0.33142 +Corrected Total 11 1322.91667 + +Root MSE 0.57569 R-Square 0.9987 +Dependent Mean 15.58333 Adj R-Sq 0.9972 +Coeff Var 3.69427 + + Parameter Estimates + +Variable DF Estimate Standard Error t Value Pr > |t| +----------- -------- -------- -------------- -------- -------- +Intercept 1 0.91639 1.03079 0.89 0.4147 +assess_val 1 0.00002 0.00001 3.77 0.0130 +landarea 1 -0.00267 0.00204 -1.31 0.2474 +dusecode021 1 0.72653 0.57960 1.25 0.2654 +dusecode022 1 0.27461 0.48674 0.56 0.5970 +dusecode023 1 0.82512 0.51498 1.60 0.1700 +dusecode024 1 -0.78791 0.45890 -1.72 0.1466 +dusecode025 1 -0.12196 0.78801 -0.15 0.8831 + + The MEANS Procedure + + Variable Label N Mean Std Dev Minimum Maximum + ----------------------------------------------------------------------------------------------- + PREDICTED Predicted Value 12 15.5833333 10.9596764 4.4084850 39.9474400 + ----------------------------------------------------------------------------------------------- + diff --git a/jenner-check/t005_units_regression/meta.json b/jenner-check/t005_units_regression/meta.json new file mode 100644 index 0000000..689468d --- /dev/null +++ b/jenner-check/t005_units_regression/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t005_units_regression", + "source_file": "Prog/RC_Regression.sas", + "source_blob_sha": "cdd2b4aba38c7e9838577413b968bf2ecf462286", + "source_commit": "ac221b855c1daf69fc24fe67326506b65c76799a", + "tier": "real_data", + "notes": "Use-code dummy-variable DATA step + PROC MEANS + PROC REG (SIMPLE, OUTPUT OUT= R=/P=) verbatim, incl. %let predictor lists; external dhcd.parcel_unit_geo_merge replaced by inline parcel sample (active_res, assess_val, landarea, usecode, ssl, ui_proptype, in_last_ownerpt)." +} diff --git a/jenner-check/t005_units_regression/script.sas b/jenner-check/t005_units_regression/script.sas new file mode 100644 index 0000000..c1db88a --- /dev/null +++ b/jenner-check/t005_units_regression/script.sas @@ -0,0 +1,72 @@ +/************************************************************************** + Bundle: t005_units_regression + Source: Prog/RC_Regression.sas (NeighborhoodInfoDC/DHCD) + Original: A. Williams, DC Rent Control Database, 11/18/10 + + Description (from source): Regression to estimate unit counts for SSLs. + This bundle reproduces the regression-prep DATA step (use-code dummy + variables), the PROC MEANS over the predictor list, and the PROC REG + model of active_res on assess_val, landarea and the use-code dummies, + exactly as written in the source. + + Adaptation: the original reads dhcd.parcel_unit_geo_merge from an + external SAS library (DCData framework). Here that input is supplied as + a small inline sample with the columns the model reads (active_res, + assess_val, landarea, usecode, ssl, ui_proptype, in_last_ownerpt). The + %let predictor lists, the DATA step, PROC MEANS and PROC REG are + reproduced exactly. +**************************************************************************/ + +** Sample standing in for dhcd.parcel_unit_geo_merge **; +data parcel_unit_geo_merge; + length ssl $ 17 ui_proptype usecode $ 3; + input ssl $ ui_proptype $ usecode $ in_last_ownerpt active_res assess_val landarea; + datalines; +0001 13 021 1 12 850000 3200 +0002 13 022 1 18 1200000 4100 +0003 13 023 1 6 420000 1800 +0004 13 024 1 4 380000 1600 +0005 13 025 1 24 1650000 5200 +0006 13 021 1 10 720000 2900 +0007 13 022 1 30 2100000 6800 +0008 13 023 1 8 510000 2100 +0009 13 021 1 16 1050000 3600 +0010 13 024 1 5 400000 1700 +0011 13 025 1 40 2700000 8100 +0012 13 022 1 14 980000 3300 +; +run; + +data reg1; + set parcel_unit_geo_merge; + If usecode='021' then dusecode021=1; else dusecode021=0; + If usecode='022' then dusecode022=1; else dusecode022=0; + If usecode='023' then dusecode023=1; else dusecode023=0; + If usecode='024' then dusecode024=1; else dusecode024=0; + If usecode='025' then dusecode025=1; else dusecode025=0; + If usecode='029' then dusecode029=1; else dusecode029=0; + + where ui_proptype = "13" and ssl ne "" and in_last_ownerpt = 1; + + run; +options mprint symbolgen; + +%let varlst= dusecode021 dusecode022 dusecode023 dusecode024 dusecode025 ; +%let varExcludeLst= dusecode029; + + +proc means data=reg1; +var &varlst. &varExcludeLst. ; +run; + +title Units regression; +proc reg data=reg1 SIMPLE; + model active_res= assess_val landarea &varlst.; + where active_res >1 and ssl ne ""; + output out=reg1_nogeo_results R=residual p=predicted; + run; + quit; + +proc means data=reg1_nogeo_results; +var predicted; +run; diff --git a/jenner-check/t006_address_units/autoexec.sas b/jenner-check/t006_address_units/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t006_address_units/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t006_address_units/expected.json b/jenner-check/t006_address_units/expected.json new file mode 100644 index 0000000..db0711e --- /dev/null +++ b/jenner-check/t006_address_units/expected.json @@ -0,0 +1,19 @@ +{ + "_captured_at": "2026-06-17T13:07:26.952523Z", + "_captured_run_id": "r_019ed5ad3ffb7e01be4b337a48606077", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Wrote addresspt (10 rows, 2 columns).", + "PROC FREQ statement used.", + "PROC MEANS statement used." + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} \ No newline at end of file diff --git a/jenner-check/t006_address_units/expected/files.md b/jenner-check/t006_address_units/expected/files.md new file mode 100644 index 0000000..59fe706 --- /dev/null +++ b/jenner-check/t006_address_units/expected/files.md @@ -0,0 +1,21 @@ +These URLs point to artifacts from a specific Jenner run and expire when that run is reaped; re-running the bundle regenerates them. + + +## Files + +| name | content_type | size_bytes | url | +|------|--------------|-----------|-----| +| listing.txt | text/plain | 592 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/files/listing.txt?token=596ea3f190754ab3bc83a5add5078220 | +| ods_output/freq_active_res.png | image/png | 13607 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/files/ods_output/freq_active_res.png?token=596ea3f190754ab3bc83a5add5078220 | +| ods_output/freq_active_res.svg | image/svg+xml | 7893 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/files/ods_output/freq_active_res.svg?token=596ea3f190754ab3bc83a5add5078220 | + +## Datasets + +| name | rows | columns | preview_url | +|------|------|---------|-------------| +| addresspt | 10 | 2 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/datasets/addresspt?token=596ea3f190754ab3bc83a5add5078220 | +| match_check | 1 | 5 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/datasets/match_check?token=596ea3f190754ab3bc83a5add5078220 | +| parcel_base | 7 | 2 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/datasets/parcel_base?token=596ea3f190754ab3bc83a5add5078220 | +| parcel_unit_merge | 6 | 5 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/datasets/parcel_unit_merge?token=596ea3f190754ab3bc83a5add5078220 | +| summed_units | 6 | 2 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/datasets/summed_units?token=596ea3f190754ab3bc83a5add5078220 | +| who_owns | 7 | 2 | https://api.jenneranalytics.com/v1/run/r_019ed5ad3ffb7e01be4b337a48606077/datasets/who_owns?token=596ea3f190754ab3bc83a5add5078220 | diff --git a/jenner-check/t006_address_units/expected/log.txt b/jenner-check/t006_address_units/expected/log.txt new file mode 100644 index 0000000..53b7bdc --- /dev/null +++ b/jenner-check/t006_address_units/expected/log.txt @@ -0,0 +1,65 @@ +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 addresspt + +NOTE: Processing inline DATALINES (10 lines) + +NOTE: Read 10 rows from DATALINES. +NOTE: Wrote addresspt (10 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA who_owns + +NOTE: Processing inline DATALINES (7 lines) + +NOTE: Read 7 rows from DATALINES. +NOTE: Wrote who_owns (7 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC FREQ +NOTE: PROC FREQ statement used. +NOTE: PROC SORT data=addresspt + +NOTE: Unlicensed mode - input limited to 100 observations. +NOTE: Read 10 rows from addresspt. +NOTE: Wrote addresspt (10 rows, 2 columns). +NOTE: PROC SORT statement used. +NOTE: PROC MEANS +NOTE: Output dataset summed_units has 6 observations and 2 variables. +NOTE: PROC MEANS statement used. +NOTE: PROC SORT data=summed_units + +NOTE: Unlicensed mode - input limited to 100 observations. +NOTE: Read 6 rows from summed_units. +NOTE: Wrote summed_units (6 rows, 2 columns). +NOTE: PROC SORT statement used. +NOTE: PROC SORT data=who_owns + +NOTE: Unlicensed mode - input limited to 100 observations. +NOTE: Read 7 rows from who_owns. +NOTE: Wrote parcel_base (7 rows, 2 columns). +NOTE: PROC SORT statement used. +NOTE: DATA parcel_unit_merge match_check + +NOTE: Multi-output DATA step splits stream into: parcel_unit_merge match_check +NOTE: Stream 1 processed 6 rows, max BY-group size: 1 (O(1) memory verified) +NOTE: Stream 2 processed 6 rows, max BY-group size: 1 (O(1) memory verified) + +NOTE: Wrote parcel_unit_merge (6 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds + +NOTE: Wrote match_check (1 rows, 5 columns). +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT unit defined (9 ranges). +NOTE: PROC FREQ +NOTE: ODS plot written: freq_active_res.spec.json +NOTE: PROC FREQ statement used. +NOTE: PROC MEANS +NOTE: PROC MEANS statement used. diff --git a/jenner-check/t006_address_units/expected/output.txt b/jenner-check/t006_address_units/expected/output.txt new file mode 100644 index 0000000..534aa23 --- /dev/null +++ b/jenner-check/t006_address_units/expected/output.txt @@ -0,0 +1,18 @@ + The FREQ Procedure + + Cumulative Cumulative +active_res Frequency Percent Frequency Percent +------------------------------------------------------------------- + The FREQ Procedure + + Cumulative Cumulative +active_res Frequency Percent Frequency Percent +------------------------------------------------------------------- +1 9 100.00 9 100.00 + The MEANS Procedure + + Variable N Mean Std Dev Minimum Maximum + ------------------------------------------------------------------------------- + active_res 4 1.7500000 0.9574271 1.0000000 3.0000000 + ------------------------------------------------------------------------------- + diff --git a/jenner-check/t006_address_units/meta.json b/jenner-check/t006_address_units/meta.json new file mode 100644 index 0000000..e7109c8 --- /dev/null +++ b/jenner-check/t006_address_units/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t006_address_units", + "source_file": "Prog/RC_Address_units.sas", + "source_blob_sha": "804d0a7003e3b10fb252f04bba5cee0a8fbaacd6", + "source_commit": "ac221b855c1daf69fc24fe67326506b65c76799a", + "tier": "real_data", + "notes": "PROC FREQ + PROC SORT + PROC SUMMARY(sum by ssl) + MERGE with in= match flags + closing PROC FREQ/MEANS verbatim; external mar.addresspt and realprop.who_owns replaced by inline samples (ssl, active_res; ssl, ui_proptype)." +} diff --git a/jenner-check/t006_address_units/script.sas b/jenner-check/t006_address_units/script.sas new file mode 100644 index 0000000..2135637 --- /dev/null +++ b/jenner-check/t006_address_units/script.sas @@ -0,0 +1,109 @@ +/************************************************************************** + Bundle: t006_address_units + Source: Prog/RC_Address_units.sas (NeighborhoodInfoDC/DHCD) + Original: A. Williams, DC Rent Control Database, 11/18/10 + + Description (from source): Preps the address-units file for the unit-count + regression. The address points (one row per residential unit) are + summed to a total active-residential unit count per SSL (parcel), then + merged onto the parcel ownership file restricted to rental apartment + buildings (ui_proptype "13"). + + Adaptation: the original reads mar.addresspt and realprop.who_owns from + external SAS libraries (DCData framework). Here those inputs are supplied + as small inline samples with the columns the program reads (ssl, + active_res; ssl, ui_proptype). The PROC FREQ, PROC SORT, PROC SUMMARY + (sum by SSL), the MERGE with match flags, and the closing PROC FREQ / + PROC MEANS are reproduced exactly as written in the source. +**************************************************************************/ + +** Sample standing in for mar.addresspt (address points, one per unit) **; +data addresspt; + length ssl $ 17; + input ssl $ active_res; + datalines; +0001 1 +0001 1 +0001 1 +0002 1 +0002 1 +0003 1 +0004 . +0005 1 +0005 1 +0006 1 +; +run; + +** Sample standing in for realprop.who_owns (parcel ownership file) **; +data who_owns; + length ssl $ 17 ui_proptype $ 3; + input ssl $ ui_proptype $; + datalines; +0001 13 +0002 13 +0003 13 +0004 13 +0005 10 +0006 13 +0007 13 +; +run; + +*Check for number of SSLs with missing units; + proc freq data=addresspt ; + table ACTIVE_RES; + where active_res=.; + run; + + proc sort data=addresspt; + by ssl; + run; + +*summarize by ssl to get total unit count; + proc summary data=addresspt sum; + var active_res; + by ssl; + where ssl ne ""; *exclude properties with missing SSLs; + output out=summed_units (keep=active_res ssl) sum=; + run; + +*Merge unit counts with parcel base info; + proc sort data= summed_units; + by SSL; + run; + + proc sort data= who_owns out=parcel_base; + by SSL; + run; + + data parcel_unit_merge match_check; + merge parcel_base (in=a where=(ui_proptype="13")) summed_units (in=b); + by SSL; + if a=1 and b=0 then parcel_only=1; else parcel_only=0; + if b=1 and a=0 then address_only=1; else address_only=0; + if a=1 then output parcel_unit_merge; else output match_check; + run; + +*Check results; +proc format; +value unit +0="0" +1="1" +2="2" +3-5="3-5" +6-10="6-10" +11-50="11-50" +50-100="50-100" +101-700="101-700" +701-high="700 or above"; +run; + + +proc freq data=addresspt; +table active_res; +run; + +proc means data=parcel_unit_merge; +var active_res; +run; diff --git a/jenner-check/t007_nsp_summary_sql/autoexec.sas b/jenner-check/t007_nsp_summary_sql/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t007_nsp_summary_sql/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t007_nsp_summary_sql/expected.json b/jenner-check/t007_nsp_summary_sql/expected.json new file mode 100644 index 0000000..94d3148 --- /dev/null +++ b/jenner-check/t007_nsp_summary_sql/expected.json @@ -0,0 +1,19 @@ +{ + "_captured_at": "2026-06-17T13:07:26.970308Z", + "_captured_run_id": "r_019ed5af706f7fb193e9332121d8c87f", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Wrote Foreclosures_year_2009_2 (6 rows, 7 columns).", + "Output dataset Neighborhoods has 9 observations and 9 variables.", + "PROC SQL statement used." + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} \ No newline at end of file diff --git a/jenner-check/t007_nsp_summary_sql/expected/files.md b/jenner-check/t007_nsp_summary_sql/expected/files.md new file mode 100644 index 0000000..0cf2a95 --- /dev/null +++ b/jenner-check/t007_nsp_summary_sql/expected/files.md @@ -0,0 +1,19 @@ +These URLs point to artifacts from a specific Jenner run and expire when that run is reaped; re-running the bundle regenerates them. + + +## Files + +| name | content_type | size_bytes | url | +|------|--------------|-----------|-----| +| listing.txt | text/plain | 2696 | https://api.jenneranalytics.com/v1/run/r_019ed5af706f7fb193e9332121d8c87f/files/listing.txt?token=2839810ad48a42f19183f706c1979483 | + +## Datasets + +| name | rows | columns | preview_url | +|------|------|---------|-------------| +| foreclosures_year_2009_2 | 6 | 7 | https://api.jenneranalytics.com/v1/run/r_019ed5af706f7fb193e9332121d8c87f/datasets/foreclosures_year_2009_2?token=2839810ad48a42f19183f706c1979483 | +| neighborhoods | 9 | 9 | https://api.jenneranalytics.com/v1/run/r_019ed5af706f7fb193e9332121d8c87f/datasets/neighborhoods?token=2839810ad48a42f19183f706c1979483 | +| neighborhoods_2 | 9 | 14 | https://api.jenneranalytics.com/v1/run/r_019ed5af706f7fb193e9332121d8c87f/datasets/neighborhoods_2?token=2839810ad48a42f19183f706c1979483 | +| neighborhoods_units | 9 | 16 | https://api.jenneranalytics.com/v1/run/r_019ed5af706f7fb193e9332121d8c87f/datasets/neighborhoods_units?token=2839810ad48a42f19183f706c1979483 | +| num_units_tr00 | 3 | 5 | https://api.jenneranalytics.com/v1/run/r_019ed5af706f7fb193e9332121d8c87f/datasets/num_units_tr00?token=2839810ad48a42f19183f706c1979483 | +| units | 4 | 7 | https://api.jenneranalytics.com/v1/run/r_019ed5af706f7fb193e9332121d8c87f/datasets/units?token=2839810ad48a42f19183f706c1979483 | diff --git a/jenner-check/t007_nsp_summary_sql/expected/log.txt b/jenner-check/t007_nsp_summary_sql/expected/log.txt new file mode 100644 index 0000000..3365c84 --- /dev/null +++ b/jenner-check/t007_nsp_summary_sql/expected/log.txt @@ -0,0 +1,46 @@ +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 Foreclosures_year_2009_2 + +NOTE: Processing inline DATALINES (6 lines) + +NOTE: Read 6 rows from DATALINES. +NOTE: Wrote Foreclosures_year_2009_2 (6 rows, 7 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA Num_units_tr00 + +NOTE: Processing inline DATALINES (3 lines) + +NOTE: Read 3 rows from DATALINES. +NOTE: Wrote Num_units_tr00 (3 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC MEANS +NOTE: Output dataset Neighborhoods has 9 observations and 9 variables. +NOTE: PROC MEANS statement used. +NOTE: PROC PRINT data=Neighborhoods + +NOTE: PROC PRINT completed: 9 observations printed, 9 variables +NOTE: PROC MEANS +NOTE: Output dataset Units has 4 observations and 7 variables. +NOTE: PROC MEANS statement used. +NOTE: PROC SQL + +NOTE: Table Neighborhoods_units created. +NOTE: PROC SQL statement used. +NOTE: DATA Neighborhoods_2 + + +NOTE: Read 9 rows from Neighborhoods_units. +NOTE: Wrote Neighborhoods_2 (9 rows, 14 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=Neighborhoods_2 + +NOTE: PROC PRINT completed: 9 observations printed, 6 variables diff --git a/jenner-check/t007_nsp_summary_sql/expected/output.txt b/jenner-check/t007_nsp_summary_sql/expected/output.txt new file mode 100644 index 0000000..5b785cd --- /dev/null +++ b/jenner-check/t007_nsp_summary_sql/expected/output.txt @@ -0,0 +1,30 @@ + File = Neighborhoods + + Obs UI_PROPTYPE REPORT_DT GEO2000 _TYPE_ _FREQ_ IN_FORECLOSURE_BEG IN_FORECLOSURE_END FORECLOSURE_START FORECLOSURE_SALE +----- ----------- --------- ----------- ------ ------ ------------------ ------------------ ----------------- ---------------- + 1 10 17167 110 2 8 11 7 3 + 2 10 17532 110 2 10 13 9 5 + 3 11 17532 110 2 3 5 5 1 + 4 10 17167 11001007503 111 1 3 5 4 1 + 5 10 17167 11001007803 111 1 5 6 3 2 + 6 10 17532 11001007503 111 1 4 6 5 2 + 7 10 17532 11001007803 111 1 6 7 4 3 + 8 11 17532 11001007503 111 1 2 3 3 1 + 9 11 17532 11001009904 111 1 1 2 2 0 + + File = Neighborhoods + + File = Neighborhoods_2 + + Obs ui_proptype report_dt geo2000 in_foreclosure_end units foreclosure_start_rate +----- ----------- --------- ----------- ------------------ ----- ---------------------- + 1 10 17167 11 2900 2.4137931034 + 2 10 17532 13 3020 2.9801324503 + 3 11 17532 5 720 6.9444444444 + 4 10 17167 11001007503 5 1200 3.3333333333 + 5 10 17167 11001007803 6 900 3.3333333333 + 6 10 17532 11001007503 6 1250 4 + 7 10 17532 11001007803 7 950 4.2105263158 + 8 11 17532 11001007503 3 320 9.375 + 9 11 17532 11001009904 2 240 8.3333333333 + diff --git a/jenner-check/t007_nsp_summary_sql/meta.json b/jenner-check/t007_nsp_summary_sql/meta.json new file mode 100644 index 0000000..10cfc46 --- /dev/null +++ b/jenner-check/t007_nsp_summary_sql/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t007_nsp_summary_sql", + "source_file": "Prog/NSP_foreclosures.sas", + "source_blob_sha": "d3a079111bc38ee3406171458a2ce361cfeb3444", + "source_commit": "ac221b855c1daf69fc24fe67326506b65c76799a", + "tier": "real_data", + "notes": "PROC SUMMARY chartype (_TYPE_ rollups) + PROC SQL left join + SELECT(year(report_dt))/WHEN unit-assignment DATA step + per-1000-unit rates verbatim; external HsngMon/RealProp datasets replaced by inline samples; colon name-prefix VAR list (units_sf_:) written out as explicit columns; ODS RTF local-path report omitted." +} diff --git a/jenner-check/t007_nsp_summary_sql/script.sas b/jenner-check/t007_nsp_summary_sql/script.sas new file mode 100644 index 0000000..69e659a --- /dev/null +++ b/jenner-check/t007_nsp_summary_sql/script.sas @@ -0,0 +1,121 @@ +/************************************************************************** + Bundle: t007_nsp_summary_sql + Source: Prog/NSP_foreclosures.sas (NeighborhoodInfoDC/DHCD) + Original: P. Tatian, NeighborhoodInfo DC, 06/24/09 + + Description (from source): Create a table of foreclosure data for DC NSP2 + neighborhoods. This bundle reproduces the pipeline core: a PROC SUMMARY + with CHARTYPE that rolls foreclosure counts up by property type, report + date and tract (keeping the all-up and tract-level _TYPE_ subsets), a + PROC SQL left join to a tract unit-count table, and the DATA step that + picks the year-matching unit column with SELECT( year( report_dt ) ) and + computes per-1,000-unit foreclosure rates. + + Adaptation: the original reads HsngMon.Foreclosures_year_2009_2 and + RealProp.Num_units_tr00 from external SAS libraries (DCData framework); + here they are supplied as small inline samples with the columns the + program reads. The PROC SUMMARY (chartype), PROC SQL join, and the + SELECT/WHEN unit-assignment DATA step are reproduced as written; the + trailing ODS-RTF report (a local D:\ path) is omitted so output renders + to the listing. The source's colon name-prefix VAR list + (var units_sf_: units_condo_:) is written out as the explicit column + names in this sample so the four unit columns are summed directly. +**************************************************************************/ + +** Sample standing in for HsngMon.Foreclosures_year_2009_2 **; +data Foreclosures_year_2009_2; + length geo2000 $ 11 ui_proptype $ 3; + input geo2000 $ ui_proptype $ report_dt :date9. + in_foreclosure_beg in_foreclosure_end foreclosure_start foreclosure_sale; + format report_dt date9.; + datalines; +11001007503 10 01jan2008 4 6 5 2 +11001007503 11 01jan2008 2 3 3 1 +11001007803 10 01jan2008 6 7 4 3 +11001009904 11 01jan2008 1 2 2 0 +11001007503 10 01jan2007 3 5 4 1 +11001007803 10 01jan2007 5 6 3 2 +; +run; + +** Sample standing in for RealProp.Num_units_tr00 **; +data Num_units_tr00; + length geo2000 $ 11; + input geo2000 $ units_sf_2007 units_sf_2008 units_condo_2007 units_condo_2008; + datalines; +11001007503 1200 1250 300 320 +11001007803 900 950 150 160 +11001009904 800 820 220 240 +; +run; + +proc summary data=Foreclosures_year_2009_2 chartype; + where ui_proptype in ( '10', '11' ); + var in_foreclosure_beg in_foreclosure_end foreclosure_start foreclosure_sale; + class ui_proptype report_dt geo2000; + output out=Neighborhoods (where=(_type_ = '110' or _type_ = '111')) sum=; +run; + +proc print data=Neighborhoods; + title2 'File = Neighborhoods'; +run; + +proc summary data=Num_units_tr00 chartype; + var units_sf_2007 units_sf_2008 units_condo_2007 units_condo_2008 ; + class geo2000; + output out=Units (where=(_type_ = '0' or _type_ = '1')) sum=; +run; + +proc sql noprint; + create table Neighborhoods_units as + select * from + Neighborhoods left join + Units + on Neighborhoods.geo2000 = Units.geo2000 + order by Neighborhoods._type_, Neighborhoods.ui_proptype, Neighborhoods.report_dt, Neighborhoods.geo2000; +quit; + +data Neighborhoods_2; + + set Neighborhoods_units; + + if ui_proptype = '10' then do; + + select ( year( report_dt ) ); + when ( 2007 ) units = units_sf_2007; + when ( 2008 ) units = units_sf_2008; + otherwise units = units_sf_2008; + end; + + end; + + else if ui_proptype = '11' then do; + + select ( year( report_dt ) ); + when ( 2007 ) units = units_condo_2007; + when ( 2008 ) units = units_condo_2008; + otherwise units = units_condo_2008; + end; + + end; + + in_foreclosure_beg_rate = 1000 * in_foreclosure_beg / units; + in_foreclosure_end_rate = 1000 * in_foreclosure_end / units; + foreclosure_start_rate = 1000 * foreclosure_start / units; + foreclosure_sale_rate = 1000 * foreclosure_sale / units; + + label + foreclosure_start = 'New foreclosure starts' + in_foreclosure_end_rate = 'Foreclosure inventory per 1,000 existing units (end of year)' + foreclosure_start_rate = 'New foreclosure starts per 1,000 existing units' + foreclosure_sale_rate = 'Foreclosure sales per 1,000 existing units' + ; + + drop units_sf_: units_condo_: ; + +run; + +proc print data=Neighborhoods_2; + var ui_proptype report_dt geo2000 in_foreclosure_end units foreclosure_start_rate; + title2 'File = Neighborhoods_2'; +run; diff --git a/jenner-check/t008_rcasd_make_formats/autoexec.sas b/jenner-check/t008_rcasd_make_formats/autoexec.sas new file mode 100644 index 0000000..2052e87 --- /dev/null +++ b/jenner-check/t008_rcasd_make_formats/autoexec.sas @@ -0,0 +1 @@ +options obs=100; diff --git a/jenner-check/t008_rcasd_make_formats/expected.json b/jenner-check/t008_rcasd_make_formats/expected.json new file mode 100644 index 0000000..d5063e3 --- /dev/null +++ b/jenner-check/t008_rcasd_make_formats/expected.json @@ -0,0 +1,19 @@ +{ + "_captured_at": "2026-06-17T13:07:26.990208Z", + "_captured_run_id": "r_019ed5aff941718097e224ee33ca296b", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "FORMAT $rcasd_notice_type defined (12 ranges).", + "Wrote rcasd_notices (14 rows, 2 columns).", + "PROC FREQ statement used." + ], + "log_does_not_contain": [ + "ERROR:", + "[JENNER-ERROR" + ], + "diagnostics": { + "parse_warnings": [], + "runtime_warnings": [] + } +} \ No newline at end of file diff --git a/jenner-check/t008_rcasd_make_formats/expected/files.md b/jenner-check/t008_rcasd_make_formats/expected/files.md new file mode 100644 index 0000000..08a5142 --- /dev/null +++ b/jenner-check/t008_rcasd_make_formats/expected/files.md @@ -0,0 +1,16 @@ +These URLs point to artifacts from a specific Jenner run and expire when that run is reaped; re-running the bundle regenerates them. + + +## Files + +| name | content_type | size_bytes | url | +|------|--------------|-----------|-----| +| listing.txt | text/plain | 1855 | https://api.jenneranalytics.com/v1/run/r_019ed5aff941718097e224ee33ca296b/files/listing.txt?token=ff7a52dd5a1843f8a3a036efbfc15b8d | +| ods_output/freq_notice_desc.png | image/png | 64655 | https://api.jenneranalytics.com/v1/run/r_019ed5aff941718097e224ee33ca296b/files/ods_output/freq_notice_desc.png?token=ff7a52dd5a1843f8a3a036efbfc15b8d | +| ods_output/freq_notice_desc.svg | image/svg+xml | 15484 | https://api.jenneranalytics.com/v1/run/r_019ed5aff941718097e224ee33ca296b/files/ods_output/freq_notice_desc.svg?token=ff7a52dd5a1843f8a3a036efbfc15b8d | + +## Datasets + +| name | rows | columns | preview_url | +|------|------|---------|-------------| +| rcasd_notices | 14 | 2 | https://api.jenneranalytics.com/v1/run/r_019ed5aff941718097e224ee33ca296b/datasets/rcasd_notices?token=ff7a52dd5a1843f8a3a036efbfc15b8d | diff --git a/jenner-check/t008_rcasd_make_formats/expected/log.txt b/jenner-check/t008_rcasd_make_formats/expected/log.txt new file mode 100644 index 0000000..037650c --- /dev/null +++ b/jenner-check/t008_rcasd_make_formats/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 $rcasd_notice_type defined (12 ranges). +NOTE: DATA rcasd_notices + +NOTE: Processing inline DATALINES (14 lines) + +NOTE: Read 14 rows from DATALINES. +NOTE: Wrote rcasd_notices (14 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=rcasd_notices + +NOTE: PROC PRINT completed: 14 observations printed, 2 variables +NOTE: PROC FREQ +NOTE: ODS plot written: freq_notice_desc.spec.json +NOTE: PROC FREQ statement used. diff --git a/jenner-check/t008_rcasd_make_formats/expected/output.txt b/jenner-check/t008_rcasd_make_formats/expected/output.txt new file mode 100644 index 0000000..689e1ec --- /dev/null +++ b/jenner-check/t008_rcasd_make_formats/expected/output.txt @@ -0,0 +1,35 @@ + RCASD notice codes recoded via $rcasd_notice_type. + +Notice_type Notice_desc +101 Condominium registration application +203 Notice of foreclosure +211 Right of first refusal +212 TOPA letter of interest +101 Condominium registration application +204 DC opportunity to purchase act (DOPA) notice +102 Not a housing accommodation exemption application +211 Right of first refusal +207 Tenant organization registration application +103 Vacancy exemption application +214 TOPA complaints +211 Right of first refusal +105 Tenant election application +201 Notice of transfer + + Distribution of RCASD notice types + + The FREQ Procedure + +Notice_desc Frequency Percent +-------------------------------------------------------------------------- +Right of first refusal 3 21.43 +Condominium registration application 2 14.29 +DC opportunity to purchase act (DOPA) notice 1 7.14 +Not a housing accommodation exemption application 1 7.14 +Notice of foreclosure 1 7.14 +Notice of transfer 1 7.14 +TOPA complaints 1 7.14 +TOPA letter of interest 1 7.14 +Tenant election application 1 7.14 +Tenant organization registration application 1 7.14 +Vacancy exemption application 1 7.14 diff --git a/jenner-check/t008_rcasd_make_formats/meta.json b/jenner-check/t008_rcasd_make_formats/meta.json new file mode 100644 index 0000000..b82e31e --- /dev/null +++ b/jenner-check/t008_rcasd_make_formats/meta.json @@ -0,0 +1,8 @@ +{ + "bundle": "t008_rcasd_make_formats", + "source_file": "Prog/RCASD/Rcasd_make_formats.sas", + "source_blob_sha": "89d7fec060401399ada371ae89a20b731109e95a", + "source_commit": "ac221b855c1daf69fc24fe67326506b65c76799a", + "tier": "real_data", + "notes": "$rcasd_notice_type PROC FORMAT VALUE entries reproduced verbatim for the codes exercised (source defines the full code list); library=DHCD dropped to WORK; notice codes recoded via PUT() and tabulated with PROC FREQ order=freq, following the source's own Notice_type formatting usage." +} diff --git a/jenner-check/t008_rcasd_make_formats/script.sas b/jenner-check/t008_rcasd_make_formats/script.sas new file mode 100644 index 0000000..238ee92 --- /dev/null +++ b/jenner-check/t008_rcasd_make_formats/script.sas @@ -0,0 +1,70 @@ +/************************************************************************** + Bundle: t008_rcasd_make_formats + Source: Prog/RCASD/Rcasd_make_formats.sas (NeighborhoodInfoDC/DHCD) + Original: P. Tatian, NeighborhoodInfo DC, 12/21/15 + + Description (from source): Make formats for the RCASD (Rental Conversion + and Sale Division) weekly report data. The $rcasd_notice_type format maps + the three-digit RCASD notice-type codes to their descriptions; this bundle + recodes a sample of notice-type codes into their labels with PUT() and + tabulates them with PROC FREQ. + + Adaptation: the original writes the format to a permanent catalog + (library=DHCD, defined by the DCData macro framework); here it builds in + the default (WORK) catalog so the program is self-contained. The + $rcasd_notice_type VALUE entries below are reproduced verbatim from the + source for the codes the sample uses (the full source format defines the + complete code list). The recode and PROC FREQ follow the source's own + usage pattern (Notice_type is formatted with $rcasd_notice_type.). +**************************************************************************/ + +proc format; + + value $rcasd_notice_type + "101" = "Condominium registration application" + "102" = "Not a housing accommodation exemption application" + "103" = "Vacancy exemption application" + "105" = "Tenant election application" + "201" = "Notice of transfer" + "203" = "Notice of foreclosure" + "204" = "DC opportunity to purchase act (DOPA) notice" + "207" = "Tenant organization registration application" + "211" = "Right of first refusal" + "212" = "TOPA letter of interest" + "214" = "TOPA complaints" + other = "(unrecognized)"; + +run; + +** Sample RCASD weekly-report notice records (Notice_type as 3-char code) **; +data rcasd_notices; + length Notice_type $ 3 Notice_desc $ 120; + input Notice_type $; + Notice_desc = put( Notice_type, $rcasd_notice_type. ); + datalines; +101 +203 +211 +212 +101 +204 +102 +211 +207 +103 +214 +211 +105 +201 +; +run; + +proc print data=rcasd_notices noobs; + var Notice_type Notice_desc; + title 'RCASD notice codes recoded via $rcasd_notice_type.'; +run; + +proc freq data=rcasd_notices order=freq; + table Notice_desc / nocum; + title 'Distribution of RCASD notice types'; +run;