Workflow, Audit, and the Portable Report
Three concerns turn this from a CRUD app into a defensible public-sector service: background processing, an audit trail that survives scrutiny, and a report worth carrying between programs.
Queued background processing
Extraction and AI analysis take seconds to minutes β far too long for a request thread. The
pattern is queue and poll: the upload action writes the submission with status Extracting
and enqueues a job; a background worker picks it up, runs the pipeline, and advances the status;
the EPP's status page (or a small poll from jQuery) reflects progress.
The idiomatic Core shape is a hosted BackgroundService reading a queue table (or Azure
Storage Queue). Hangfire backed by SQL Server is a fine step up when you want persistent
jobs, retries, and a dashboard without new infrastructure. Either
way, obey two rules: jobs must be idempotent (a retry after a crash must not duplicate
findings β key them on submission + methodology version), and a poison message must move to a
dead-letter state that shows up on the ops dashboard, not silently retry forever.
The immutable audit trail
The Commission must be able to reconstruct any determination years later. That means an append-only audit log: every status transition, every AI suggestion, every analyst accept or override (with reason code), and the final determination β each with actor, UTC timestamp, and before/after values.
Implementation is deliberately boring: an AuditEvent table written inside the same
transaction as the change it records, so audit and data can never disagree. No UPDATE or
DELETE grants on the table. Corrections are new events ("determination amended, superseding
event 4417"), never edits. If your agency uses SQL Server temporal tables, they're a nice
belt-and-suspenders under the explicit log, but they don't replace it β temporal tables capture
what changed, while the audit log captures who intended what and why.
The portable report
The end product is a standardized PDF: candidate identity, institutions and coursework considered, per-SMR-domain outcomes, identified gaps with the remediation paths (coursework or exam), the analyst determination, and β critically β the methodology version applied. Generate it server-side from a template (a Razor-to-PDF pipeline or a report library your shop already licenses), store the generated file alongside its hash, and treat regeneration as a new versioned artifact. The report must stand alone: another EPP reading it should need no login and no phone call.
Versioning the methodology
SMR definitions and matching rules will change. If findings only point at "the current rules,"
every past determination becomes unexplainable the day the rules change. So the methodology β
SMR hierarchy, rule set, prompt/model configuration for the AI step β is a versioned,
immutable snapshot, and every finding, determination, and report references its
MethodologyVersionId. New submissions use the newest version; old decisions keep the version
that produced them. This one design decision is what lets a five-year-old report remain
defensible, and Module 11 leans on it hard.