Skip to content

Convert portfolio COBOL programs to a JS Lambda handler, proven by a golden-dataset parity harness - #269

Open
hannahhkyme wants to merge 5 commits into
mainfrom
devin/1787681112-portfolio-js-parity
Open

hannahhkyme wants to merge 5 commits into
mainfrom
devin/1787681112-portfolio-js-parity

Conversation

@hannahhkyme

@hannahhkyme hannahhkyme commented Aug 25, 2026

Copy link
Copy Markdown

Summary

Ports the seven src/programs/portfolio/ programs to one Lambda-style JS handler and proves behavioural parity against actual GnuCOBOL runs of the legacy programs. node golden/parity/run.js reports 50/50 PASS, 42 of them against executed COBOL; the harness is wired into jest so any divergence fails the build. Nothing under src/ is modified.

The interesting part of this PR isn't the port — it's that executing the legacy code contradicted the documented rules in three places, so the port reproduces measured behaviour rather than the spec.

PORTVALD doesn't validate anything

The brief describes ID = PORT + 4 numerics and account = 10 numerics. Compiling PORTVALD and CALLing it for 20 vectors (golden/cobol/VALDRV.cbl -> golden/expected/portvald.txt) shows every ID and every account is rejected, and every amount is accepted:

MOVE LS-INPUT-VALUE(5:4) TO VAL-NUMERIC-CHECK   *> VAL-NUMERIC-CHECK is PIC X(10)
IF VAL-NUMERIC-CHECK IS NOT NUMERIC             *> "0001      " -> never numeric

PORT0001 fails. 2000-VALIDATE-ACCOUNT tests all 50 bytes of a PIC X(50) for NUMERIC, so a 10-digit account plus 40 spaces fails. VAL-MIN/MAX-AMOUNT are the field's own representable range, so NOTANUM passes. Only type validation works.

So validation.js has two modes — legacy (proven against the executed baseline) and modernized (the documented intent) — and the three differences are enumerated as DIV-VALD-I/A/M in the report. A divergence not on that list is a failure.

PORTTRAN's position math is dead code

2000-PROCESS-TRANSACTION.
    PERFORM 2100-VALIDATE-TRANSACTION
    IF WS-VALID-TRANSACTION
        ADD 1 TO WS-PROCESSED-COUNT
    END-IF.

2200-UPDATE-POSITIONS — and 2210-PROCESS-BUY .. 2240-PROCESS-FEE, plus the audit trail — is never PERFORMed from anywhere. The reachable program validates, counts, and mutates nothing. mode: 'legacy' reproduces that; mode: 'modernized' implements the orphaned paragraphs as written (transfer stays a rejection, because "not implemented" is the behaviour).

PORTUPDT has no WHEN OTHER

An unrecognised action code falls through the EVALUATE, reaches the REWRITE, and is counted as a successful update. Confirmed by the executed run: the deck feeds action X and COBOL reports updates 4 for three real updates. Reproduced in legacy mode, rejected in modernized mode (DIV-UPDT-UNKNOWN-ACTION).

Executed vs derived

Five programs were compiled and run; the expectations in golden/expected/ are bytes that came out of a COBOL process. Two cannot be compiled and are labelled DERIVED in every report line, with the reasoning in golden/expected/DERIVED.md:

  • PORTTRAN — does COPY PORTREC, and no PORTREC copybook exists in this repo. Its PORT-TOTAL-UNITS/PORT-TOTAL-COST aren't in PORTFLIO.cpy either. The layout in schema/records.js is reconstructed from field usage inside PORT-FILLER; it is not a copybook that was found.
  • PORTMSTRPROCEDURE DIVISION USING on a program built as an executable, plus LS-*/ERR-* fields no copybook defines.

Three of the five that run needed a mechanical staging transform first (misaligned banner comment, duplicated copybook, unsupported ACCEPT ... FROM TIME STAMP). golden/cobol/stage.sh applies those to a copy under build/stage/; each is listed in modernized/CONTRACTS.md §6.

COMP-3 is proven byte-for-byte

12 packed-decimal vectors were emitted as raw bytes by a COBOL program and committed as golden/vectors/comp3-vectors.bin; the JS codec round-trips all 12. Two are byte-identical at different scales (000000000000001C is 0.01 at V99 and 0.0001 at V9(4)), which is why scale comes from the schema and never from the bytes. All money and quantity values are Decimal end to end.

Layout

modernized/src/schema/records.js   PORT-RECORD (148B), TRANSACTION-RECORD (152B), audit layouts
modernized/src/codec/comp3.js      decode/encode packed decimal, rejects bad sign/digit nibbles
modernized/src/codec/fixed.js      fixed-width record decode/encode from layout descriptors
modernized/src/validation.js       PORTVALD, legacy + modernized modes
modernized/src/store.js            KSDS -> keyed store: '22' dup, '23' notfound, '10' EOF, sorted READ NEXT
modernized/src/programs/*.js       one module per COBOL program, paragraph names preserved
modernized/src/handler.js          Lambda entry point
golden/cobol/                      GOLDGEN, GOLDDUMP, VALDRV, stage.sh, run-baseline.sh
golden/expected/                   the captured "before"
golden/parity/run.js               the diff -> golden/PARITY-REPORT.txt
docs/before-after/                 one artifact per program

Golden inputs are fixed literals written by GOLDGEN.cbl, so there is no RANDOM-SEED to pin — a stronger reproducibility guarantee than TSTGEN00. The only nondeterministic value in the baseline is the create-path ACCEPT ... FROM DATE stamp, masked to @RUNDATE on both sides. Audit timestamps are excluded from comparison rather than faked.

Reproducing

./golden/cobol/stage.sh && ./golden/cobol/run-baseline.sh   # capture the "before" (needs GnuCOBOL)
node golden/parity/run.js                                   # run the "after" and diff
npx jest                                                    # same, as a build gate

Devin-Org: engineering

Link to Devin session: https://app.devin.ai/sessions/789eae6dc9444ef7a07e5ed5ca94ecdf
Requested by: @hannahhkyme


Devin Review

Status Commit
⚪ Not started

Run Devin Review

💡 Connect your GitHub account to enable automatic code reviews.

Devin Review (Staging)
Open in Devin Review

devin-ai-integration Bot and others added 3 commits August 25, 2026 18:20
Co-Authored-By: hannah.kyme <hannahhkyme@gmail.com>
Co-Authored-By: hannah.kyme <hannahhkyme@gmail.com>
Captures the COBOL 'before' by compiling PORTVALD/PORTREAD/PORTADD/PORTUPDT/PORTDEL
with GnuCOBOL and running them against fixed golden inputs, then replays the same
decks through the JS handler and diffs. 42 of 50 cases are proven against a real
COBOL run; the 8 PORTTRAN/PORTMSTR cases are derived from source because those two
programs cannot be compiled, and are labelled DERIVED everywhere they appear.

Co-Authored-By: hannah.kyme <hannahhkyme@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

The stated cause (USING on an executable, undefined LS-*/ERR-* fields) is real but is
not what cobc reports: a misaligned banner comment fails at line 1 first and hides it.
Records both, and why no staging transform gets past the second set.

Co-Authored-By: hannah.kyme <hannahhkyme@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown

Runtime verification — COBOL → JS portfolio parity

Ran the whole before/after flow live on this branch with GnuCOBOL 3.1.2 and Node 20. No mocks: the legacy COBOL was compiled and executed, then the JS port was run against the same golden inputs.

Parity result: 50/50, 42 EXECUTED / 8 DERIVED

node golden/parity/run.js → exit 0

parity footer

parity table

The "before": real COBOL executing

./golden/cobol/stage.sh then ./golden/cobol/run-baseline.sh compile and run the legacy programs. Rerunning them leaves git status clean, so the committed baselines are byte-identical to a fresh COBOL run — they are captured output, not hand-written.

COBOL stdout

The "after": modernized JS suite

jest

The harness is not vacuous (mutation check)

Corrupting one committed COBOL baseline (Records added: 00000010000009) makes the harness fail loudly — ADD-COUNTS ... FAIL, 49 passed / 1 failed, exit 1. Restoring the file returns it to 50/50.

mutation

Why 8 of 50 cases are DERIVED, not executed

PORTTRAN and PORTMSTR cannot be compiled at all, so their 8 cases are reasoned from source and labelled DERIVED in the report's BASELINE column. find . -iname 'PORTREC*' returns nothing:

PORTTRAN compile failure

DERIVED rows

Verifying this turned up an inaccuracy in my own docs, fixed in 4498904: PORTMSTR does fail to compile, but a plain cobc -x reports PROGRAM-ID header missing / syntax error, unexpected * at line 1 — a misaligned banner comment — and never reaches the PROCEDURE DIVISION USING and undefined LS-*/ERR-* problems that DERIVED.md gave as the reason. Applying staging transform 1 confirms those problems are real (:83: executable program requested but PROCEDURE/ENTRY has USING clause, plus 22 undefined identifiers), and that no mechanical transform gets past them — the linkage section would have to be authored. So the DERIVED label stands; only the stated reason needed correcting.

Written by Devin

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Open in Devin Review

Comment thread docs/before-after/PORTREAD.md Outdated
Comment on lines +50 to +56
try {
quantity = new Decimal(input.quantity || 0);
price = new Decimal(input.price || 0);
amount = new Decimal(input.amount || 0);
} catch {
return transactionValidation('Amount must be greater than zero');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Misleading message on numeric parse failure in transaction validation

In validateTransaction, the catch around parsing quantity/price/amount returns 'Amount must be greater than zero' even when quantity is the offending field. All golden inputs decode to valid Decimals so this branch is never exercised, but a non-numeric input would surface a message that names the wrong field.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, and unreachable from the golden set — the decks only carry well-formed packed-decimal operands, so nothing in the parity matrix distinguishes the three fields here. Leaving it as-is for now and flagging it to Hannah rather than changing behaviour that the executed baseline doesn't cover; happy to split the message per field if she wants it.

Comment thread modernized/src/programs/porttran.js
Comment thread modernized/src/canonical.js
Comment thread modernized/src/codec/comp3.js
GOLDGEN must write the seed deck ascending (INDEXED OPEN OUTPUT), so insertion
order and sorted order coincided and the case passed even without a sort. Seed
the JS store reversed instead; verified it fails when store.js drops .sort().

Co-Authored-By: hannah.kyme <hannahhkyme@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 new potential issues.

Open in Devin Review

Comment thread golden/parity/cases.js
Comment thread modernized/src/store.js
Comment on lines +82 to +90
write(record) {
const canonical = canonicalize(record);
const key = keyOf(canonical);
if (Buffer.byteLength(key, 'ascii') !== 18) return { status: '35' };
if (this.records.has(key)) return { status: '22' };
this.records.set(key, clone(canonical));
this.persist();
return { status: '00', record: clone(canonical) };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Wrong-width key surfaces as HTTP 500

store.write returns FILE STATUS 35 when the composed 18-byte key is the wrong length, and fileResult maps any unknown status through FILE_STATUS_DEFAULT to ioError/500. A create with a malformed portId+accountNo therefore reports a server error, not a 400 rejection. The golden deck never hits this path, so parity is unaffected.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — store.js:85 returns '35' for a wrong-width key and FILE_STATUS_DEFAULT sends anything unmapped to 500. Reusing 35 (which in COBOL means "file not found on OPEN") for a malformed key was my choice and it's the weak part; a 400 would be the right answer. Unexercised by the golden deck, so I'm flagging it to Hannah rather than changing it here.

Comment on lines +27 to +30
function asDecimal(value) {
if (value === undefined || value === null || value === '') return new Decimal(0);
return Decimal.isDecimal(value) ? new Decimal(value) : new Decimal(value);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Redundant ternary in asDecimal

In asDecimal, Decimal.isDecimal(value) ? new Decimal(value) : new Decimal(value) is the same expression on both branches, so the isDecimal check does nothing. Harmless, but reads like a lost optimization (e.g. returning the value directly when it is already a Decimal).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, both branches are identical — leftover from making the copy unconditional so callers can't alias a stored Decimal. return new Decimal(value) alone says the same thing. Batching it with the other two nits for Hannah rather than pushing a one-liner mid-review.

Comment thread modernized/src/programs/porttran.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant