SF Enterprise Hackathon 2.0

BBS SG Bank:
from flat-file COBOL
to an indexed batch

A truthful, evidence-linked legacy modernization — the planning driven through the Opsera Forge pipeline, the engine written and tested here.

1 Assessment2 Intent3 PRD-Spec 4 Architecture5 User Stories6 Testing 7 Delivery — pending
BBS
BBS SG Bank modernization
23 September 2026 · hosted: https://bbs-sg-bank-demo.pages.dev
127.0.0.1 — demo app, overview
Screenshot of the local BBS SG Bank demo app overview: the legacy COBOL nightly batch panel beside the modern ledger engine panel, with synthetic demo data.
The demo app we built runs the unmodified modern/bank.py engine over synthetic data. Fictional bank; no real accounts.
BBS SG Bank1 / 12 · synthetic fixtures · fictional institution

The problem

The overnight batch gets slower as the bank grows

For every single operation, the legacy program:

  • re-opens accounts.dat and scans every account, then
  • rewrites every account row to apply one change.
Cost = operations × accounts. Double the volume and the work roughly quadruples.
read operations.dat
for each operation:          # O(M)
    scan ALL accounts         # O(A)
    rewrite ALL accounts      # O(A)
display PROCESSED / REJECTED / TOTAL_CENTS
legacy O(M·A) modern O(M+A) small bank large bank work / file I/O

Shapes are drawn from the code: the legacy inner loops are proportional to the account count, the modern engine is not.

Why it hurts: the overnight window is fixed, the transaction volume is not. At the default fixture size the legacy batch performs on the order of 1.4 million row visits where about 1,600 would do (docs/02-before-after.md §3).
BBS SG Bank2 / 12

Before — legacy COBOL

Two lines in the real source explain everything

legacy/bank.cob
L108  *> Legacy bottleneck: reopen and scan the flat file for every operation.
L133  *> Legacy bottleneck: rewrite every account for every accepted operation.

Cost model

O(M·A)

Passes / accepted op

full file

Runtime deps

GnuCOBOL
PER ACCEPTED OPERATION — THE WHOLE FILE, THREE TIMES 1 find / validate 2 rewrite → temp 3 temp → accounts full accounts.dat
127.0.0.1:8792 — operator terminal
Screenshot of the operator terminal running a real compiled legacy/bank.cob job: PROCESSED 400, REJECTED 3, STEP010 0.873s wall clock, parity check PASS.
The legacy program still runs — our operator terminal compiles and executes the unmodified bank.cob for real (GnuCOBOL 3.2.0). Preset M shown: 1,200 accounts × 400 ops, 400 processed / 3 rejected, 0.873 s real COBOL wall clock, parity PASS. Synthetic fixture; the screen labels its replay rows as simulated.
BBS SG Bank3 / 12 · real COBOL, synthetic data

After — modern engine

Index once, apply in O(1), write once

# modern/bank.py
accounts = {}                      # load once  O(A)
for row in accounts_file:
    account, cents = row.split("|")
    accounts[account] = int(cents)

for row in operations_file:       # O(M), O(1)/op
    ... validate via dict lookups ...
    accounts[source] += amount

write accounts once                # O(A)

Same validation rules, same on-disk format, same stdout contract — the parity harness enforces it.

Cost model

O(A+M)

Writes / run

1
no temp file

Runtime deps

python3
stdlib only
PER RUN — EACH FILE TOUCHED ONCE read accounts read operations write accounts in-memory dict · O(1) lookups · state written once
Takeaway: three file touches per run — one read of accounts, one read of operations, one write — regardless of how many operations arrive. The legacy program spent three full traversals of the ledger on every accepted operation.
BBS SG Bank4 / 12

Before / after

What actually changed

DimensionBefore — legacyAfter — modern
ParadigmFlat-file, re-scan per operationIn-memory index, one pass
CostO(operations × accounts)O(accounts + operations)
File writesWhole file per accepted op (via temp file)Once per run
Source172 lines COBOL54 lines Python
ToolchainGnuCOBOLpython3 (stdlib)
Change safety netNone in repoParity harness + 5 unit tests
Output contractUnchanged — byte-identical accounts.dat and stdout on identical input
Unchanged output is the headline. The migration removes work; it does not change a single balance, count or byte.

Safety divergences, stated plainly: the modern engine fails closed on duplicate account ids and on a balance that would exceed PIC 9(12), where the legacy program keeps duplicates and truncates. This is deliberate and characterised by tests.

BBS SG Bank5 / 12

Correctness

We prove it — we don't promise it

# scripts/benchmark.py — same fixtures, both engines
legacy = run("cobc" compiled binary)
modern = run("python3 modern/bank.py")
assert legacy.counts == modern.counts
assert legacy.accounts_bytes == modern.accounts_bytes

A divergence raises. Correctness is a test, not a claim.

AT-1…AT-13 pass deposits, withdrawals, transfers, every rejection case, format, totals, malformed input, parity.

core suites green 33 in scripts/test_bank.py + 5 in tests/test_modern.py

108 local tests passing site 50 · operator terminal 20 · core 38

parity byte-identical legacy vs modern on identical fixtures.

127.0.0.1 — batch console
Screenshot of the demo app batch console after a live run: 63 rows submitted, 60 posted, 3 rejected, engine time 0.33 ms, with value conservation and re-simulation checks marked green.
Live run in the app: 63 rows submitted → 60 posted / 3 rejected, engine time 0.33 ms. The console re-derives the expectation independently and checks the ledger total and accounts.dat before claiming a pass. Synthetic data.
AT-12 is the parity gate (identical counts, byte-identical accounts.dat); AT-13 is the scaling gate — the speedup must grow with the account count, and it does: 4.3× → 8.0× → 13.1× → 24.6× across the sweep.
BBS SG Bank6 / 12 · synthetic fixtures

Performance — measured

Real numbers, with the caveat stated in the same breath

MEDIAN SPEEDUP, BYTE-IDENTICAL PARITY, ISOLATED GnuCOBOL 3.2.0 1200 × 400 28.6× · 1.004 s → 0.035 s 5000 × 500 128.1× · 4.728 s → 0.037 s SWEEP, 200 ops fixed — the ratio grows with the account count 300 → 4.3× 600 → 8.0× 1200 → 13.1× 2400 → 24.6×

Live COBOL job (preset M)

400 / 3
processed / rejected · 1.197 s real COBOL, 1.234 s overall · modern total matched

Complexity

O(M·A) → O(M+A)
writes: many per run → exactly one

When unmeasured

null
no cobcspeedup: null, never an estimate
127.0.0.1 — benchmark tab, live run
Screenshot of the demo app benchmark tab measuring both engines live: legacy 4.318 s versus modern 0.036 s on 5,000 accounts x 500 operations, with a parity check and a verdict stating the ratio grows with account count.
Measured on this host, live in the app: legacy 4.318 s vs modern 0.036 s on 5,000 × 500 (120×), parity checked first. Recorded medians in the repo are the 28.6× (1200×400) and 128.1× (5000×500) above.
Disclosure, stated with every number: the workload is synthetic and the legacy design is deliberately inefficient — it re-reads and rewrites the whole ledger per operation. The ratio is a property of that algorithm, not a general claim about COBOL, GnuCOBOL or banking workloads. Transcripts in scripts/evidence/; the demo app reports speedup: null when cobc is absent.
BBS SG Bank7 / 12 · synthetic workload · deliberately inefficient baseline

Built with Opsera Forge

Forge framed the modernization — the engine is our code

Assessmentcomplete · raw export absent Intentv1 approved · exported PRD-Specv1 approved · exported Architecturev1 approved · exported User Storiesv1 approved · exported Testingv1 exported · not approved Deliverypending
StageArtifactRepo homeStatus
AssessmentLegacy analysis · ForgeScoredocs/forge/assessment.mdcomplete · raw export absent
IntentGoal + constraintsdocs/forge/intent.mdv1 approved · exported
PRD-SpecFR / NFRdocs/forge/prd.mdv1 approved · exported
ArchitectureDesign + decisionsdocs/forge/architecture.mdv1 approved · exported
User StoriesWork ordersdocs/forge/work-orders.mdv1 approved · exported
TestingTest plandocs/forge/testing.mdv1 exported · not approved
DeliveryRelease + hosted appdocs/forge/delivery.mdpending

ForgeScore

57/100
Developing · 10 findings incl. 3 high: O(m×n) scans, full-file rewrites, missing specs

Exported to docs/forge/

5 v1 docs
Intent · PRD-Spec (~95%) · Architecture (~100%) · User Stories (6 epics / 33 proposed stories, ~75%) · Testing (132 proposed cases)
What we will not overstate. The raw Assessment report is not exportedassessment.md is a team transcription of the values read from the Forge UI. Testing v1 was generated and exported without an approval step (no Approve action shown). The exported documents sit in docs/forge/ with verified SHA-256 hashes. The 33 stories and 132 test cases are proposals — not implemented features and not executed tests; the local executable suites are 108 passing. The supplied MCP token returns 401, so exports came via signed-in browser downloads. Forge did not generate or run the modern engine, its document proposals (safe temp-file replacement, append-only audit, FastAPI, Forge Shipping) are not implemented, and nothing is deployed.
BBS SG Bank8 / 12 · Forge authored the planning documents, not the engine

Modernization impact

Why this matters to the bank

Scales

Linear batch: growing volume no longer degrades super-linearly.

Cheaper I/O

One write per run instead of one or more per transaction.

Safer change

A parity harness guards every future modification.

Portable

Drops the GnuCOBOL toolchain; runs on python3 stdlib.

Compatible

Files and stdout unchanged — no downstream breakage.

Documented

Every claim linked to code, a test, or a re-runnable command.

Observable

Every posting audited with a status and a reason in the demo app.

Frozen semantics

Same validation rules and rejection reasons, byte-for-byte output.

BEFORE · NIGHTLY BATCH batch window only · no queryable audit whole-file rewrite per accepted op change risk: none guarded in repo AFTER · INDEXED LEDGER post any time · per-row audit trail one write per run · O(A+M) parity harness + 108 local tests NOT CLAIMED public hosting · demo recording Forge Delivery · published posts raw Assessment export
Net effect for the bank: the batch stops being the constraint — the overnight window no longer grows with the customer base, balances are postable outside the batch, and every future change is guarded by the parity harness.
BBS SG Bank9 / 12

Honest status

What's done, what's open

Done — evidence in the repo

  • Modern batch implemented (modern/bank.py)
  • AT-1…AT-13 pass; core suites 33 + 5 green
  • Parity byte-identical; 28.6× / 128.1× measured; live COBOL job
  • Working local demo app (site/, 50 tests)
  • Operator terminal runs real COBOL (legacy-ui/, 20 tests)
  • Forge Assessment complete (57/100); Intent, PRD-Spec, Architecture, User Stories approved; Testing v1 exported (not approved)

Open — stated, not hidden

  • 🟡 Forge Delivery pending; raw Assessment report not exported
  • 🟡 Remaining Forge exports (MCP token 401; browser downloads work)
  • Public hosted interactive app (Worker)
  • 73-second public demo recording
  • Per-member public posts
  • Deck not yet rehearsed end-to-end
Showing the open list is intentional — it is the same state as the submission checklist, so no claim here outruns its evidence.
Current defensible claim: a modern, linear-time batch that passes AT-1…AT-13 with 33 + 5 core tests (plus 50 site and 20 terminal), byte-identical legacy/modern parity, and measured 28.6× / 128.1× on synthetic fixtures against a deliberately inefficient legacy design — wrapped in a hosted JavaScript-port demo and a local operator terminal that runs real COBOL; the raw Assessment export and Forge Delivery are still pending.
BBS SG Bank10 / 12

Team & links

BBS SG Bank modernization

Project links and evidence for the hackathon entry. Individual contributors and their public posts will be supplied when confirmed.

Repo

https://github.com/vasanthsreeram/bbs-sg-bank-modernization

Demo video

https://bbs-sg-bank-demo.pages.dev/media/bbs-sg-bank-demo.mp4 — 73-second recorded demo

Hosted

Open the interactive app · free Worker uses a JavaScript port; real COBOL terminal runs locally

Per-member public post links are required for submission and have not been supplied.

Reproduce it in four commands: python3 scripts/test_bank.py → 33 tests · python3 tests/test_modern.py → 5 tests · python3 scripts/benchmark.py --accounts 1200 --operations 400 → parity + timings (needs cobc) · python3 site/backend/app.py → the demo app on 127.0.0.1.
BBS SG Bank11 / 12

Thank you

Same result.
Linear cost.
Proof in the repo.

Questions? We'll answer with code, tests, and commands — never with a number we didn't measure.

parity: byte-identical 28.6× · 128.1× measured, disclosed 108 local tests passing synthetic data · fictional bank