{"id":51126768,"url":"https://github.com/kylewelsby/foundation-health-code-challenge","last_synced_at":"2026-06-25T08:32:17.592Z","repository":{"id":365944662,"uuid":"1274150887","full_name":"kylewelsby/foundation-health-code-challenge","owner":"kylewelsby","description":"Foundation Health code challenge repository","archived":false,"fork":false,"pushed_at":"2026-06-19T16:02:00.000Z","size":1687,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-19T16:21:32.235Z","etag":null,"topics":["code-challenge"],"latest_commit_sha":null,"homepage":"https://mp3-frame-analysis.mekyle.com/","language":"TypeScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kylewelsby.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-19T08:19:22.000Z","updated_at":"2026-06-19T16:02:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kylewelsby/foundation-health-code-challenge","commit_stats":null,"previous_names":["kylewelsby/foundation-health-code-challenge"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/kylewelsby/foundation-health-code-challenge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylewelsby%2Ffoundation-health-code-challenge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylewelsby%2Ffoundation-health-code-challenge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylewelsby%2Ffoundation-health-code-challenge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylewelsby%2Ffoundation-health-code-challenge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kylewelsby","download_url":"https://codeload.github.com/kylewelsby/foundation-health-code-challenge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kylewelsby%2Ffoundation-health-code-challenge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34661706,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-challenge"],"created_at":"2026-06-25T08:32:15.030Z","updated_at":"2026-06-25T08:32:17.568Z","avatar_url":"https://github.com/kylewelsby.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MP3 Frame Analysis\n\nAn HTTP API and Svelte web UI that accept an MP3 upload and return the number of\n**MPEG-1 Audio Layer III** frames in it, plus bonus metadata. The frame parser is\nhand-written (no MP3-parsing dependency), pure, and fully typed. The UI also previews\nthe uploaded file with a scrubbable audio player that shows the current playback frame.\n\n![MP3 Frame Analysis app screenshot](web/public/app-screenshot.png)\n\n```bash\ncurl -X POST -F \"file=@tests/fixtures/sample.mp3\" http://localhost:8787/file-upload\n# {\"frameCount\":6089, ...}\n```\n\n## Quick start\n\nPrerequisites: [mise](https://mise.jdx.dev) (pins the toolchain) — or just\n[Bun](https://bun.sh) ≥ 1.3 directly. **No Node required.**\n\n```bash\nmise install          # installs Bun (the only pinned tool)\nbun install           # installs wrangler + dev dependencies\n\nbun run build         # builds the Svelte UI (web/ -\u003e dist/)\nbun run dev           # serves the UI + API on workerd via `wrangler dev` (:8787)\n\nbun test              # parser + endpoint test suites\nbun run typecheck     # tsc --noEmit (strict; no `any`/`unknown`)\nbun run check:svelte  # svelte-check (UI types)\nbun run lint          # Biome (lint + format)\n```\n\nOpen **http://localhost:8787** for the upload UI, or hit the endpoint directly:\n\n```bash\ncurl -X POST -F \"file=@tests/fixtures/sample.mp3\" http://localhost:8787/file-upload\n```\n\nInteractive API reference (Scalar) is at **`/docs`**; the raw spec at **`/openapi.json`**.\nFor UI development with hot reload, run `bun run dev:web` (Vite, proxies the API to `wrangler dev`).\n\n## Web UI\n\nThe home page is a drag-and-drop upload UI. It shows client-side size validation,\nan upload progress bar, the analyzed frame count, and — after a successful upload —\na player with a scrub bar that displays the current **playback frame** and elapsed\nduration while the audio is playing or scrubbing.\n\n## Routes\n\n| Route | Purpose |\n| ----- | ------- |\n| `GET /` | Svelte upload UI (served from `dist/` via Workers Assets) |\n| `POST /file-upload` | the API — count frames in an uploaded MP3 |\n| `GET /docs` | interactive API reference (Scalar) |\n| `GET /openapi.json` | OpenAPI 3.1 spec |\n| `GET /privacy` | privacy policy (no-persistence statement) |\n\n## API\n\n### `POST /file-upload`\n\nMultipart form upload; the MP3 must be sent as the **`file`** field.\n\n**`200 OK`** — JSON (a superset; `frameCount` is always present, top-level, numeric):\n\n```jsonc\n{\n  \"frameCount\": 6089,            // audio frames — the required value\n  \"framesIncludingHeader\": 6090, // structural total (incl. the Xing/Info/VBRI header frame)\n  \"durationSeconds\": 159.0596,   // frameCount * 1152 / sampleRate (display duration)\n  \"sampleRate\": 44100,\n  \"channelMode\": \"joint_stereo\",\n  \"bitrate\": { \"mode\": \"vbr\", \"averageKbps\": 73 },\n  \"header\": { \"kind\": \"xing\", \"declaredFrameCount\": 6089 },\n  \"flags\": { \"truncated\": false, \"corrupt\": false }\n}\n```\n\n**Errors** — JSON `{ \"error\": { \"code\", \"message\" } }` with a meaningful status:\n\n| Status | When |\n| ------ | ---- |\n| `400` | no `file` field · empty file · body isn't multipart |\n| `413` | upload exceeds the 100 MB limit |\n| `415` | not MPEG-1 Layer III (wrong MPEG version/layer, free-format, or not an MP3) |\n| `405` | method isn't `POST` |\n| `404` | path isn't `/file-upload` |\n\nA **corrupt** stream (valid frames then unrecoverable garbage) and a **truncated**\nfinal frame still return `200` with the count so far and `flags.corrupt` /\n`flags.truncated` set — they are not errors.\n\n## How the count works\n\nThe parser finds the 4-byte frame header (11-bit sync + MPEG-1 + Layer III), reads\nthe bitrate/sample-rate/padding, computes `frameLength = 144·bitrate/sampleRate +\npadding`, and **hops** to the next header — it never reads the payload, so counting\nis `O(frames)`, not `O(bytes)`. It skips ID3v2 tags (syncsafe size), ignores the\nID3v1 trailer, classifies CBR vs VBR, and resyncs (bounded, with 2-frame\nconfirmation) across corruption.\n\n**`frameCount` excludes the Xing/Info/VBRI header frame.** On the provided\n`sample.mp3` (VBR) that's **6089** — matching `mediainfo` (the assessment's suggested\nverification tool), `ffprobe`, and the file's own Xing declared count. The structural\ntotal (6090) is exposed as `framesIncludingHeader`. Full reasoning, including the\ntool disagreement on CBR files, is in\n[ADR 0001](docs/adr/0001-framecount-excludes-header-frame.md).\n\n## Design decisions \u0026 tradeoffs\n\n- **Hand-written parser, pure library.** [`src/lib/mp3/analyze.ts`](src/lib/mp3/analyze.ts)\n  has no Worker/DOM/Node dependencies — unit-testable in isolation and embeddable by\n  any consumer. The Worker is a thin HTTP adapter over it.\n- **Plain Cloudflare Worker (not SvelteKit).** A single API endpoint needs no\n  framework; Bun + `wrangler dev` keeps the toolchain minimal.\n- **Cloudflare Workers Free.** Near-zero cost, no cold starts, single-command deploy.\n  Constraints (128 MB memory, 10 ms CPU, 100 MB body) and the scale-out ladder are in\n  [ADR 0002](docs/adr/0002-workers-free-stream-hop-and-scale-ladder.md).\n- **100 MB upload cap → `413`.** The cap matches the Cloudflare Workers Free request-body\n  limit. A 5 s in-isolate parsing budget provides a graceful 503 backstop for pathological\n  streams. See [ADR 0002](docs/adr/0002-workers-free-stream-hop-and-scale-ladder.md) and\n  [ADR 0003](docs/adr/0003-raise-upload-cap-and-add-processing-budget.md) for the ladder\n  and the cap-raise rationale.\n- **No persistence.** Uploaded audio is parsed in memory and discarded — storing it\n  would raise copyright/legal questions for no benefit to a stateless analyzer.\n- **Superset response.** Returning metadata alongside `frameCount` shows range while\n  keeping the required key exact; documented rather than hidden behind a flag.\n- **Out-of-scope formats** (other MPEG versions/layers, free-format) are rejected\n  gracefully (`415`), never miscounted.\n\n## Scalability\n\nCounting is `O(frames)` via header-hopping (a 100 MB file is low-single-digit ms of\nparsing CPU); a bounded, `indexOf`-based resync caps worst-case CPU on garbage. The\ncap is set by the platform's body limit and ingestion cost, not the algorithm. Beyond\nthe cap, the documented ladder is: embed the library (no upload) → chunked upload to\nR2 + async → Cloudflare Containers → plan bump. See ADR 0002.\n\n## Deployment\n\nCI deploys to Cloudflare Workers on every push to `main` (after the checks pass) — it\nbuilds the SPA and runs `wrangler deploy`. The production Worker is served from\n**https://mp3-frame-analysis.mekyle.com** (configured as a custom domain in\n`wrangler.toml`). Two repo secrets are required\n(**Settings → Secrets and variables → Actions**):\n\n- `CLOUDFLARE_API_TOKEN` — an API token with the **Edit Cloudflare Workers** template\n  (Workers Scripts: Edit). Create it at **dash.cloudflare.com → My Profile → API Tokens**.\n- `CLOUDFLARE_ACCOUNT_ID` — your account ID (only needed if the token spans multiple accounts).\n\nTo deploy manually instead: `wrangler login` then `bun run deploy` (builds + deploys).\n\nCI deploys via the official **`cloudflare/wrangler-action@v4`**, which auto-detects\nBun (from `bun.lock`) and uses the project's locked `wrangler` v4 — the same version\nas local dev and the build gate, so what's validated is what ships.\n\n## Testing\n\n`bun test` runs the parser suite and the endpoint suite against committed fixtures in\n`tests/fixtures/` (CBR/VBR/ID3v2+cover/ID3v1/truncated/corrupt/free-format/MPEG-2/\nLayer II, plus the real `sample.mp3`). Fixtures are `ffmpeg`/`lame`-generated from\nsilence or a tone (no copyright concern), and counts are cross-checked against\n`mediainfo` and `ffprobe`.\n\n## Project layout\n\n```\nsrc/lib/mp3/analyze.ts   # the pure, typed frame parser (the heart of the exercise)\nsrc/worker.ts            # the Cloudflare Worker: routes /file-upload, /docs, /openapi.json\nsrc/openapi.ts           # OpenAPI 3.1 spec\nweb/                     # Svelte 5 + Vite + Tailwind upload UI (builds to dist/)\ntests/                   # bun test suites + fixtures\nCONTEXT.md               # domain glossary\ndocs/adr/                # architecture decision records\nPRD.md                   # build spec \u0026 acceptance criteria\n```\n\n## AI assistance\n\nThis solution was developed with AI assistance (Claude). The design decisions,\ntradeoffs, and implementation are owned and understood by the author — the ADRs and\nthis README capture the reasoning so each choice can be defended unaided.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkylewelsby%2Ffoundation-health-code-challenge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkylewelsby%2Ffoundation-health-code-challenge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkylewelsby%2Ffoundation-health-code-challenge/lists"}