From b51c7233f4458ddbb6cc463a49e8c211de7c34e2 Mon Sep 17 00:00:00 2001 From: jenner-analytics <299455446+jenner-analytics@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:49:47 +0000 Subject: [PATCH] jenner-check: add 5 Jenner compatibility bundles + runner Adds a self-contained jenner-check/ directory with five bundles derived from this library's own macros and format programs: t001_chas_stattest - %chas_stattest two-estimate significance test t002_reshape_chas - %reshape_chas varlist -> positional c1..cN t003_sec8mf_addr_correct- %Sec8MF_addr_correct SELECT/WHEN address fixups t004_sec8mf_formats - Sec8MF_formats.sas PROC FORMAT catalog t005_lihtc_formats - Make_formats_lihtc.sas PROC FORMAT catalog Each bundle ships a copy of the program, a small mock input standing in for the site data set it reads, the captured log and listing, and pinned expected.json fields. A shell runner (run_jenner.sh) re-runs every bundle against api.jenneranalytics.com and checks the pinned fields. Nothing outside jenner-check/ is touched and nothing runs on merge. --- jenner-check/.gitignore | 2 + jenner-check/README.md | 76 ++++ jenner-check/run_jenner.sh | 357 ++++++++++++++++++ jenner-check/t001_chas_stattest/autoexec.sas | 2 + jenner-check/t001_chas_stattest/expected.json | 14 + .../t001_chas_stattest/expected/log.txt | 29 ++ .../t001_chas_stattest/expected/output.txt | 8 + jenner-check/t001_chas_stattest/meta.json | 7 + jenner-check/t001_chas_stattest/script.sas | 58 +++ jenner-check/t002_reshape_chas/autoexec.sas | 6 + jenner-check/t002_reshape_chas/expected.json | 14 + .../t002_reshape_chas/expected/log.txt | 21 ++ .../t002_reshape_chas/expected/output.txt | 4 + jenner-check/t002_reshape_chas/meta.json | 7 + jenner-check/t002_reshape_chas/script.sas | 146 +++++++ .../t003_sec8mf_addr_correct/autoexec.sas | 2 + .../t003_sec8mf_addr_correct/expected.json | 13 + .../t003_sec8mf_addr_correct/expected/log.txt | 21 ++ .../expected/output.txt | 7 + .../t003_sec8mf_addr_correct/meta.json | 7 + .../t003_sec8mf_addr_correct/script.sas | 63 ++++ jenner-check/t004_sec8mf_formats/autoexec.sas | 2 + .../t004_sec8mf_formats/expected.json | 15 + .../t004_sec8mf_formats/expected/log.txt | 37 ++ .../t004_sec8mf_formats/expected/output.txt | 23 ++ jenner-check/t004_sec8mf_formats/meta.json | 7 + jenner-check/t004_sec8mf_formats/script.sas | 156 ++++++++ jenner-check/t005_lihtc_formats/autoexec.sas | 2 + jenner-check/t005_lihtc_formats/expected.json | 15 + .../t005_lihtc_formats/expected/log.txt | 37 ++ .../t005_lihtc_formats/expected/output.txt | 23 ++ jenner-check/t005_lihtc_formats/meta.json | 7 + jenner-check/t005_lihtc_formats/script.sas | 102 +++++ 33 files changed, 1290 insertions(+) create mode 100644 jenner-check/.gitignore create mode 100644 jenner-check/README.md create mode 100755 jenner-check/run_jenner.sh create mode 100644 jenner-check/t001_chas_stattest/autoexec.sas create mode 100644 jenner-check/t001_chas_stattest/expected.json create mode 100644 jenner-check/t001_chas_stattest/expected/log.txt create mode 100644 jenner-check/t001_chas_stattest/expected/output.txt create mode 100644 jenner-check/t001_chas_stattest/meta.json create mode 100644 jenner-check/t001_chas_stattest/script.sas create mode 100644 jenner-check/t002_reshape_chas/autoexec.sas create mode 100644 jenner-check/t002_reshape_chas/expected.json create mode 100644 jenner-check/t002_reshape_chas/expected/log.txt create mode 100644 jenner-check/t002_reshape_chas/expected/output.txt create mode 100644 jenner-check/t002_reshape_chas/meta.json create mode 100644 jenner-check/t002_reshape_chas/script.sas create mode 100644 jenner-check/t003_sec8mf_addr_correct/autoexec.sas create mode 100644 jenner-check/t003_sec8mf_addr_correct/expected.json create mode 100644 jenner-check/t003_sec8mf_addr_correct/expected/log.txt create mode 100644 jenner-check/t003_sec8mf_addr_correct/expected/output.txt create mode 100644 jenner-check/t003_sec8mf_addr_correct/meta.json create mode 100644 jenner-check/t003_sec8mf_addr_correct/script.sas create mode 100644 jenner-check/t004_sec8mf_formats/autoexec.sas create mode 100644 jenner-check/t004_sec8mf_formats/expected.json create mode 100644 jenner-check/t004_sec8mf_formats/expected/log.txt create mode 100644 jenner-check/t004_sec8mf_formats/expected/output.txt create mode 100644 jenner-check/t004_sec8mf_formats/meta.json create mode 100644 jenner-check/t004_sec8mf_formats/script.sas create mode 100644 jenner-check/t005_lihtc_formats/autoexec.sas create mode 100644 jenner-check/t005_lihtc_formats/expected.json create mode 100644 jenner-check/t005_lihtc_formats/expected/log.txt create mode 100644 jenner-check/t005_lihtc_formats/expected/output.txt create mode 100644 jenner-check/t005_lihtc_formats/meta.json create mode 100644 jenner-check/t005_lihtc_formats/script.sas diff --git a/jenner-check/.gitignore b/jenner-check/.gitignore new file mode 100644 index 0000000..75fb4a3 --- /dev/null +++ b/jenner-check/.gitignore @@ -0,0 +1,2 @@ +*_response.json +response.json diff --git a/jenner-check/README.md b/jenner-check/README.md new file mode 100644 index 0000000..58459d6 --- /dev/null +++ b/jenner-check/README.md @@ -0,0 +1,76 @@ +# jenner-check: compatibility test bundles + +Each `tNNN_/` subdirectory is a self-contained test bundle built from SAS +code that already lives in this repository. A bundle pins the output of a +captured passing run so the same script can be re-run later and compared +against that snapshot. + +This directory is fully self-contained: nothing outside `jenner-check/` is +referenced or modified, nothing runs on merge or checkout, and deleting the +directory removes every trace of it. + +## What the runner sends — read before running + +`run_jenner.sh` uploads only a bundle's SAS **source text** — `autoexec.sas` +plus `script.sas`, and nothing else — over HTTPS to `api.jenneranalytics.com`, +where it runs and returns the log and listing. It does not read or upload any +data files, so anything sitting next to a script stays on your machine; the +only thing transmitted is the code you run, as when pasting a snippet into any +hosted tool. Nothing is sent unless you run a command yourself. To point the +runner at a different endpoint, set `JENNER_HOST`. + +## What's in here + +``` +jenner-check/ +├── README.md # this file +├── run_jenner.sh # runner (bash + curl; python3 used if present) +└── tNNN_/ + ├── script.sas # the SAS under test (derived from this repo) + ├── autoexec.sas # run options prepended to script.sas at run time + ├── expected.json # fields pinned from the captured passing run + ├── meta.json # provenance: source file, blob sha, commit + └── expected/ # human-readable snapshot of that run + ├── log.txt + └── output.txt +``` + +`meta.json` records the source path, blob sha, and commit of the script each +bundle was built from, so you can verify the copy matches your code. + +## How to run + +From inside this directory: + +```bash +./run_jenner.sh --list # show the bundles in this directory +./run_jenner.sh --all # run every bundle, verify pinned fields +./run_jenner.sh tNNN_ # run just one (use a name from --list) +``` + +For each bundle the runner submits the SAS, writes the JSON response to +`_response.json`, and compares the response against the bundle's +`expected.json` — status, exit code, and the pinned log lines. A bundle +passes only when every pinned field matches, so an `N pass, 0 fail` summary +from `--all` means the captured results still hold. + +You can also compare a saved response offline, with no network call: + +```bash +./run_jenner.sh --compare tNNN__response.json tNNN_/expected.json +``` + +Requirements: `bash` 4+ and `curl` (both ship with mainstream Linux and +macOS); `python3` for the pinned-field comparison. On Windows, run it under +WSL. + +## Removing this directory + +`git rm -r jenner-check/` (or just delete the folder). Nothing else in the +repository references it. + +--- + +`run_jenner.sh` and this README are provided under the MIT license. Each +bundle's `script.sas` is a copy of code that already lives in this repository +and remains under this repository's own license — nothing here relicenses it. diff --git a/jenner-check/run_jenner.sh b/jenner-check/run_jenner.sh new file mode 100755 index 0000000..ad7825a --- /dev/null +++ b/jenner-check/run_jenner.sh @@ -0,0 +1,357 @@ +#!/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] +# ./run_jenner.sh --compare RESPONSE.json EXPECTED.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. +# +# --compare Purely local, no network: check RESPONSE.json against the +# pinned EXPECTED.json and print one line per check. Only +# keys present in EXPECTED.json are checked (keys starting +# with "_" are provenance and ignored): status / exit_code +# must match exactly, every log_contains entry must appear +# in the response log, no log_does_not_contain entry may +# appear, output_contains entries must appear in the output, +# and pinned diagnostics lists must match exactly. Unknown +# keys print a warning and do not fail. +# Exit 0 all-match, 1 any mismatch, 2 usage/unreadable file/ +# invalid JSON, 5 python3 unavailable. +# +# 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. +# +# What "pass" means: a bundle run passes only if the POST returns HTTP 200 +# AND — when the bundle carries an expected.json and python3 is available — +# the fresh response matches every pinned field (the --compare semantics +# above). Without python3 the pinned comparison is skipped with a notice +# and the run falls back to the old HTTP/status-only behavior. +# +# Requires: bash 4+, curl. Both ship with every mainstream Linux distro +# and macOS 12+. python3 (present on virtually every modern system) is +# optional but needed for the pinned-result comparison and the pretty +# summary. 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' "$(basename "$0")" + printf ' %s --compare RESPONSE.json EXPECTED.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")" + printf ' %s --compare r.json e.json # check a response against pinned fields\n' "$(basename "$0")" + } >&2 + exit "$status" +} + +# Compare a response.json against a pinned expected.json. Purely local — +# no network. Only keys present in expected.json are checked; keys whose +# name starts with "_" are capture provenance and are ignored. Prints one +# line per check (ok/FAIL/warn). Return codes: 0 all checks match, +# 1 any mismatch, 2 unreadable file or invalid JSON, 5 python3 missing. +compare_response_to_expected() { + local resp_file=$1 expected_file=$2 + if ! command -v python3 >/dev/null 2>&1; then + printf 'error: the pinned comparison needs python3, which was not found\n' >&2 + return 5 + fi + python3 - "$resp_file" "$expected_file" <<'PY' +import json, sys + +def load(path, label): + try: + with open(path, encoding="utf-8") as f: + return json.load(f) + except (OSError, ValueError) as e: + print(f"compare: cannot read {label} file {path}: {e}", file=sys.stderr) + sys.exit(2) + +resp = load(sys.argv[1], "response") +exp = load(sys.argv[2], "expected") +if not isinstance(resp, dict) or not isinstance(exp, dict): + print("compare: both files must contain a JSON object", file=sys.stderr) + sys.exit(2) + +failures = 0 + +def ok(msg): + print(f" ok {msg}") + +def bad(msg): + global failures + failures += 1 + print(f" FAIL {msg}") + +def as_list(v): + return v if isinstance(v, list) else [v] + +log = resp.get("log") or "" +output = resp.get("output") or "" + +for key, want in exp.items(): + if key.startswith("_"): + continue # provenance (_captured_at, ...), not a check + if key in ("status", "exit_code"): + got = resp.get(key) + if got == want: + ok(f"{key} == {want!r}") + else: + bad(f"{key}: expected {want!r}, got {got!r}") + elif key == "log_contains": + for s in as_list(want): + if s in log: + ok(f"log contains {s!r}") + else: + bad(f"log_contains: log is missing {s!r}") + elif key == "log_does_not_contain": + for s in as_list(want): + if s not in log: + ok(f"log does not contain {s!r}") + else: + bad(f"log_does_not_contain: log unexpectedly contains {s!r}") + elif key == "output_contains": + for s in as_list(want): + if s in output: + ok(f"output contains {s!r}") + else: + bad(f"output_contains: output is missing {s!r}") + elif key == "diagnostics": + diag = resp.get("diagnostics") or {} + for dk, dv in (want or {}).items(): + if dk.startswith("_"): + continue + got = diag.get(dk) + if got == dv: + ok(f"diagnostics.{dk} == {dv!r}") + else: + bad(f"diagnostics.{dk}: expected {dv!r}, got {got!r}") + else: + print(f" warn unknown expected.json key {key!r} — not checked") + +if failures: + print(f"pinned comparison: {failures} check(s) FAILED") + sys.exit(1) +print("pinned comparison: all pinned checks passed") +PY +} + +# --- 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 + ;; + --compare) + # Purely local: diff a response.json against a pinned expected.json. + if [[ $# -ne 3 ]]; then + printf 'usage: %s --compare RESPONSE.json EXPECTED.json\n' "$(basename "$0")" >&2 + exit 2 + fi + _rc=0 + compare_response_to_expected "$2" "$3" || _rc=$? + exit "$_rc" + ;; + --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 + +# --- pinned-result comparison --------------------------------------------- +# A bundle that ships an expected.json only passes if the fresh response +# matches every pinned field — HTTP 200 alone is not a pass. Without +# python3 we can't parse JSON portably, so fall back to the old behavior +# and say so. +if [[ -d "$TARGET" && -f "$TARGET/expected.json" ]]; then + if command -v python3 >/dev/null 2>&1; then + printf 'Comparing response against pinned %s/expected.json\n' "${TARGET%/}" + _rc=0 + compare_response_to_expected "$OUT" "$TARGET/expected.json" || _rc=$? + if [[ $_rc -ne 0 ]]; then + printf 'Bundle FAILED the pinned comparison (see lines above)\n' >&2 + exit "$_rc" + fi + else + printf 'pinned comparison skipped (python3 not found) — HTTP/status only\n' + fi +fi diff --git a/jenner-check/t001_chas_stattest/autoexec.sas b/jenner-check/t001_chas_stattest/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t001_chas_stattest/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t001_chas_stattest/expected.json b/jenner-check/t001_chas_stattest/expected.json new file mode 100644 index 0000000..1dc64ca --- /dev/null +++ b/jenner-check/t001_chas_stattest/expected.json @@ -0,0 +1,14 @@ +{ + "_captured_at": "2026-07-10T12:32:49+00:00", + "_captured_run_id": "r_d1dffdda5a474be1871f1efc95033ca0", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Read 5 rows from DATALINES.", + "Wrote a_tests (5 rows, 12 columns).", + "Wrote a_sig (5 rows, 1 columns).", + "PROC PRINT completed: 5 observations printed, 1 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t001_chas_stattest/expected/log.txt b/jenner-check/t001_chas_stattest/expected/log.txt new file mode 100644 index 0000000..4bdcda4 --- /dev/null +++ b/jenner-check/t001_chas_stattest/expected/log.txt @@ -0,0 +1,29 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA chas_indicators + +NOTE: Processing inline DATALINES (5 lines) + +NOTE: Read 5 rows from DATALINES. +NOTE: Wrote chas_indicators (5 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA a_tests + + +NOTE: Read 5 rows from chas_indicators. +NOTE: Wrote a_tests (5 rows, 12 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA a_sig + + +NOTE: Read 5 rows from a_tests. +NOTE: Wrote a_sig (5 rows, 1 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=a_sig + +NOTE: PROC PRINT completed: 5 observations printed, 1 variables diff --git a/jenner-check/t001_chas_stattest/expected/output.txt b/jenner-check/t001_chas_stattest/expected/output.txt new file mode 100644 index 0000000..99657c9 --- /dev/null +++ b/jenner-check/t001_chas_stattest/expected/output.txt @@ -0,0 +1,8 @@ + Obs sig +----- --- + 1 *** + 2 + 3 *** + 4 + 5 + diff --git a/jenner-check/t001_chas_stattest/meta.json b/jenner-check/t001_chas_stattest/meta.json new file mode 100644 index 0000000..baeb35e --- /dev/null +++ b/jenner-check/t001_chas_stattest/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t001_chas_stattest", + "source_file": "Macros/chas_stattest.sas", + "source_blob_sha": "14f15071ebe5f5c7308e88be1bcb722f1b38aa4c", + "source_commit": "32c82cfe30277279feb8220b5ad9efe0155495ba", + "notes": "%chas_stattest reproduced verbatim: tests statistical significance of two CHAS estimates given margins of error, emitting a ***/**/* flag. Added a 5-row mock estimate/MOE table (stands in for a CHAS summary data set) and invoked the macro as the library does. Exercises DATA-step arithmetic, the chained comparison 2.576 > abs(Tstat) >= 1.96, and PROC PRINT." +} diff --git a/jenner-check/t001_chas_stattest/script.sas b/jenner-check/t001_chas_stattest/script.sas new file mode 100644 index 0000000..57ed841 --- /dev/null +++ b/jenner-check/t001_chas_stattest/script.sas @@ -0,0 +1,58 @@ +/************************************************************************** + Bundle: t001_chas_stattest + Source: Macros/chas_stattest.sas (NeighborhoodInfoDC/HUD) + + The %chas_stattest macro is reproduced verbatim from the repository. It + runs a test of statistical significance between two CHAS estimates given + their margins of error, producing a "***" / "**" / "*" / "" flag. Here we + supply a small mock table of estimate/MOE pairs (the real caller reads a + CHAS summary data set) and invoke the macro exactly as the library does. +**************************************************************************/ + +/** ---- Macro reproduced verbatim from Macros/chas_stattest.sas ---- **/ + +%macro chas_stattest (indata,est1,moe1,est2,moe2); + +data a_tests ; + set &indata.; + + E1 = &est1.; + M1 = &moe1.; + E2 = &est2.; + M2 = &moe2.; + +Vdiff = sqrt( ((M1/1.96)**2) + ((M2/1.96)**2) ); +Tstat = ( E1 - E2 ) / Vdiff ; + +if abs(Tstat) >= 2.576 then sig = "***"; +else if 2.576 > abs(Tstat) >= 1.96 then sig = "**"; +else if 1.96 > abs(Tstat) >= 1.645 then sig = "*"; +else if abs(Tstat) < 1.645 then sig = ""; + +run; + +data a_sig; + set a_tests; + keep sig; +run; + +proc print data = a_sig; run; + +%mend chas_stattest; + +/** ---- Mock CHAS estimate/MOE pairs (stands in for a summary data set) ---- **/ + +data chas_indicators; + input geoid $ est_a moe_a est_b moe_b; + datalines; +11001 4200 310 3100 290 +11003 1850 240 1810 250 +11005 9600 410 7300 380 +11007 2200 200 2130 210 +11009 5400 350 5390 360 +; +run; + +/** ---- Invoke exactly as the library does ---- **/ + +%chas_stattest( chas_indicators, est_a, moe_a, est_b, moe_b ) diff --git a/jenner-check/t002_reshape_chas/autoexec.sas b/jenner-check/t002_reshape_chas/autoexec.sas new file mode 100644 index 0000000..20681da --- /dev/null +++ b/jenner-check/t002_reshape_chas/autoexec.sas @@ -0,0 +1,6 @@ +/* cap input rows for the captured run */ +options obs=100; + +/* Reshape_CHAS.sas reads the global &chas_in as its input data set, + exactly as Export_CHAS_csv.sas sets it before calling the macro. */ +%let chas_in = chas_dc; diff --git a/jenner-check/t002_reshape_chas/expected.json b/jenner-check/t002_reshape_chas/expected.json new file mode 100644 index 0000000..0a0f858 --- /dev/null +++ b/jenner-check/t002_reshape_chas/expected.json @@ -0,0 +1,14 @@ +{ + "_captured_at": "2026-07-10T12:32:49+00:00", + "_captured_run_id": "r_8723ac04c6ff418eaeba8348e0d8e75f", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Read 1 rows from DATALINES.", + "Wrote chas_dc (1 rows, 6 columns).", + "Wrote table1a_row3 (1 rows, 6 columns).", + "PROC PRINT completed: 1 observations printed, 6 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t002_reshape_chas/expected/log.txt b/jenner-check/t002_reshape_chas/expected/log.txt new file mode 100644 index 0000000..082bcd8 --- /dev/null +++ b/jenner-check/t002_reshape_chas/expected/log.txt @@ -0,0 +1,21 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA chas_dc + +NOTE: Processing inline DATALINES (1 lines) + +NOTE: Read 1 rows from DATALINES. +NOTE: Wrote chas_dc (1 rows, 6 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA table1a_row3 + + +NOTE: Read 1 rows from chas_dc. +NOTE: Wrote table1a_row3 (1 rows, 6 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=table1a_row3 + +NOTE: PROC PRINT completed: 1 observations printed, 6 variables diff --git a/jenner-check/t002_reshape_chas/expected/output.txt b/jenner-check/t002_reshape_chas/expected/output.txt new file mode 100644 index 0000000..4991902 --- /dev/null +++ b/jenner-check/t002_reshape_chas/expected/output.txt @@ -0,0 +1,4 @@ + Obs c1 c2 c3 c4 c5 c6 +----- ------ ------ ----- ----- ----- ----- + 1 120400 128900 41200 39800 58700 61300 + diff --git a/jenner-check/t002_reshape_chas/meta.json b/jenner-check/t002_reshape_chas/meta.json new file mode 100644 index 0000000..8c0b6e5 --- /dev/null +++ b/jenner-check/t002_reshape_chas/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t002_reshape_chas", + "source_file": "Macros/Reshape_CHAS.sas", + "source_blob_sha": "3d5816290c34c047f1d4f384bfb077f366716352", + "source_commit": "32c82cfe30277279feb8220b5ad9efe0155495ba", + "notes": "%reshape_chas reproduced verbatim: counts a space-delimited varlist with %sysfunc(countw()), then %scan pulls each variable into a positional c1..cN column. Added a 1-row mock CHAS summary (chas_dc, referenced via the global &chas_in exactly as Export_CHAS_csv.sas sets it) and called the macro for table 1a / row 3 with the same 6-variable renter-unit list that program uses. Exercises the %if &var_cnt=6 macro branch, %scan token extraction, and the positional keep list." +} diff --git a/jenner-check/t002_reshape_chas/script.sas b/jenner-check/t002_reshape_chas/script.sas new file mode 100644 index 0000000..a47e9c7 --- /dev/null +++ b/jenner-check/t002_reshape_chas/script.sas @@ -0,0 +1,146 @@ +/************************************************************************** + Bundle: t002_reshape_chas + Source: Macros/Reshape_CHAS.sas (NeighborhoodInfoDC/HUD) + + The %reshape_chas macro is reproduced verbatim from the repository. It + uses %sysfunc(countw()) to count the variables in a space-delimited + varlist, then %scan to pull each one and copy it into a positional + c1..cN column, keeping only those. Here we supply a small mock CHAS + summary data set (chas_dc, named via the global &chas_in as in + Export_CHAS_csv.sas) and call the macro exactly as that export program + does for row 3 of table 1a (a 6-variable list). +**************************************************************************/ + +/** ---- Macro reproduced verbatim from Macros/Reshape_CHAS.sas ---- **/ + +%macro reshape_chas (indata,table,row,varlist); + +%let var_cnt=%sysfunc(countw(&varlist.)); + +data table&table._row&row.; + set &chas_in.; + + %if &var_cnt. = 8 %then %do; + %let c1 = %scan(&varlist., 1, " ") ; + %let c2 = %scan(&varlist., 2, " ") ; + %let c3 = %scan(&varlist., 3, " ") ; + %let c4 = %scan(&varlist., 4, " ") ; + %let c5 = %scan(&varlist., 5, " ") ; + %let c6 = %scan(&varlist., 6, " ") ; + %let c7 = %scan(&varlist., 7, " ") ; + %let c8 = %scan(&varlist., 8, " ") ; + c1 = &c1.; + c2 = &c2.; + c3 = &c3.; + c4 = &c4.; + c5 = &c5.; + c6 = &c6.; + c7 = &c7.; + c8 = &c8.; + keep c1 c2 c3 c4 c5 c6 c7 c8; + %end; + + %else %if &var_cnt. = 7 %then %do; + %let c1 = %scan(&varlist., 1, " ") ; + %let c2 = %scan(&varlist., 2, " ") ; + %let c3 = %scan(&varlist., 3, " ") ; + %let c4 = %scan(&varlist., 4, " ") ; + %let c5 = %scan(&varlist., 5, " ") ; + %let c6 = %scan(&varlist., 6, " ") ; + %let c7 = %scan(&varlist., 7, " ") ; + c1 = &c1.; + c2 = &c2.; + c3 = &c3.; + c4 = &c4.; + c5 = &c5.; + c6 = &c6.; + c7 = &c7.; + keep c1 c2 c3 c4 c5 c6 c7; + %end; + + %else %if &var_cnt. = 6 %then %do; + %let c1 = %scan(&varlist., 1, " ") ; + %let c2 = %scan(&varlist., 2, " ") ; + %let c3 = %scan(&varlist., 3, " ") ; + %let c4 = %scan(&varlist., 4, " ") ; + %let c5 = %scan(&varlist., 5, " ") ; + %let c6 = %scan(&varlist., 6, " ") ; + c1 = &c1.; + c2 = &c2.; + c3 = &c3.; + c4 = &c4.; + c5 = &c5.; + c6 = &c6.; + keep c1 c2 c3 c4 c5 c6 ; + %end; + + %else %if &var_cnt. = 5 %then %do; + %let c1 = %scan(&varlist., 1, " ") ; + %let c2 = %scan(&varlist., 2, " ") ; + %let c3 = %scan(&varlist., 3, " ") ; + %let c4 = %scan(&varlist., 4, " ") ; + %let c5 = %scan(&varlist., 5, " ") ; + c1 = &c1.; + c2 = &c2.; + c3 = &c3.; + c4 = &c4.; + c5 = &c5.; + keep c1 c2 c3 c4 c5 ; + %end; + + %else %if &var_cnt. = 4 %then %do; + %let c1 = %scan(&varlist., 1, " ") ; + %let c2 = %scan(&varlist., 2, " ") ; + %let c3 = %scan(&varlist., 3, " ") ; + %let c4 = %scan(&varlist., 4, " ") ; + c1 = &c1.; + c2 = &c2.; + c3 = &c3.; + c4 = &c4.; + keep c1 c2 c3 c4; + %end; + + %else %if &var_cnt. = 3 %then %do; + %let c1 = %scan(&varlist., 1, " ") ; + %let c2 = %scan(&varlist., 2, " ") ; + %let c3 = %scan(&varlist., 3, " ") ; + c1 = &c1.; + c2 = &c2.; + c3 = &c3.; + keep c1 c2 c3 ; + %end; + + %else %if &var_cnt. = 2 %then %do; + %let c1 = %scan(&varlist., 1, " ") ; + %let c2 = %scan(&varlist., 2, " ") ; + c1 = &c1.; + c2 = &c2.; + keep c1 c2; + %end; + + %else %if &var_cnt. = 1 %then %do; + %let c1 = %scan(&varlist., 1, " ") ; + c1 = &c1.; + keep c1 ; + %end; + +run; + +%mend reshape_chas; + +/** ---- Mock CHAS summary row (columns match the row-3 varlist below) ---- **/ + +data chas_dc; + input renter_unit_tot_2006_10 renter_unit_tot_2012_16 + renter_unit_aff30_2006_10 renter_unit_aff30_2012_16 + renter_unit_aff50_2006_10 renter_unit_aff50_2012_16; + datalines; +120400 128900 41200 39800 58700 61300 +; +run; + +/** ---- Invoke as Export_CHAS_csv.sas does (table 1a, row 3, 6 vars) ---- **/ + +%reshape_chas(&chas_in.,1a,3,renter_unit_tot_2006_10 renter_unit_tot_2012_16 renter_unit_aff30_2006_10 renter_unit_aff30_2012_16 renter_unit_aff50_2006_10 renter_unit_aff50_2012_16); + +proc print data=table1a_row3; run; diff --git a/jenner-check/t003_sec8mf_addr_correct/autoexec.sas b/jenner-check/t003_sec8mf_addr_correct/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t003_sec8mf_addr_correct/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t003_sec8mf_addr_correct/expected.json b/jenner-check/t003_sec8mf_addr_correct/expected.json new file mode 100644 index 0000000..ee7e27f --- /dev/null +++ b/jenner-check/t003_sec8mf_addr_correct/expected.json @@ -0,0 +1,13 @@ +{ + "_captured_at": "2026-07-10T12:32:49+00:00", + "_captured_run_id": "r_94893fa75a0e4e43ae8564f6b3870d47", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "Wrote sec8mf_contracts (4 rows, 2 columns).", + "Wrote sec8mf_corrected (4 rows, 2 columns).", + "PROC PRINT completed: 4 observations printed, 2 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t003_sec8mf_addr_correct/expected/log.txt b/jenner-check/t003_sec8mf_addr_correct/expected/log.txt new file mode 100644 index 0000000..89ac758 --- /dev/null +++ b/jenner-check/t003_sec8mf_addr_correct/expected/log.txt @@ -0,0 +1,21 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA sec8mf_contracts + +NOTE: Processing inline DATALINES (4 lines) + +NOTE: Read 4 rows from DATALINES. +NOTE: Wrote sec8mf_contracts (4 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA sec8mf_corrected + + +NOTE: Read 4 rows from sec8mf_contracts. +NOTE: Wrote sec8mf_corrected (4 rows, 2 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=sec8mf_corrected + +NOTE: PROC PRINT completed: 4 observations printed, 2 variables diff --git a/jenner-check/t003_sec8mf_addr_correct/expected/output.txt b/jenner-check/t003_sec8mf_addr_correct/expected/output.txt new file mode 100644 index 0000000..11eb50e --- /dev/null +++ b/jenner-check/t003_sec8mf_addr_correct/expected/output.txt @@ -0,0 +1,7 @@ + Obs state_code address_line1_text +----- ---------- ------------------------- + 1 DC 2518 17th ST NW + 2 DC 324 ANACOSTIA ROAD SE + 3 DC 1500 MASSACHUSETTS AVE NW + 4 MD 324 ANACOSTIA AVE SE + diff --git a/jenner-check/t003_sec8mf_addr_correct/meta.json b/jenner-check/t003_sec8mf_addr_correct/meta.json new file mode 100644 index 0000000..fcc98d0 --- /dev/null +++ b/jenner-check/t003_sec8mf_addr_correct/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t003_sec8mf_addr_correct", + "source_file": "Macros/Sec8MF_addr_correct.sas", + "source_blob_sha": "f1634e125e455342682285a826222b09de8efbbd", + "source_commit": "32c82cfe30277279feb8220b5ad9efe0155495ba", + "notes": "%Sec8MF_addr_correct reproduced verbatim: an autocall macro that patches known-bad DC Section 8 MF street addresses via a SELECT ( address_line1_text ) block guarded by state_code = 'DC'. Added a 4-row mock contract table hitting both WHEN corrections, a DC address that falls through the otherwise branch, and a non-DC row, then invoked the macro inside a DATA step exactly as Sec8MF_dmvw.sas does. Confirms the two corrections apply, the pass-through and state guard leave rows unchanged." +} diff --git a/jenner-check/t003_sec8mf_addr_correct/script.sas b/jenner-check/t003_sec8mf_addr_correct/script.sas new file mode 100644 index 0000000..c8ff945 --- /dev/null +++ b/jenner-check/t003_sec8mf_addr_correct/script.sas @@ -0,0 +1,63 @@ +/************************************************************************** + Bundle: t003_sec8mf_addr_correct + Source: Macros/Sec8MF_addr_correct.sas (NeighborhoodInfoDC/HUD) + + The %Sec8MF_addr_correct macro is reproduced verbatim from the + repository. It is an autocall macro that Sec8MF_dmvw.sas drops inside a + DATA step to fix a handful of known-bad HUD Section 8 multifamily street + addresses (DC only), using a SELECT ( address_line1_text ) block. Here we + supply a small mock contract table exercising both corrected addresses, + a DC address that should pass through unchanged, and a non-DC row (so the + outer "if state_code = 'DC'" guard is exercised too), then invoke the + macro inside a DATA step exactly as Sec8MF_dmvw does. +**************************************************************************/ + +/** ---- Macro reproduced verbatim from Macros/Sec8MF_addr_correct.sas ---- **/ + +%macro Sec8MF_addr_correct; + + ** DC addresses only **; + + if state_code = "DC" then do; + + select ( address_line1_text ); + + %** Add a new WHEN statement for each address to be corrected **; + + when ( "2518 NW 17th ST NW" ) + address_line1_text = "2518 17th ST NW"; + + when ( "324 ANACOSTIA AVE SE" ) + address_line1_text = "324 ANACOSTIA ROAD SE"; + + %** Do not change below this line **; + + otherwise /** No correction **/; + + end; + + end; + +%mend Sec8MF_addr_correct; + +/** ---- Mock Section 8 MF contract addresses ---- **/ + +data sec8mf_contracts; + length state_code $2 address_line1_text $40; + input state_code $ address_line1_text $char40.; + datalines; +DC 2518 NW 17th ST NW +DC 324 ANACOSTIA AVE SE +DC 1500 MASSACHUSETTS AVE NW +MD 324 ANACOSTIA AVE SE +; +run; + +/** ---- Apply the corrections inside a DATA step, as Sec8MF_dmvw does ---- **/ + +data sec8mf_corrected; + set sec8mf_contracts; + %Sec8MF_addr_correct +run; + +proc print data=sec8mf_corrected; var state_code address_line1_text; run; diff --git a/jenner-check/t004_sec8mf_formats/autoexec.sas b/jenner-check/t004_sec8mf_formats/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t004_sec8mf_formats/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t004_sec8mf_formats/expected.json b/jenner-check/t004_sec8mf_formats/expected.json new file mode 100644 index 0000000..56b2648 --- /dev/null +++ b/jenner-check/t004_sec8mf_formats/expected.json @@ -0,0 +1,15 @@ +{ + "_captured_at": "2026-07-10T12:32:49+00:00", + "_captured_run_id": "r_2f93f420ecfc4ac389ab4ea2470c0012", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "FORMAT $s8stat defined (6 ranges).", + "FORMAT $s8pgmnm defined (24 ranges).", + "FORMAT s8ui2cat defined (4 ranges).", + "Wrote demo (3 rows, 6 columns).", + "PROC PRINT completed: 3 observations printed, 6 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t004_sec8mf_formats/expected/log.txt b/jenner-check/t004_sec8mf_formats/expected/log.txt new file mode 100644 index 0000000..ffc6127 --- /dev/null +++ b/jenner-check/t004_sec8mf_formats/expected/log.txt @@ -0,0 +1,37 @@ +NOTE: Option OBS changed to 100. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT $s8stat defined (6 ranges). +NOTE: FORMAT $s8exqtr defined (4 ranges). +NOTE: FORMAT $s8docty defined (9 ranges). +NOTE: FORMAT $s8pgmnm defined (24 ranges). +NOTE: FORMAT $s8pgmgr defined (14 ranges). +NOTE: FORMAT $s8prfin defined (6 ranges). +NOTE: FORMAT $s8owmgt defined (4 ranges). +NOTE: FORMAT $s8prpct defined (10 ranges). +NOTE: FORMAT s8ui4cat defined (4 ranges). +NOTE: FORMAT s8ui2cat defined (4 ranges). +NOTE: PROC CATALOG catalog=WORK.FORMATS + +NOTE: MODIFY: Entry S8STAT description set to 'S8/MF TRACS contract status' +NOTE: MODIFY: Entry S8EXQTR description set to 'S8/MF Contract expiration quarter' +NOTE: MODIFY: Entry S8DOCTY description set to 'S8/MF Document type of contract' +NOTE: MODIFY: Entry S8PGMNM description set to 'S8/MF Program name' +NOTE: MODIFY: Entry S8PGMGR description set to 'S8/MF Program type group' +NOTE: MODIFY: Entry S8PRFIN description set to 'S8/MF Property financing' +NOTE: MODIFY: Entry S8OWMGT description set to 'S8/MF Owner/manager company type' +NOTE: MODIFY: Entry S8PRPCT description set to 'S8/MF Property category' +NOTE: MODIFY: Entry S8UI4CAT description set to 'S8/MF UI status code (4 categories)' +NOTE: MODIFY: Entry S8UI2CAT description set to 'S8/MF UI status code (2 categories)' +NOTE: DATA demo + +NOTE: Processing inline DATALINES (3 lines) + +NOTE: Read 3 rows from DATALINES. +NOTE: Wrote demo (3 rows, 6 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=demo + +NOTE: PROC PRINT completed: 3 observations printed, 6 variables diff --git a/jenner-check/t004_sec8mf_formats/expected/output.txt b/jenner-check/t004_sec8mf_formats/expected/output.txt new file mode 100644 index 0000000..b145609 --- /dev/null +++ b/jenner-check/t004_sec8mf_formats/expected/output.txt @@ -0,0 +1,23 @@ +Contents of Catalog WORK.FORMATS +Name Type Description +---- ---- ----------- +S8DOCTY FORMATC S8/MF Document type of contract +S8EXQTR FORMATC S8/MF Contract expiration quarter +S8OWMGT FORMATC S8/MF Owner/manager company type +S8PGMGR FORMATC S8/MF Program type group +S8PGMNM FORMATC S8/MF Program name +S8PRFIN FORMATC S8/MF Property financing +S8PRPCT FORMATC S8/MF Property category +S8STAT FORMATC S8/MF TRACS contract status +S8UI2CAT FORMAT S8/MF UI status code (2 categories) +S8UI4CAT FORMAT S8/MF UI status code (4 categories) + +10 entries in catalog. + + + Obs tracs_status status_desc program_type_group program_desc ui_status ui_desc +----- ------------ ----------- ------------------ ------------------ --------- ------- + 1 A Active 202 Section 202 1 Active + 2 T Terminated LMSA S8 Loan Management 4 Expired + 3 X Expired PRAC PRAC 202/811 2 Active + diff --git a/jenner-check/t004_sec8mf_formats/meta.json b/jenner-check/t004_sec8mf_formats/meta.json new file mode 100644 index 0000000..d394ea9 --- /dev/null +++ b/jenner-check/t004_sec8mf_formats/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t004_sec8mf_formats", + "source_file": "Prog/Sec8MF/Sec8MF_formats.sas", + "source_blob_sha": "c4d6832173e811fc2ba1bd6c30514d78e966752b", + "source_commit": "32c82cfe30277279feb8220b5ad9efe0155495ba", + "notes": "PROC FORMAT value maps reproduced verbatim (10 formats for HUD Section 8 MF data: contract status, expiration quarter, document type, program name/group, financing, owner/manager, property category, and the 4- and 2-category UI status codes). Only change vs upstream: format library HUD -> WORK so the run is self-contained; the PROC CATALOG description-modify block follows to WORK.formats. Added a small DATA step applying $s8stat, $s8pgmgr and the multi-value s8ui2cat (1,2,3='Active') via PUT so the listing shows them resolving. Exercises PROC FORMAT char/numeric value ranges, multi-value ranges, PROC CATALOG modify + CONTENTS, and PUT-based format application." +} diff --git a/jenner-check/t004_sec8mf_formats/script.sas b/jenner-check/t004_sec8mf_formats/script.sas new file mode 100644 index 0000000..fe67e60 --- /dev/null +++ b/jenner-check/t004_sec8mf_formats/script.sas @@ -0,0 +1,156 @@ +/************************************************************************** + Bundle: t004_sec8mf_formats + Source: Prog/Sec8MF/Sec8MF_formats.sas (NeighborhoodInfoDC/HUD) + + The PROC FORMAT value definitions are reproduced verbatim from the + repository. The only change from the upstream program is the format + library target: HUD (the site DCData library) becomes WORK so the run + is self-contained. Every value map and the PROC CATALOG descriptions + are exactly as written upstream. After building the catalog we apply a + few of the formats via PUT so the listing shows them resolving. +**************************************************************************/ + +proc format library=WORK; + + value $s8stat + 'A' = 'Active' + 'E' = 'Executed' + 'P' = 'Pending' + 'S' = 'Suspended' + 'T' = 'Terminated' + 'X' = 'Expired'; + + value $s8exqtr + 'Q1' = 'Oct - Dec' + 'Q2' = 'Jan - Mar' + 'Q3' = 'Apr - Jun' + 'Q4' = 'Jul - Sep'; + + /* + value $yesno + 'Y' = 'Yes' + 'N' = 'No'; + */ + + value $s8docty + 'ACC' = 'Annual Contributions Contract' + 'AHAP' = 'Housing Assistance Payments Contract' + 'APRAC' = 'APRAC' + 'HAP' = 'Housing Assistance Payments' + 'PAC' = 'PAC' + 'PRAC' = 'Project Rental Assistance Contract' + 'RAP' = 'Rental Assistance Program' + 'SCHAP' = 'Senior Citizen Homeowner Assistance Program' + 'SUP' = 'Rent Supplement'; + + value $s8pgmnm + '202/162 MR' = '202/162 MR' + '202/162 NC' = '202/162 NC' + '202/162 SR' = '202/162 SR' + '202/8 NC' = '202/8 NC' + '202/8 SR' = '202/8 SR' + '515/8 NC' = '515/8 NC' + '515/8 SR' = '515/8 SR' + 'HFDA/8 NC' = 'HFDA/8 NC' + 'HFDA/8 SR' = 'HFDA/8 SR' + 'LMSA' = 'LMSA' + 'Old 202' = 'Old 202' + 'PAC/202' = 'PAC/202' + 'PAC/811' = 'PAC/811' + 'PD/8 Existing' = 'PD/8 Existing' + 'PD/8 MR' = 'PD/8 MR' + 'PD/8 SR' = 'PD/8 SR' + 'PRAC/202' = 'PRAC/202' + 'PRAC/811' = 'PRAC/811' + 'Preservation' = 'Preservation' + 'RAP' = 'Rental Assistance Program' + 'Rent Supp' = 'Rent Supplement' + 'Sec 8 NC' = 'Sec 8 New Construction' + 'Sec 8 SR' = 'Sec 8 Substantial Rehabilitation' + 'UnasstPrj SCHAP' = 'UnasstPrj SCHAP'; + + value $s8pgmgr + '202' = 'Section 202' + '515' = 'S8 FmHA' + 'HFDA' = 'S8 State Agency' + 'LMSA' = 'S8 Loan Management' + 'PAC' = 'PAC 202/811' + 'PD' = 'S8 Property Disposition' + 'PRAC' = 'PRAC 202/811' + 'PRES' = 'S8 Preservation' + 'RAP' = 'Sec. 236 Rental Assistance Program' + 'S8NC' = 'Other S8 New Construction' + 'S8SR' = 'Other S8 Rehabilitation' + 'SUPP' = 'Rent Supplement' + 'SC' = 'Service Coordinator' + 'UNK' = 'Unknown'; + + value $s8prfin + '202811' = '202/811' + 'FLEXSB' = 'Flexible Subsidy' + 'HUDHLD' = 'HUD Held' + 'HUDOWN' = 'HUD Owned' + 'INSURD' = 'Insured' + 'NONINS' = 'Non-Insured'; + + value $s8owmgt + 'LD' = 'Limited Dividend' + 'NP' = 'Non-Profit' + 'OT' = 'Other' + 'PM' = 'Profit Motivated'; + + value $s8prpct + '202811' = '202/811' + 'HUDHLD' = 'HUD Held' + 'HUDOWN' = 'HUD Owned' + 'INSPRE' = 'Insured - Previously Subsidized' + 'INSSUB' = 'Insured - Subsidized' + 'OTSSRO' = 'Other Subsidy Servicing Required (Only)' + 'SUB202' = 'Subsidized - Previously 202/811' + 'SUBINS' = 'Subsidized - Previously Insured' + 'SUBNOH' = 'Subsidized, No HUD Financing' + 'USERES' = 'Use Restriction'; + + value s8ui4cat + 1 = 'Active-New' + 2 = 'Active-Renewed' + 3 = 'Active' + 4 = 'Expired'; + + value s8ui2cat + 1,2,3 = 'Active' + 4 = 'Expired'; + +run; + +proc catalog catalog=WORK.formats; + modify s8stat (desc="S8/MF TRACS contract status") /et=formatc; + modify s8exqtr (desc="S8/MF Contract expiration quarter") /et=formatc; + modify s8docty (desc="S8/MF Document type of contract") /et=formatc; + modify s8pgmnm (desc="S8/MF Program name") /et=formatc; + modify s8pgmgr (desc="S8/MF Program type group") /et=formatc; + modify s8prfin (desc="S8/MF Property financing") /et=formatc; + modify s8owmgt (desc="S8/MF Owner/manager company type") /et=formatc; + modify s8prpct (desc="S8/MF Property category") /et=formatc; + modify s8ui4cat (desc="S8/MF UI status code (4 categories)") /et=format; + modify s8ui2cat (desc="S8/MF UI status code (2 categories)") /et=format; + contents; + quit; + +run; + +/** ---- Apply a few of the formats so the listing shows them resolving ---- **/ + +data demo; + input tracs_status $ program_type_group $ ui_status; + status_desc = put(tracs_status, $s8stat.); + program_desc = put(program_type_group, $s8pgmgr.); + ui_desc = put(ui_status, s8ui2cat.); + datalines; +A 202 1 +T LMSA 4 +X PRAC 2 +; +run; + +proc print data=demo; var tracs_status status_desc program_type_group program_desc ui_status ui_desc; run; diff --git a/jenner-check/t005_lihtc_formats/autoexec.sas b/jenner-check/t005_lihtc_formats/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t005_lihtc_formats/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t005_lihtc_formats/expected.json b/jenner-check/t005_lihtc_formats/expected.json new file mode 100644 index 0000000..4c75ba6 --- /dev/null +++ b/jenner-check/t005_lihtc_formats/expected.json @@ -0,0 +1,15 @@ +{ + "_captured_at": "2026-07-10T12:32:49+00:00", + "_captured_run_id": "r_a55dc0ecf05c4d1785e3eb68fa63fed4", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "FORMAT lihtc_reg defined (4 ranges).", + "FORMAT lihtc_inc_ceil defined (3 ranges).", + "FORMAT $lihtc_record_stat defined (3 ranges).", + "Wrote demo (3 rows, 8 columns).", + "PROC PRINT completed: 3 observations printed, 8 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t005_lihtc_formats/expected/log.txt b/jenner-check/t005_lihtc_formats/expected/log.txt new file mode 100644 index 0000000..8147783 --- /dev/null +++ b/jenner-check/t005_lihtc_formats/expected/log.txt @@ -0,0 +1,37 @@ +NOTE: Option OBS changed to 100. +NOTE: PROC FORMAT library=WORK + +NOTE: FORMAT lihtc_reg defined (4 ranges). +NOTE: FORMAT lihtc_inc_ceil defined (3 ranges). +NOTE: FORMAT lihtc_yr_pis defined (2 ranges). +NOTE: FORMAT lihtc_rentassist defined (5 ranges). +NOTE: FORMAT lihtc_type defined (4 ranges). +NOTE: FORMAT lihtc_credit defined (4 ranges). +NOTE: FORMAT lihtc_metro defined (3 ranges). +NOTE: FORMAT lihtc_dda defined (5 ranges). +NOTE: FORMAT lihtc_nlm_reason defined (3 ranges). +NOTE: FORMAT $lihtc_record_stat defined (3 ranges). +NOTE: PROC CATALOG catalog=WORK.FORMATS + +NOTE: MODIFY: Entry LIHTC_REG description set to 'LIHTC census region' +NOTE: MODIFY: Entry LIHTC_INC_CEIL description set to 'LIHTC income ceiling' +NOTE: MODIFY: Entry LIHTC_YR_PIS description set to 'LIHTC year placed in service' +NOTE: MODIFY: Entry LIHTC_RENTASSIST description set to 'LIHTC rental assistance contract' +NOTE: MODIFY: Entry LIHTC_TYPE description set to 'LIHTC type of construction' +NOTE: MODIFY: Entry LIHTC_CREDIT description set to 'LIHTC type of credit' +NOTE: MODIFY: Entry LIHTC_METRO description set to 'LIHTC metro area' +NOTE: MODIFY: Entry LIHTC_DDA description set to 'LIHTC DDA status' +NOTE: MODIFY: Entry LIHTC_NLM_REASON description set to 'LIHTC reason prop. no longer monitored' +NOTE: MODIFY: Entry LIHTC_RECORD_STAT description set to 'LIHTC rec. status compared to prev. db' +NOTE: DATA demo + +NOTE: Processing inline DATALINES (3 lines) + +NOTE: Read 3 rows from DATALINES. +NOTE: Wrote demo (3 rows, 8 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=demo + +NOTE: PROC PRINT completed: 3 observations printed, 8 variables diff --git a/jenner-check/t005_lihtc_formats/expected/output.txt b/jenner-check/t005_lihtc_formats/expected/output.txt new file mode 100644 index 0000000..d45697e --- /dev/null +++ b/jenner-check/t005_lihtc_formats/expected/output.txt @@ -0,0 +1,23 @@ +Contents of Catalog WORK.FORMATS +Name Type Description +---- ---- ----------- +LIHTC_CREDIT FORMAT LIHTC type of credit +LIHTC_DDA FORMAT LIHTC DDA status +LIHTC_INC_CEIL FORMAT LIHTC income ceiling +LIHTC_METRO FORMAT LIHTC metro area +LIHTC_NLM_REASON FORMAT LIHTC reason prop. no longer monitored +LIHTC_RECORD_STAT FORMATC LIHTC rec. status compared to prev. db +LIHTC_REG FORMAT LIHTC census region +LIHTC_RENTASSIST FORMAT LIHTC rental assistance contract +LIHTC_TYPE FORMAT LIHTC type of construction +LIHTC_YR_PIS FORMAT LIHTC year placed in service + +10 entries in catalog. + + + Obs region region_desc credit credit_desc inc_ceil ceil_desc rec_stat rec_desc +----- ------ ----------- ------ ------------------------------------ -------- --------- -------- --------------------------------------------- + 1 1 Northeast 2 9 percent (70 percent present value) 1 50% AMI N New + 2 3 South 3 Both 4 percent and 9 percent 2 60% AMI U Updated + 3 4 West 1 4 percent (30 percent present value) 1 50% AMI X Existing (unchanged from previous DB version) + diff --git a/jenner-check/t005_lihtc_formats/meta.json b/jenner-check/t005_lihtc_formats/meta.json new file mode 100644 index 0000000..a1b74c5 --- /dev/null +++ b/jenner-check/t005_lihtc_formats/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t005_lihtc_formats", + "source_file": "Prog/LIHTC/Make_formats_lihtc.sas", + "source_blob_sha": "bd161c4a01827037770e82bad5913bba7ffa767c", + "source_commit": "32c82cfe30277279feb8220b5ad9efe0155495ba", + "notes": "PROC FORMAT value maps for the HUD LIHTC data sets reproduced verbatim (10 formats: census region, income ceiling, year placed in service, rental-assistance contract, construction type, credit type, metro area, DDA status, no-longer-monitored reason, and the char record-status format). These mix plain numeric maps with SAS special-missing keys (.n, .u, .c) and one character format. Only change vs upstream: format library HUD -> WORK so the run is self-contained; the PROC CATALOG entrytype-modify block follows to WORK.Formats. Added a small DATA step applying lihtc_reg, lihtc_credit, lihtc_inc_ceil and $lihtc_record_stat via PUT so the listing shows them resolving. Exercises PROC FORMAT numeric + character VALUE ranges, PROC CATALOG modify + CONTENTS, and PUT-based format application." +} diff --git a/jenner-check/t005_lihtc_formats/script.sas b/jenner-check/t005_lihtc_formats/script.sas new file mode 100644 index 0000000..a353c5c --- /dev/null +++ b/jenner-check/t005_lihtc_formats/script.sas @@ -0,0 +1,102 @@ +/************************************************************************** + Bundle: t005_lihtc_formats + Source: Prog/LIHTC/Make_formats_lihtc.sas (NeighborhoodInfoDC/HUD) + + The PROC FORMAT value definitions for the HUD LIHTC data sets are + reproduced verbatim from the repository. The only change from upstream is + the format library target: HUD becomes WORK so the run is self-contained. + These formats mix plain numeric maps with SAS special-missing keys (.n, + .u, .c) and one character format. After building the catalog we apply a + couple of the formats via PUT so the listing shows them resolving. +**************************************************************************/ + +proc format library=WORK; + + value lihtc_reg + 1 = "Northeast" + 2 = "Midwest" + 3 = "South" + 4 = "West"; + + value lihtc_inc_ceil + 1 = "50% AMI" + 2 = "60% AMI" + .n = "Not reported"; + + value lihtc_yr_pis + .u = 'Unknown' + .c = 'Not confirmed'; + + value lihtc_rentassist + 1 = "Federal" + 2 = "State" + 3 = "Both federal and state" + 4 = "Neither" + 5 = "Unknown whether federal or state"; + + value lihtc_type + 1 = "New construction" + 2 = "Acquisition and rehab" + 3 = "Both new construction and acquisition/rehab" + 4 = "Existing"; + + value lihtc_credit + 1 = "4 percent (30 percent present value)" + 2 = "9 percent (70 percent present value)" + 3 = "Both 4 percent and 9 percent" + 4 = "Tax credit exchange program (TCEP) only"; + + value lihtc_metro + 1 = "Metro/non-central city" + 2 = "Metro/central city" + 3 = "Non-metro"; + + value lihtc_dda + 0 = "Not in DDA" + 1 = "In metro DDA" + 2 = "In non-metro DDA" + 3 = "In metro GO zone DDA" + 4 = "In non-metro GO zone DDA"; + + value lihtc_nlm_reason + 1 = "Completed extended-use period" + 2 = "Sale under qualified contract" + 3 = "Other"; + + value $lihtc_record_stat + 'N' = "New" + 'U' = "Updated" + 'X' = "Existing (unchanged from previous DB version)"; + +run; + +proc catalog catalog=WORK.Formats; + modify lihtc_reg (desc="LIHTC census region") / entrytype=format; + modify lihtc_inc_ceil (desc="LIHTC income ceiling") / entrytype=format; + modify lihtc_yr_pis (desc="LIHTC year placed in service") / entrytype=format; + modify lihtc_rentassist (desc="LIHTC rental assistance contract") / entrytype=format; + modify lihtc_type (desc="LIHTC type of construction") / entrytype=format; + modify lihtc_credit (desc="LIHTC type of credit") / entrytype=format; + modify lihtc_metro (desc="LIHTC metro area") / entrytype=format; + modify lihtc_dda (desc="LIHTC DDA status") / entrytype=format; + modify lihtc_nlm_reason (desc="LIHTC reason prop. no longer monitored") / entrytype=format; + modify lihtc_record_stat (desc="LIHTC rec. status compared to prev. db") / entrytype=formatc; + contents; +quit; + +/** ---- Apply a few of the formats via PUT so the listing shows them resolving ---- **/ + +data demo; + input region credit inc_ceil rec_stat $; + region_desc = put(region, lihtc_reg.); + credit_desc = put(credit, lihtc_credit.); + ceil_desc = put(inc_ceil, lihtc_inc_ceil.); + rec_desc = put(rec_stat, $lihtc_record_stat.); + datalines; +1 2 1 N +3 3 2 U +4 1 1 X +; +run; + +proc print data=demo; var region region_desc credit credit_desc inc_ceil ceil_desc rec_stat rec_desc; run;