From 76261ec028cf9ada4d170930111c16cea3e16497 Mon Sep 17 00:00:00 2001 From: Christopher Bratkovics Date: Thu, 24 Sep 2026 19:49:01 -0400 Subject: [PATCH 1/9] refactor(case-study): extract EvidenceLinks, Section, and FindingBlock into shared components The EV case-study page keeps identical markup; the helpers now live in components/case-study/ so a second case-study route can reuse them. Co-Authored-By: Claude Fable 5.1 --- .../ev-charging-data-unified-schema/page.tsx | 21 ++++++------------- .../components/case-study/EvidenceLinks.tsx | 7 +++++++ .../components/case-study/FindingBlock.tsx | 3 +++ portfolio/components/case-study/Section.tsx | 3 +++ 4 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 portfolio/components/case-study/EvidenceLinks.tsx create mode 100644 portfolio/components/case-study/FindingBlock.tsx create mode 100644 portfolio/components/case-study/Section.tsx diff --git a/portfolio/app/projects/ev-charging-data-unified-schema/page.tsx b/portfolio/app/projects/ev-charging-data-unified-schema/page.tsx index ef03570..2c38f94 100644 --- a/portfolio/app/projects/ev-charging-data-unified-schema/page.tsx +++ b/portfolio/app/projects/ev-charging-data-unified-schema/page.tsx @@ -2,6 +2,9 @@ import type { Metadata } from "next"; import Link from "next/link"; import { ArrowLeft, ExternalLink, Github } from "lucide-react"; import { SITE } from "@/config/site"; +import EvidenceLinks, { type EvidenceLink } from "@/components/case-study/EvidenceLinks"; +import Section from "@/components/case-study/Section"; +import FindingBlock from "@/components/case-study/FindingBlock"; const title = "EV Charging Data: Unified Schema | Christopher J. Bratkovics"; const description = "A dbt and DuckDB case study integrating three incompatible public EV charging datasets into a tested schema with contracts, quarantine, reconciliation, intentional utilization metrics, and evidence-backed findings."; @@ -15,24 +18,12 @@ export const metadata: Metadata = { twitter: { card: "summary_large_image", title, description }, }; -const links = [ +const links: EvidenceLink[] = [ { label: "Repository", href: "https://github.com/cbratkovics/ev-charging-data-unified-schema", icon: Github }, { label: "dbt docs and lineage", href: "https://cbratkovics.github.io/ev-charging-data-unified-schema/", icon: ExternalLink }, { label: "Findings", href: "https://github.com/cbratkovics/ev-charging-data-unified-schema/blob/main/docs/FINDINGS.md", icon: ExternalLink }, ]; -function EvidenceLinks() { - return
{links.map(({ label, href, icon: Icon }) => )}
; -} - -function Section({ title: heading, children }: { title: string; children: React.ReactNode }) { - return

{heading}

{children}
; -} - -function FindingBlock({ title: heading, children, why, recommendation }: { title: string; children: React.ReactNode; why: React.ReactNode; recommendation: React.ReactNode }) { - return

{heading}

Found

{children}

Why it matters

{why}

What I’d tell the decision-maker

{recommendation}
; -} - export default function EvChargingCaseStudy() { return <> Skip to case study @@ -45,7 +36,7 @@ export default function EvChargingCaseStudy() {

EV Charging Data: Unified Schema

Three public datasets in three incompatible shapes, conformed into one tested dbt schema, with every number traceable to evidence.

As of v0.1.0

-
+
@@ -78,7 +69,7 @@ export default function EvChargingCaseStudy() {
  • Daylight-saving resolution: I assumed the database resolved ambiguous daylight-saving times to the first occurrence. An empirical test showed it uses the second.
  • A phantom port: rounding a session end by up to thirty seconds created a port that did not exist at a single-port station. I caught it because two reports disagreed.
  • A false reproducibility claim: after release, a reproducibility claim turned out to be false for two tables, because a non-deterministic pick was choosing station attributes. It now uses a majority rule with a tie-break, and a test that compares two independent builds byte for byte.
-

The project was AI-assisted, using Claude Code. It worked under a written brief with phase-gated review, which means I approved or amended every phase. The brief and sixteen decision records are in the repository.

It is an independent project on open data and contains no employer code, data, or business rules.

Stack: Python, SQL, dbt-core, DuckDB, pytest, GitHub Actions, GitHub Pages.

+

The project was AI-assisted, using Claude Code. It worked under a written brief with phase-gated review, which means I approved or amended every phase. The brief and sixteen decision records are in the repository.

It is an independent project on open data and contains no employer code, data, or business rules.

Stack: Python, SQL, dbt-core, DuckDB, pytest, GitHub Actions, GitHub Pages.

diff --git a/portfolio/components/case-study/EvidenceLinks.tsx b/portfolio/components/case-study/EvidenceLinks.tsx new file mode 100644 index 0000000..ee60aa3 --- /dev/null +++ b/portfolio/components/case-study/EvidenceLinks.tsx @@ -0,0 +1,7 @@ +import type { LucideIcon } from "lucide-react"; + +export interface EvidenceLink { label: string; href: string; icon: LucideIcon } + +export default function EvidenceLinks({ links }: { links: EvidenceLink[] }) { + return
{links.map(({ label, href, icon: Icon }) => )}
; +} diff --git a/portfolio/components/case-study/FindingBlock.tsx b/portfolio/components/case-study/FindingBlock.tsx new file mode 100644 index 0000000..de05a4f --- /dev/null +++ b/portfolio/components/case-study/FindingBlock.tsx @@ -0,0 +1,3 @@ +export default function FindingBlock({ title: heading, children, why, recommendation }: { title: string; children: React.ReactNode; why: React.ReactNode; recommendation: React.ReactNode }) { + return

{heading}

Found

{children}

Why it matters

{why}

What I’d tell the decision-maker

{recommendation}
; +} diff --git a/portfolio/components/case-study/Section.tsx b/portfolio/components/case-study/Section.tsx new file mode 100644 index 0000000..4273cc7 --- /dev/null +++ b/portfolio/components/case-study/Section.tsx @@ -0,0 +1,3 @@ +export default function Section({ title: heading, children }: { title: string; children: React.ReactNode }) { + return

{heading}

{children}
; +} From c680a5e3c0ed30d98a41e3e860589a23d47b0b69 Mon Sep 17 00:00:00 2001 From: Christopher Bratkovics Date: Thu, 24 Sep 2026 19:49:06 -0400 Subject: [PATCH 2/9] fix(case-study): restore og:image and twitter:image on case-study routes Page-level openGraph and twitter objects replace the root layout's, so the EV route rendered without a social image (Codex finding on PR #13). A shared caseStudyMetadata() helper now builds the route metadata with the site's social image in both objects. Co-Authored-By: Claude Fable 5.1 --- .../ev-charging-data-unified-schema/page.tsx | 10 ++-------- portfolio/components/case-study/metadata.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 portfolio/components/case-study/metadata.ts diff --git a/portfolio/app/projects/ev-charging-data-unified-schema/page.tsx b/portfolio/app/projects/ev-charging-data-unified-schema/page.tsx index 2c38f94..6f0bb76 100644 --- a/portfolio/app/projects/ev-charging-data-unified-schema/page.tsx +++ b/portfolio/app/projects/ev-charging-data-unified-schema/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { ArrowLeft, ExternalLink, Github } from "lucide-react"; -import { SITE } from "@/config/site"; +import { caseStudyMetadata } from "@/components/case-study/metadata"; import EvidenceLinks, { type EvidenceLink } from "@/components/case-study/EvidenceLinks"; import Section from "@/components/case-study/Section"; import FindingBlock from "@/components/case-study/FindingBlock"; @@ -10,13 +10,7 @@ const title = "EV Charging Data: Unified Schema | Christopher J. Bratkovics"; const description = "A dbt and DuckDB case study integrating three incompatible public EV charging datasets into a tested schema with contracts, quarantine, reconciliation, intentional utilization metrics, and evidence-backed findings."; const path = "/projects/ev-charging-data-unified-schema"; -export const metadata: Metadata = { - title, - description, - alternates: { canonical: path }, - openGraph: { title, description, type: "article", url: path, siteName: SITE.shortTitle }, - twitter: { card: "summary_large_image", title, description }, -}; +export const metadata: Metadata = caseStudyMetadata({ title, description, path }); const links: EvidenceLink[] = [ { label: "Repository", href: "https://github.com/cbratkovics/ev-charging-data-unified-schema", icon: Github }, diff --git a/portfolio/components/case-study/metadata.ts b/portfolio/components/case-study/metadata.ts new file mode 100644 index 0000000..437c55d --- /dev/null +++ b/portfolio/components/case-study/metadata.ts @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; +import { SITE } from "@/config/site"; + +// Page-level `openGraph` and `twitter` objects replace the root layout's objects rather than +// merging with them, so each case-study route must restate the site's social image here. +const socialImage = { url: "/opengraph-image", width: 1200, height: 630, alt: `${SITE.author.name} — ${SITE.author.jobTitle}` }; + +export function caseStudyMetadata({ title, description, path }: { title: string; description: string; path: string }): Metadata { + return { + title, + description, + alternates: { canonical: path }, + openGraph: { title, description, type: "article", url: path, siteName: SITE.shortTitle, images: [socialImage] }, + twitter: { card: "summary_large_image", title, description, images: [socialImage.url] }, + }; +} From b12a89241369703ac8aa07b05beb29205363f819 Mon Sep 17 00:00:00 2001 From: Christopher Bratkovics Date: Thu, 24 Sep 2026 19:51:30 -0400 Subject: [PATCH 3/9] feat(projects): re-tier the project grid to match the resume lineup EV Charging stays the flagship; Fantasy Football and NBA Stat Predictor are featured; SQL Genius, AI Chat, and Document Intelligence move to additional work. Data fields and order only; the content-contract tier assertions name the new contract. Co-Authored-By: Claude Fable 5.1 --- portfolio/data/projects.ts | 40 +++++++++++------------ portfolio/tests/content-contract.test.mjs | 9 +++-- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/portfolio/data/projects.ts b/portfolio/data/projects.ts index a4e987a..ca99b63 100644 --- a/portfolio/data/projects.ts +++ b/portfolio/data/projects.ts @@ -155,6 +155,25 @@ export const projects: Project[] = [ { label: "Pinned evaluation", url: footballEvaluation.sourceUrl } ], featured: true }, + { + id: "nba-ml", title: "NBA Stat Predictor", + summary: "A LightGBM batch pipeline with point-in-time features, GitHub Actions, Hugging Face artifacts, Next.js artifact-reading pages, season replay reconciliation, and a read-only tool-grounded brief.", + detail: "The holdout artifact reports 4.764 points MAE versus a 4.908 last-10 baseline for 22,244 eligible 2025–26 player-games (at least 10 minutes with baseline available). The distinct all-replay population does not beat its baseline, and post-game minutes eligibility is not pregame knowledge.", + inspect: "Inspect cohort-aware metrics, replay reconciliation, and the read-only tool-grounded brief. Published replay differences are +0.0021 points, +0.0008 rebounds, and +0.0010 assists against a 0.05 tolerance; the restricted replay and holdout cohorts have different eligibility rules.", + narrative: { + decisionContext: "Does a favorable restricted-cohort score justify the model for the full pregame population?", findingBasis: "Measured evaluation", + finding: "The restricted eligible cohort improves on its baseline, while the distinct all-replay population does not.", whyItMatters: "Post-game minutes eligibility is unavailable at the pregame decision point, so mixing populations can reverse the recommendation.", + recommendation: "Compare like-for-like populations using decision-time information, and prefer the supported baseline where the comparison does not justify the model.", recommendationStatus: "Evidence-based interpretation", + limitations: "This is a conclusion about the scoped evaluations, not every target or possible model." + }, + tech: ["Python", "LightGBM", "GitHub Actions", "Hugging Face", "Next.js"], + githubUrl: "https://github.com/cbratkovics/nba-ai-ml", liveUrl: "https://nba-ai-ml.vercel.app", liveLabel: "Project overview", + evidence: [ + { label: "Replay", url: "https://nba-ai-ml.vercel.app/replay" }, + { label: "Agent brief", url: "https://nba-ai-ml.vercel.app/brief" }, + { label: "Reconciliation notes", url: "https://github.com/cbratkovics/nba-ai-ml/blob/master/docs/reconciliation.md" } + ], featured: true + }, { id: "sql-genius", title: "SQL Genius AI | SQL Analytics Playground", summary: "An inspectable browser analytics workflow: explore a synthetic sample schema, draft or edit SQL, explicitly run an accepted read-only query in SQLite, preview bounded results, and export CSV.", @@ -168,7 +187,7 @@ export const projects: Project[] = [ }, tech: ["TypeScript", "Next.js", "Browser SQLite", "Local templates", "Read-only policy"], githubUrl: "https://github.com/cbratkovics/sql-genius-ai", liveUrl: "https://sql-genius-ai.vercel.app/demo", liveLabel: "Open playground", - evidence: [{ label: "Implementation evidence", url: "https://github.com/cbratkovics/sql-genius-ai/blob/main/docs/PORTFOLIO_EVIDENCE.md" }], featured: true + evidence: [{ label: "Implementation evidence", url: "https://github.com/cbratkovics/sql-genius-ai/blob/main/docs/PORTFOLIO_EVIDENCE.md" }], featured: false }, { id: "ai-chatbot", title: "AI Chat System | Multi-Provider LLM Gateway", @@ -186,25 +205,6 @@ export const projects: Project[] = [ evidence: [ { label: "System evaluations", url: "https://chatbot-ai-system.vercel.app/evals" }, { label: "Committed benchmark", url: "https://github.com/cbratkovics/chatbot-ai-system/blob/main/evals/results/latest.md" } - ], featured: true - }, - { - id: "nba-ml", title: "NBA Stat Predictor", - summary: "A LightGBM batch pipeline with point-in-time features, GitHub Actions, Hugging Face artifacts, Next.js artifact-reading pages, season replay reconciliation, and a read-only tool-grounded brief.", - detail: "The holdout artifact reports 4.764 points MAE versus a 4.908 last-10 baseline for 22,244 eligible 2025–26 player-games (at least 10 minutes with baseline available). The distinct all-replay population does not beat its baseline, and post-game minutes eligibility is not pregame knowledge.", - inspect: "Inspect cohort-aware metrics, replay reconciliation, and the read-only tool-grounded brief. Published replay differences are +0.0021 points, +0.0008 rebounds, and +0.0010 assists against a 0.05 tolerance; the restricted replay and holdout cohorts have different eligibility rules.", - narrative: { - decisionContext: "Does a favorable restricted-cohort score justify the model for the full pregame population?", findingBasis: "Measured evaluation", - finding: "The restricted eligible cohort improves on its baseline, while the distinct all-replay population does not.", whyItMatters: "Post-game minutes eligibility is unavailable at the pregame decision point, so mixing populations can reverse the recommendation.", - recommendation: "Compare like-for-like populations using decision-time information, and prefer the supported baseline where the comparison does not justify the model.", recommendationStatus: "Evidence-based interpretation", - limitations: "This is a conclusion about the scoped evaluations, not every target or possible model." - }, - tech: ["Python", "LightGBM", "GitHub Actions", "Hugging Face", "Next.js"], - githubUrl: "https://github.com/cbratkovics/nba-ai-ml", liveUrl: "https://nba-ai-ml.vercel.app", liveLabel: "Project overview", - evidence: [ - { label: "Replay", url: "https://nba-ai-ml.vercel.app/replay" }, - { label: "Agent brief", url: "https://nba-ai-ml.vercel.app/brief" }, - { label: "Reconciliation notes", url: "https://github.com/cbratkovics/nba-ai-ml/blob/master/docs/reconciliation.md" } ], featured: false }, { diff --git a/portfolio/tests/content-contract.test.mjs b/portfolio/tests/content-contract.test.mjs index a299714..8e2e32d 100644 --- a/portfolio/tests/content-contract.test.mjs +++ b/portfolio/tests/content-contract.test.mjs @@ -64,9 +64,12 @@ test("education is accurate, ordered, and separate from employment", () => { }); test("project structure, repositories, and capability boundaries remain scoped", () => { - assert.equal((content.match(/featured: true/g) ?? []).length, 4); - assert.equal((content.match(/featured: false/g) ?? []).length, 2); - ordered(content, ["EV Charging Data: Unified Schema", "Fantasy Football Data Platform & Decision Lab", "SQL Genius AI | SQL Analytics Playground", "AI Chat System | Multi-Provider LLM Gateway", "NBA Stat Predictor", "Document Intelligence | Hybrid Retrieval With Visible Evidence"]); + // Tier contract (matches the resume's Selected Independent Projects lineup): EV Charging is the + // flagship; Fantasy Football and NBA are featured; SQL Genius, AI Chat, and Document Intelligence + // are additional work. + assert.equal((content.match(/featured: true/g) ?? []).length, 3); + assert.equal((content.match(/featured: false/g) ?? []).length, 3); + ordered(content, ["EV Charging Data: Unified Schema", "Fantasy Football Data Platform & Decision Lab", "NBA Stat Predictor", "SQL Genius AI | SQL Analytics Playground", "AI Chat System | Multi-Provider LLM Gateway", "Document Intelligence | Hybrid Retrieval With Visible Evidence"]); for (const repo of ["ev-charging-data-unified-schema", "fantasy-football-ai", "sql-genius-ai", "chatbot-ai-system", "nba-ai-ml", "document-intelligence-ai"]) assert.match(content, new RegExp(`github\\.com/cbratkovics/${repo}`)); assert.match(content, /maintained demo defaults to local reviewed-intent\/template generation[\s\S]*legacy Python\/FastAPI Anthropic route remains optional/); assert.match(content, /in-memory cache with configured embeddings[\s\S]*not a production SLA[\s\S]*Redis-backed benchmark/); From bbacd4c87485c543745149d31c45850d66f63a15 Mon Sep 17 00:00:00 2001 From: Christopher Bratkovics Date: Thu, 24 Sep 2026 19:52:51 -0400 Subject: [PATCH 4/9] feat(case-study): add the Entity Resolution case-study route Mirrors the EV page: shared case-study helpers and metadata (with the site's social image), an "As of v0.1.0" scope line, evidence links at top and bottom, and every figure scoped to the v0.1.0 artifacts. Registered in the sitemap. Co-Authored-By: Claude Fable 5.1 --- .../app/projects/entity-resolution/page.tsx | 78 +++++++++++++++++++ portfolio/app/sitemap.ts | 1 + 2 files changed, 79 insertions(+) create mode 100644 portfolio/app/projects/entity-resolution/page.tsx diff --git a/portfolio/app/projects/entity-resolution/page.tsx b/portfolio/app/projects/entity-resolution/page.tsx new file mode 100644 index 0000000..cb1ebc2 --- /dev/null +++ b/portfolio/app/projects/entity-resolution/page.tsx @@ -0,0 +1,78 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { ArrowLeft, ExternalLink, Github } from "lucide-react"; +import { caseStudyMetadata } from "@/components/case-study/metadata"; +import EvidenceLinks, { type EvidenceLink } from "@/components/case-study/EvidenceLinks"; +import Section from "@/components/case-study/Section"; +import FindingBlock from "@/components/case-study/FindingBlock"; + +const title = "Entity Resolution: Rules vs. Calibrated Classifier | Christopher J. Bratkovics"; +const description = "A record-linkage case study matching MusicBrainz album release groups to Discogs masters against labelled ground truth: blocking completeness, a weighted rules baseline versus an isotonic-calibrated classifier, tiered decisions, review-queue cost, and coverage reported separately from accuracy."; +const path = "/projects/entity-resolution"; + +export const metadata: Metadata = caseStudyMetadata({ title, description, path }); + +const links: EvidenceLink[] = [ + { label: "Repository", href: "https://github.com/cbratkovics/entity-resolution", icon: Github }, + { label: "Results site", href: "https://cbratkovics.github.io/entity-resolution/", icon: ExternalLink }, + { label: "Findings", href: "https://github.com/cbratkovics/entity-resolution/blob/main/docs/FINDINGS.md", icon: ExternalLink }, + { label: "Methods card", href: "https://github.com/cbratkovics/entity-resolution/blob/main/docs/METHODS_CARD.md", icon: ExternalLink }, +]; + +export default function EntityResolutionCaseStudy() { + return <> + Skip to case study +