Convert portfolio COBOL programs to a JS Lambda handler, proven by a golden-dataset parity harness - #269
Convert portfolio COBOL programs to a JS Lambda handler, proven by a golden-dataset parity harness#269hannahhkyme wants to merge 5 commits into
Conversation
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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
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>
Runtime verification — COBOL → JS portfolio parityRan 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. The "before": real COBOL executing
The harness is not vacuous (mutation check)Corrupting one committed COBOL baseline ( Why 8 of 50 cases are DERIVED, not executed
Verifying this turned up an inaccuracy in my own docs, fixed in 4498904: |
| 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'); | ||
| } |
There was a problem hiding this comment.
📝 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
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>
| 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) }; | ||
| } |
There was a problem hiding this comment.
📝 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
| function asDecimal(value) { | ||
| if (value === undefined || value === null || value === '') return new Decimal(0); | ||
| return Decimal.isDecimal(value) ? new Decimal(value) : new Decimal(value); | ||
| } |
There was a problem hiding this comment.
📝 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).
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
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.jsreports 50/50 PASS, 42 of them against executed COBOL; the harness is wired intojestso any divergence fails the build. Nothing undersrc/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.
PORTVALDdoesn't validate anythingThe brief describes ID =
PORT+ 4 numerics and account = 10 numerics. CompilingPORTVALDandCALLing 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:PORT0001fails.2000-VALIDATE-ACCOUNTtests all 50 bytes of aPIC X(50)forNUMERIC, so a 10-digit account plus 40 spaces fails.VAL-MIN/MAX-AMOUNTare the field's own representable range, soNOTANUMpasses. Only type validation works.So
validation.jshas two modes —legacy(proven against the executed baseline) andmodernized(the documented intent) — and the three differences are enumerated asDIV-VALD-I/A/Min the report. A divergence not on that list is a failure.PORTTRAN's position math is dead code2000-PROCESS-TRANSACTION. PERFORM 2100-VALIDATE-TRANSACTION IF WS-VALID-TRANSACTION ADD 1 TO WS-PROCESSED-COUNT END-IF.2200-UPDATE-POSITIONS— and2210-PROCESS-BUY..2240-PROCESS-FEE, plus the audit trail — is neverPERFORMed 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).PORTUPDThas noWHEN OTHERAn unrecognised action code falls through the
EVALUATE, reaches theREWRITE, and is counted as a successful update. Confirmed by the executed run: the deck feeds actionXand COBOL reportsupdates 4for 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 labelledDERIVEDin every report line, with the reasoning ingolden/expected/DERIVED.md:PORTTRAN— doesCOPY PORTREC, and noPORTRECcopybook exists in this repo. ItsPORT-TOTAL-UNITS/PORT-TOTAL-COSTaren't inPORTFLIO.cpyeither. The layout inschema/records.jsis reconstructed from field usage insidePORT-FILLER; it is not a copybook that was found.PORTMSTR—PROCEDURE DIVISION USINGon a program built as an executable, plusLS-*/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.shapplies those to a copy underbuild/stage/; each is listed inmodernized/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 (000000000000001Cis0.01atV99and0.0001atV9(4)), which is why scale comes from the schema and never from the bytes. All money and quantity values areDecimalend to end.Layout
Golden inputs are fixed literals written by
GOLDGEN.cbl, so there is noRANDOM-SEEDto pin — a stronger reproducibility guarantee thanTSTGEN00. The only nondeterministic value in the baseline is the create-pathACCEPT ... FROM DATEstamp, masked to@RUNDATEon both sides. Audit timestamps are excluded from comparison rather than faked.Reproducing
Devin-Org: engineering
Link to Devin session: https://app.devin.ai/sessions/789eae6dc9444ef7a07e5ed5ca94ecdf
Requested by: @hannahhkyme
Devin Review