LIBAUDIT/i is a batch library-retention utility: it classifies candidate objects by age
against a retention policy, purges those past the threshold, keeps the rest, and persists a verifiable run
summary as real object state. It is driven the way IBM i shops actually run batch REXX — a CL program
(LADRIVER) fetches the policy from a data area and hands it to STRREXPRC, which
runs the REXX/400 exec LAMAIN; that exec does all its work through ADDRESS COMMAND
to CL and calls external routines LACLASS, LARPT and LASELFTEST.
Honest framing: this application is also a deliberate REXX/400 language-reference exercise. Each
moving part is chosen to drive a specific REXX feature end-to-end — the CL→REXX data-area bridge,
PARSE/PULL, string abuttal and FORMAT, CHANGESTR/COUNTSTR,
CONDITION under CALL ON vs SIGNAL ON, same-source-file and
*LIBL external-routine resolution, and the REXX error-28 LEAVE-name boundary. It is
documented here as a real retention utility, but it exists primarily to exercise those features. This manual
is grounded entirely in the committed source (rexx-app/src/ladriver.cl.txt,
lamain.rexx.txt, laclass.rexx.txt, larpt.rexx.txt,
laselftest.rexx.txt, and the test/libaudit.mjs driver).
LIBAUDIT/i performs one retention sweep per run:
LAPOLICY data area and passes it, with a run mode, to the REXX exec.LAMAIN creates five real candidate physical
files (LAOBJ0001–LAOBJ0005) in the scratch library
LAWORK, each with a known synthetic age.LACLASS compares an object's
age to the policy threshold and returns PURGE (strictly over the threshold) or
KEEP.PURGE classification triggers a real
DLTF of the object and accumulates reclaimed bytes; a KEEP leaves it.LARPT, an exponential capacity figure is emitted with FORMAT, and the run
summary (P=purged K=kept) is written to the LARESULT data area so the
result is verifiable independently of the REXX SAY transcript.With the tested policy of 30 days, the five candidates (ages 5, 40, 12, 91, 30) resolve to
purged=2, kept=3: only the ages strictly greater than 30 (40 and 91) are purged; age 30 is the
boundary and is kept (the comparison is >, not ≥).
The application is split across two layers with a hard, purposeful boundary:
RTVDTAARA ... RTNVAR() can bind — that keyword never works from a bare REXX command
clause. So LADRIVER retrieves LIBAUD/LAPOLICY into &POLAGE,
sets the run mode, ensures the scratch library LAWORK exists, builds a PARM token, and
invokes STRREXPRC SRCMBR(LAMAIN) SRCFILE(LIBAUD/QREXSRC) PARM(&PARMSTR). After the
exec returns, it re-verifies the committed LARESULT data area with CHKOBJ.PARSE ARG the
policy and mode, drive every piece of object state through ADDRESS COMMAND to CL
(CRTPF, DLTF, CHKOBJ, CRTDTAARA,
CHGDTAARA), call the classification and reporting external routines, and trap host-command
and syntax conditions with CALL ON / SIGNAL ON handlers.The benefit is that policy retrieval lives where the OS actually permits it (CL), while the classification,
reporting, and control logic stay in REXX, and the result is provable from committed objects rather than
transcript scraping. Everything runs in library LIBAUD (with report routine
LARPT in LARPTLIB and candidates in LAWORK).
CL DRIVER BRIDGE REXX EXECS
--------- ------ ----------
LADRIVER ----------> STRREXPRC -------> LAMAIN (the sweep)
RTVDTAARA LAPOLICY PARM='polage mode' PARSE ARG polage runmode
CRTLIB LAWORK (if none) PULL ovr (empty in batch)
PARM = &POLAGE &RUNMODE CALL ON ERROR name handler
| |
| +--CALL--> LACLASS (same srcfile)
| | age vs policy -> PURGE/KEEP
| +--CALL--> LARPT (LARPTLIB/QREXSRC, *LIBL)
| | CHANGESTR/COUNTSTR redact+total
| +--CALL--> LASELFTEST (LEAVE-name error 28)
v | trapped via SIGNAL ON SYNTAX
CHKOBJ LARESULT <---- CHGDTAARA LARESULT <------+ ADDRESS COMMAND: CRTPF/DLTF/CHKOBJ/CRTDTAARA
A single run flows: LADRIVER reads LAPOLICY → calls
STRREXPRC with the policy+mode PARM → LAMAIN preflights, creates the five
candidates, classifies each via LACLASS, DLTFs the over-threshold ones, formats
the audit line via LARPT, persists LARESULT, runs the LASELFTEST
boundary check → LADRIVER re-verifies LARESULT exists.
| Object | Type | Role |
|---|---|---|
| LADRIVER | CL program (QCLSRC) | Batch entry point; policy retrieval + STRREXPRC + state re-check. |
| LAMAIN | REXX (QREXSRC) | The sweep exec: classify / purge / report / persist. |
| LACLASS | REXX (same QREXSRC) | External classification routine (age vs policy). |
| LARPT | REXX (LARPTLIB/QREXSRC) | External report routine resolved via *LIBL. |
| LASELFTEST | REXX (same QREXSRC) | Deliberate LEAVE-name error-28 self-test. |
| LAPOLICY | *DTAARA (LIBAUD) | Retention age in days (input; read by CL). |
| LARESULT | *DTAARA (LIBAUD) | Persisted run summary "P=n K=n" (output). |
| LAWORK | *LIB | Scratch/candidate library (created on demand). |
| LAOBJ0001-0005 | *FILE (LAWORK) | Five synthetic candidate objects. |
| QREXSRC | Source PF | REXX source (LIBAUD and LARPTLIB copies). |
| QCLSRC | Source PF | CL source (LADRIVER). |
The full catalogue is 1 CL program + 4 REXX execs, 2 data areas, 1 scratch library holding up to 5 candidate files, over 2 source PFs. Sections D and F expand each.
Honest statement: LIBAUDIT/i has no interactive 5250 screen, no DDS display file, no subfile,
and no menu. It is a batch administrative exec. The only "user interface" is the job's SAY
transcript in the joblog and the committed object state it leaves behind. There is nothing to key an option
against; an operator runs the CL driver and then inspects results. This is normal for a REXX/400 retention
utility — it is driven, not operated interactively.
Because there is no screen, everything an operator does is on the command-entry line (or via a JOBQ/scheduler): set the policy, submit the driver, then read the transcript and verify committed state.
On IBM i, the utility is reached by calling its CL driver by name. Before invoking it, the job's library
list must include LIBAUD and LARPTLIB — the tested job runs with
LIBL = QSYS LIBAUD LARPTLIB QGPL QTEMP and CURLIB = LIBAUD. LARPTLIB
must be on the list so STRREXPRC can resolve the external routine LARPT along
*LIBL.
| To do this | Type on the command line |
|---|---|
| Set the retention policy (days) | CHGDTAARA DTAARA(LIBAUD/LAPOLICY) VALUE('30') |
| Run the retention sweep (the driver) | CALL LIBAUD/LADRIVER (or SBMJOB it) |
| Run the REXX exec directly (advanced) | STRREXPRC SRCMBR(LAMAIN) SRCFILE(LIBAUD/QREXSRC) PARM('0000030 LIVE') |
| Inspect the persisted run summary | DSPDTAARA DTAARA(LIBAUD/LARESULT) |
The driver takes no CALL parameters — it reads the policy from LAPOLICY itself,
so a scheduled submission is a bare CALL LIBAUD/LADRIVER. Running LAMAIN directly
is possible but bypasses the CL data-area bridge: you must supply the policy age and mode as a PARM token
yourself (e.g. '0000030 LIVE'), because RTVDTAARA ... RTNVAR() only binds inside a
structured CL program (see A.2).
LAMAIN issues a PULL for an operator override card immediately after startup.
In batch there are no stacked input lines, so the external PULL resolves to an empty string,
and the exec treats that honestly as "no override supplied" — not as an error:
pull ovr
if ovr = '' then
say 'no operator override card supplied (expected in batch)'
else
say 'operator override:' ovr
This is the closest thing to interactive input the utility has: if the job were run with a stacked queue line, that value would be surfaced; in normal batch it is empty and the sweep proceeds with the policy from the data area. There is no maker–checker, approval gate, or four-eyes workflow — the sweep applies its purges immediately.
LIBAUDIT/i runs as a single batch job: LADRIVER, which invokes the LAMAIN exec
through STRREXPRC. There is one logical run — the retention sweep — with three visible
phases: candidate classification/purge, the capacity report, and the error-handling/self-test. The policy is
read from the LAPOLICY data area, so a scheduled submission needs no parameters.
-- set the retention threshold the next sweep runs against (days) CHGDTAARA DTAARA(LIBAUD/LAPOLICY) VALUE('30') -- then submit the (parameterless) driver SBMJOB CMD(CALL PGM(LIBAUD/LADRIVER)) JOB(LIBAUDIT)
| Program | Purpose | Routines called | Inputs | Outputs | Frequency |
|---|---|---|---|---|---|
| LADRIVER | Batch entry point: fetch policy, ensure LAWORK, run the exec, re-verify result. | STRREXPRC(LAMAIN); CL RTVDTAARA/CHKOBJ/CRTLIB. |
LIBAUD/LAPOLICY data area; run mode 'LIVE'. |
Committed LAWORK library + LARESULT data area (CHKOBJ'd on return). |
On demand / scheduled. |
| LAMAIN | Classify five candidates, purge over-threshold, report, persist summary, self-test. | LACLASS, LARPT, LASELFTEST; ADDRESS COMMAND CRTPF/DLTF/CHKOBJ/CRTDTAARA/CHGDTAARA. |
PARM 'polage runmode' (e.g. '0000030 LIVE'). |
SAY transcript; LAOBJ* files (kept/deleted); LARESULT = "P=n K=n". |
Per driver run. |
The sweep proper (LAMAIN, lines 38–83): it seeds five in-exec candidates with known
ages, and for each one CRTPFs a real candidate file in LAWORK, calls
class = laclass(oage, polage), and on PURGE issues a real
DLTF FILE(LAWORK/onm) guarded by an rc = 0 check, accumulating
bytesreclaimed = bytesreclaimed + (oage * 5120) and appending onm:PURGED; to the
raw report; otherwise it counts the object as kept and appends onm:KEPT;.
Expected SAY (policy=30, five candidates): LIBAUDIT/i sweep starting, policy=30d mode=LIVE no operator override card supplied (expected in batch) candidates evaluated=5 purged=2 kept=3 ages 40 & 91 over 30
Classification (LACLASS) is a pure function: PURGE when
oage > pol, else KEEP. The boundary is strict — age exactly equal to the
policy is kept (candidate LAOBJ0005, age 30, policy 30, is kept).
After the loop, the reclaimed-byte total is emitted through REXX FORMAT in exponential form:
format(bytesreclaimed, , , 3, 0). With bytesreclaimed = 670720 the engine's TRL-2
FORMAT produces scientific notation with a 3-digit exponent field:
reclaimed bytes (exponential): 6.70720E+005
The redacted audit line is then built by the external routine LARPT (see F.3): it runs
CHANGESTR('LAOBJ', raw, 'OBJ#') to redact every object-name prefix and
COUNTSTR('PURGED;', ...) / COUNTSTR('KEPT;', ...) to total the outcomes, returning
one line:
AUDIT REPORT [OBJ#0001:KEPT;OBJ#0002:PURGED;OBJ#0003:KEPT;OBJ#0004:PURGED;OBJ#0005:KEPT;] totals purged=2 kept=3
Finally the run summary is persisted through the CL bridge: CHKOBJ the LARESULT
data area, CRTDTAARA it if absent, then CHGDTAARA it to
'P=' || purged || ' K=' || kept — i.e. P=2 K=3.
Two distinct REXX conditions are exercised and trapped, and both are expected outcomes, not failures:
LAMAIN issues
'CHKOBJ OBJ(LAWORK/LASTALEPRV) OBJTYPE(*FILE)' against an object that never exists on a
fresh run. The failing host command (RC=30) fires the CALL ON ERROR handler by name, which
runs and returns, so execution resumes at the next clause. CONDITION('C')='ERROR'
and CONDITION('I')='CALL' here.call laselftest invokes an
external routine whose body does leave ghostloop with no enclosing loop named
GHOSTLOOP — a REXX error 28. A failing external routine surfaces to the caller as
error 40 (a SYNTAX-class condition), which the SIGNAL ON SYNTAX trap catches. Here
CONDITION('C')='SYNTAX' and CONDITION('I')='SIGNAL', distinguishing it from
the CALL-driven recovery above.Expected SAY (the two trapped conditions):
preflight probe rc=30 handlerfired=1
handler: cond=ERROR inst=CALL rc=30
selftest trapped as expected: cond=SYNTAX inst=SIGNAL rc=40
CALL precisely so the REXX error 28
it triggers does not abort the whole sweep. This is the utility deliberately proving it can rely on the
engine's LEAVE-name contract (loud error 28 → error 40 to the caller) without crashing.LIBAUDIT/i has no relational schema — it touches IBM i objects (data areas, a library,
physical files) rather than DB2 tables. The objects it reads, creates, and commits, grounded in the source
and the test/libaudit.mjs harness:
| Attribute | Value | Meaning |
|---|---|---|
| Object | LIBAUD/LAPOLICY | *DTAARA, TYPE(*CHAR) LEN(6). |
| Value | e.g. 30 | Retention threshold in days; an object strictly older is purged. |
| Read by | LADRIVER | Via RTVDTAARA ... RTNVAR(&POLAGE) (CL-only bridge). |
The zoned 6-char token is de-padded in the exec with polage = polage + 0.
| Attribute | Value | Meaning |
|---|---|---|
| Object | LIBAUD/LARESULT | *DTAARA, TYPE(*CHAR) LEN(50). |
| Value | P=2 K=3 | Purged / kept counts of the completed sweep. |
| Written by | LAMAIN | CHKOBJ → CRTDTAARA (if absent) → CHGDTAARA. |
| Re-checked by | LADRIVER | CHKOBJ OBJ(LIBAUD/LARESULT) OBJTYPE(*DTAARA) on return. |
This is the utility's authoritative result: it proves the sweep's outcome independently of the
SAY transcript.
| Attribute | Value | Meaning |
|---|---|---|
| Object | LAWORK | *LIB, created on demand by LADRIVER (CHKOBJ + MONMSG SFF9801 → CRTLIB). |
| Holds | LAOBJ0001–LAOBJ0005 | The candidate physical files. |
| Candidate | Age (days) | Class @ policy 30 | Outcome |
|---|---|---|---|
| LAOBJ0001 | 5 | KEEP | File remains in LAWORK. |
| LAOBJ0002 | 40 | PURGE | DLTF'd; +204800 bytes reclaimed. |
| LAOBJ0003 | 12 | KEEP | File remains in LAWORK. |
| LAOBJ0004 | 91 | PURGE | DLTF'd; +465920 bytes reclaimed. |
| LAOBJ0005 | 30 | KEEP | Boundary (age = policy, not strictly over) — kept. |
Each candidate is a real CRTPF FILE(LAWORK/LAOBJnnnn) RCDLEN(80), so a
DLTF is a genuine state change. Reclaimed bytes = age × 5120; total
204800 + 465920 = 670720.
| Object | Type | Members |
|---|---|---|
| LIBAUD/QREXSRC | Source PF (RCDLEN 112) | LAMAIN, LACLASS, LASELFTEST. |
| LARPTLIB/QREXSRC | Source PF (RCDLEN 112) | LARPT (resolved via *LIBL). |
| LIBAUD/QCLSRC | Source PF (RCDLEN 112) | LADRIVER. |
LADRIVER reads LAPOLICY and drives LAMAIN via STRREXPRC.LAMAIN creates/deletes LAOBJ* in LAWORK and writes LARESULT.LAMAIN calls LACLASS and LASELFTEST from its own source file, and LARPT from LARPTLIB/QREXSRC along *LIBL.LIBAUD and LARPTLIB (the latter so
LARPT resolves via *LIBL), with CURLIB = LIBAUD.CHGDTAARA DTAARA(LIBAUD/LAPOLICY) VALUE('30').SBMJOB CMD(CALL PGM(LIBAUD/LADRIVER)) JOB(LIBAUDIT).SAY lines) for the run's phases.Pre-checks: confirm LAPOLICY holds the intended threshold; confirm
LARPTLIB is on the library list (otherwise LARPT will not resolve).
Post-checks — the healthy transcript:
LIBAUDIT/i sweep starting, policy=30d mode=LIVEno operator override card supplied (expected in batch)preflight probe rc=30 handlerfired=1 and handler: cond=ERROR inst=CALL rc=30candidates evaluated=5 purged=2 kept=3reclaimed bytes (exponential): 6.70720E+005AUDIT REPORT [OBJ#0001:KEPT;...] totals purged=2 kept=3run summary persisted to LIBAUD/LARESULTselftest trapped as expected: cond=SYNTAX inst=SIGNAL rc=40selftest: no trap fired (unexpected) must be absent — its
presence would mean the LEAVE-name boundary did not fire, which is a real defect. Likewise any
unexpected DLTF failure for ... line indicates a purge did not complete.Because the utility persists its outcome as real objects, verification does not depend on the transcript:
LAWORK exists (created by the driver).LAOBJ0001, LAOBJ0003, LAOBJ0005
(CHKOBJ OBJ(LAWORK/LAOBJ0001) OBJTYPE(*FILE), etc.).LAOBJ0002, LAOBJ0004 no longer exist.DSPDTAARA DTAARA(LIBAUD/LARESULT) must read exactly P=2 K=3.These are precisely the assertions the test/libaudit.mjs harness makes against an
independent hand-derived oracle (purged=2, kept=3, reclaimed 670720 bytes, LARESULT = "P=2 K=3").
| Situation | Behaviour | Action |
|---|---|---|
| Re-submit LADRIVER after a completed run | Kept objects (LAOBJ0001/3/5) still exist, so their CRTPF collides (object already exists) and trips the same ADDRESS COMMAND error handler mid-scan. | Safe: the handler resumes, the sweep completes, and LARESULT is overwritten with the same true counts (P=2 K=3). The purge set (0002/0004) is never resurrected. |
| LAWORK missing | Driver CHKOBJ fails (SFF9801), MONMSG catches it, CRTLIB creates it. | Automatic — no operator action. |
| LARESULT missing | Exec CHKOBJ non-zero, then CRTDTAARA creates it before CHGDTAARA. | Automatic — first run creates it. |
| LARPTLIB not on *LIBL | LARPT cannot be resolved; the sweep fails at the report step. | Add LARPTLIB to the library list and re-run. |
| Policy changed | The classification boundary moves; more or fewer candidates purge. | Update LAPOLICY and re-run; the summary reflects the new counts. |
LARESULT is stable
across re-runs at the same policy.The complete surface: one CL driver and four REXX execs, grounded in the rexx-app/src/*.txt
members. This section documents each routine and which REXX/400 features it exercises.
&POLAGE (CHAR 6), &RUNMODE (CHAR 10),
&PARMSTR (CHAR 30).RTNVAR() only binds inside a
structured CL program, never from a bare REXX command clause — so the policy is fetched here and
handed to REXX as a PARM token.'policyAge runMode' token with *BCAT (blank-cat) and starts the REXX procedure.SAY transcript — the
driver confirms the summary data area the exec must have created.PARSE ARG splits the PARM token; the + 0 de-pads the zoned 6-char policy.a || b || c) building the startup line.PULL for an operator card; empty in batch and treated as "no override".handler: label returns, resuming
after the failing clause. Reports CONDITION('C')/CONDITION('I')/rc.LACLASS, and on PURGE
run a real DLTF guarded by if rc = 0, accumulating reclaimed bytes; a
SELECT ... WHEN ... OTHERWISE routes PURGE vs KEEP.FORMAT with expp=3, expt=0 forces exponential output (6.70720E+005).*LIBL (not in this source file).q = "'" quote var to embed a literal
quote without doubling risk.selftesttrap:) reports cond=SYNTAX inst=SIGNAL rc=40 and exits cleanly.parse arg oage, pol; returns 'PURGE' when
oage > pol, else 'KEEP'. Exercises same-source-file external-routine
resolution and arithmetic. The strict > is what keeps the age-30 boundary candidate.parse arg raw; tagged = changestr('LAOBJ', raw, 'OBJ#') redacts every object
prefix; countstr('PURGED;', tagged) and countstr('KEEP;'…)—actually
countstr('KEPT;', tagged)—total the outcomes; returns the one-line
AUDIT REPORT [...] totals purged=n kept=n. Exercises cross-library *LIBL
name resolution plus CHANGESTR/COUNTSTR.leave ghostloop with no enclosing DO named
GHOSTLOOP, raising REXX error 28. As a failing external routine it surfaces to the caller
as error 40 (SYNTAX class), proving the engine's LEAVE-name boundary is loud and the app can depend on
it. The return 'unreachable' is never reached.| Feature | Where | What it proves |
|---|---|---|
| CL→REXX data-area bridge | LADRIVER RTVDTAARA→PARM→LAMAIN | RTNVAR binds only in a CL program; policy handed to REXX as a token. |
| PARSE ARG / PULL / PARSE LINEIN | LAMAIN lines 12, 19 | PARM splitting; empty external PULL handled as "no override". |
| String abuttal (a||b||c) | LAMAIN say lines | Concatenation without intervening blanks. |
| FORMAT(n,,,expp,expt) | LAMAIN line 86 | Exponential/scientific formatting (6.70720E+005). |
| CHANGESTR / COUNTSTR | LARPT | Redaction and substring counting built-ins. |
| CALL ON ERROR + CONDITION | LAMAIN handler | Host-command RC trap; CONDITION('I')='CALL', resumes after clause. |
| SIGNAL ON SYNTAX + CONDITION | LAMAIN selftesttrap | Syntax-class trap; CONDITION('I')='SIGNAL', distinct from CALL recovery. |
| Same-srcfile external CALL | LACLASS, LASELFTEST | Routine resolution within the caller's own source member. |
| *LIBL external CALL | LARPT | STRREXPRC walks the library list to a different library's QREXSRC. |
| LEAVE-name error 28 | LASELFTEST | Unmatched LEAVE name surfaces loudly (error 28→40 to caller). |
| ADDRESS COMMAND to CL | LAMAIN (CRTPF/DLTF/CHKOBJ/CRTDTAARA/CHGDTAARA) | Real object state driven entirely from REXX host commands. |
RC special variable.CALL ON runs a handler that RETURNs and
resumes after the failing clause (CONDITION('I')='CALL'); SIGNAL ON transfers
control to a label and does not resume (CONDITION('I')='SIGNAL').CHANGESTR(needle, haystack, new) replaces every occurrence;
COUNTSTR(needle, haystack) counts them. Used by LARPT to redact and total.CRTPF file (LAOBJ0001…)
in LAWORK with a known synthetic age.'C' the condition name (ERROR/SYNTAX…),
'I' the instruction that raised it (CALL/SIGNAL).LAPOLICY supplies the policy (input);
LARESULT holds the run summary (output).CRTPFs candidates and
DLTFs the purged ones — genuine state changes.6.70720E+005).LEAVE loopname with no matching enclosing loop is a REXX error 28. LASELFTEST triggers it
on purpose to prove the boundary is enforced.LAPOLICY) above which an object is purged. An object exactly at the
threshold is kept (strict >).PURGE (age strictly over policy → delete) or
KEEP (retain). Decided by LACLASS.PARM token. LADRIVER uses it to launch LAMAIN.*LIBL to resolve LARPT in LARPTLIB/QREXSRC.