Cadmus could only hold writing — everything else travelled by email, and we lost tenders over it. Two epics, both shipped, including camera capture inside a lockdown browser where file upload has to stay disabled.
Two people were losing time, at opposite ends of the same assessment.
No way to attach a file to a response. Work went out by email, or not at all.
Chased those files by hand, then manually related and evaluated them alongside the submission being marked.
A missing button is an inconvenience. A reconciliation workflow repeated for every student in a cohort is a recurring tax — and it capped Cadmus at writing-based assessment.
Live in commercial conversations, including an active tender requiring audio-visual and viva-style assessment. Not shipping risked pilot conversions, not just satisfaction.
Appetite set before solution. Two man-months, fixed. Shaping the solution to fit the time is what kept the out-of-scope list honest.
A lockdown browser stops a student reaching their machine, so a file picker can't be allowed — but the exam need is identical. That asymmetry is why the work split in two.
Attach existing files to a submission. Establishes the upload API, storage, virus scanning, and the file-state model everything else depends on.
Photo in-place from the device camera. Depends on the upload pipeline, so it could only follow — and it unlocks the lockdown-exam case.
Epic 1 could ship and start earning while Epic 2 was still being proven. Bundling them would have held everything behind the harder half.
Proved it with a proof-of-concept on native browser media APIs before pitching — the feature hinged on whether it was possible at all.
Uploading a file that works is easy. The spec effort went into everything around it — where I caught a gap in my own.
Per assessment in Classic, per question in MFA — the same capability exposed at the level each format is authored at.
1
2
3
Sandbox tenant with a test student and placeholder question content.
Nothing attached yet. Needs to make the affordance obvious without implying it's mandatory.
Wrong format, or over the size cap. Has to fail before the upload starts, with a reason the student can act on.
Uploaded, being virus-checked, not yet confirmed. The student needs to know their work is safe but not yet final.
Attached and confirmed, visible to the student and later to the marker alongside the submission.
Scanning had no design. The state a student is most likely to see and misread — complete, but not confirmed. Catching it in review rather than production changed how I write state coverage.
Photo and video — what makes async vivas and OSCE-style questions possible, not just handwritten working. Comment anchoring to the file chip shipped and was well received.
No teacher-set limit in v1. A fixed ceiling meant far less data-handling dependency, so it shipped faster. When teachers surfaced the limit we raised it per tenant to 200MB.
A file failing scanning after a deadline was real but speculative. De-prioritised to ship faster, basic checker kept. No incident since.
Substantial set-flow changes. Cut explicitly and named phase 2 — so it read as a decision, not an oversight.
Not a rollout-safety measure. It existed so the capability could be packaged and billed as an add-on rather than absorbed into the base tier.
Two full QA passes, logged. Gaps that don't appear in a spec review — but would have appeared in the support queue.
Missing formats in the supported matrix — audio and older spreadsheet types students would obviously reach for.
Duplicate file handling was undefined — what should happen when a student attaches the same file twice hadn't been specified either way.
The virus-scan path wasn't verifiable — no way to test a rejection until a harmless standard test file was used to trigger one.
Selection state persisted when it shouldn't — a UI state carried over between contexts in a way that would have confused markers.
Teacher setup and student submission, both written by me. Documentation is part of shipping — writing it finds the parts of a flow you can't explain simply.
I specced from the happy path outward — the scanning state is the proof. I'd now enumerate states before flows: every condition, transient and failed, then the paths between them.
Shipping 50MB and moving it later. Waiting until several universities independently raised the limit made 200MB a two-line justification, not a debate.