Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions IMPROVEMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,18 @@ Implemented in the follow-up PR:
aria-labels on icon buttons, and a stacking responsive layout below the `lg` breakpoint.
- **Pillar 5:** MIT LICENSE, CONTRIBUTING.md with a level-authoring guide.

Still open: build-time expected results (3.4), the expected-vs-actual value diff (4.1 — the
shape reveal shipped; a value-level diff is a deliberate product decision), and a deeper
mobile/touch pass (4.4).
Implemented since (iteration 8):

- **4.5 (partial):** the anti-join syllabus gap is closed — level 66 teaches the
`LEFT JOIN … IS NULL` anti-join, level 67 teaches correlated `NOT EXISTS` (and the
`NOT IN` NULL trap). Remaining 4.5 gaps: a dedicated date-functions drill and earlier
`CASE` exposure are partially covered by existing levels (28, 38, 62, 65) — revisit
only if player feedback shows a hole.

Still open: build-time expected results (3.4 — deprioritized: the level-up modal now
deliberately reveals the model answer after a win, so hiding solutions from the bundle
buys nothing), the expected-vs-actual value diff (4.1 — the shape reveal shipped; a
value-level diff is a deliberate product decision), and a deeper mobile/touch pass (4.4).

---

Expand Down
26 changes: 26 additions & 0 deletions evolution_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ Each entry records one autonomous improvement iteration.

---

## Iteration 8 — 2026-06-12

### [Game Design Tweaks]

- **Levels 66–67: the anti-join gap is closed.** The 65-level catalog contained exactly one
`EXISTS` and no dedicated `LEFT JOIN … IS NULL` drill — yet "find the rows in A with no
match in B" is among the most common SQL interview questions (IMPROVEMENT_PLAN.md §4.5).
- **Level 66** *(Expert, difficulty 3)* — "Unfinanced Fleet Exposure (Anti-Join)": vessels
with no trade finance facility on file, via `LEFT JOIN trade_finance_facilities … WHERE
f.facility_id IS NULL`, ordered by `dwt_tonnes` DESC. The description teaches why the
NULL test belongs on the right table's primary key. The four matching vessels are exactly
the ones not owned by LCB customers — uncollateralised third-party carriers, which makes
the compliance narrative true in the data, not just flavour text.
- **Level 67** *(Expert, difficulty 4)* — "Lending Whitespace: Depositors Without Loans
(NOT EXISTS)": customers holding accounts but no loans, with `total_deposits` aggregated,
via correlated `NOT EXISTS`. The description teaches the `NOT IN` NULL trap (one NULL in
the subquery silently yields zero rows) — the reason production code prefers `NOT EXISTS`.
- Both levels' results were verified non-empty and deterministic against the live seed
(distinct ORDER BY keys, 4 and 5 rows respectively) before authoring; the generic
level-integrity and determinism suites picked them up automatically (375 tests green).
- **`LevelUpModal` next-hint map** extended with entries for 66 (LEFT JOIN anti-join) and 67
(NOT EXISTS); sentinel advanced 65 → 67. Level counts everywhere else derive from
`levels.length` (Iteration 5's progression module), so no other touch points existed.

---

## Iteration 7 — 2026-06-12

### [UI/UX Improvements]
Expand Down
4 changes: 3 additions & 1 deletion src/components/LevelUpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const EPOCH_NEXT_HINT: Record<number, string> = {
63: 'Rolling volatility: SQRT(AVG(x²) − AVG(x)²) — population std dev via window functions',
64: 'NTILE bucketing: equal-count portfolio tranching for Basel III capital adequacy reporting',
65: 'Running balance: SUM() OVER (ROWS UNBOUNDED PRECEDING) — the universal treasury ledger pattern',
66: 'Anti-joins: LEFT JOIN … IS NULL — finding the rows that have no match',
67: 'NOT EXISTS: the correlated anti-join without the NOT IN NULL trap',
};

export default function LevelUpModal() {
Expand Down Expand Up @@ -57,7 +59,7 @@ export default function LevelUpModal() {
return () => window.removeEventListener('keydown', onKey);
}, [showLevelUp, handleClose]);

const nextHintKey = [10, 20, 30, 40, 44, 54, 57, 59, 61, 63, 64, 65].find((k) => currentLevel < k) ?? 65;
const nextHintKey = [10, 20, 30, 40, 44, 54, 57, 59, 61, 63, 64, 65, 66, 67].find((k) => currentLevel < k) ?? 67;
const xpEarned = xpFor(currentLevel);

return (
Expand Down
77 changes: 77 additions & 0 deletions src/data/levels/expert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,4 +955,81 @@ SELECT month,
epoch: 'Expert',
difficulty: 4,
},

// ============================================================
// ANTI-JOIN PATTERNS (Levels 66–67)
// LEFT JOIN … IS NULL; correlated NOT EXISTS
// ============================================================

{
id: 66,
title: 'Unfinanced Fleet Exposure (Anti-Join)',
description: `Trade Finance Compliance has flagged a gap: some vessels moving cargo through our network have **no trade finance facility on file** — no Letter of Credit, no Shipping Guarantee, nothing. Every one of them is uncollateralised operational risk.

Find every vessel with no row in \`trade_finance_facilities\`. Return \`vessel_name\`, \`vessel_type\`, \`flag_state\`, and \`dwt_tonnes\`, ordered by \`dwt_tonnes\` descending (largest exposure first).

Use the **anti-join** pattern: \`LEFT JOIN\` the facilities table, then keep only the rows where the join found no match — \`WHERE f.facility_id IS NULL\`. Test a column that can never be NULL in a real match (the primary key is the safe choice): if it's NULL after a LEFT JOIN, the match doesn't exist.

"Find the rows in A with no match in B" is one of the most common interview questions there is — customers with no orders, users with no logins, products never sold. This LEFT JOIN form is the classic answer.`,
hint: "LEFT JOIN trade_finance_facilities f ON f.vessel_id = v.vessel_id, then WHERE f.facility_id IS NULL. The NULL test must be on the right-hand (facilities) table — that's what marks an unmatched row.",
seedQuery: `SELECT v.vessel_name,
v.vessel_type,
v.flag_state,
v.dwt_tonnes
FROM vessels v
LEFT JOIN trade_finance_facilities f ON
WHERE f.facility_id IS
ORDER BY v.dwt_tonnes DESC`,
solutionQuery: `SELECT v.vessel_name,
v.vessel_type,
v.flag_state,
v.dwt_tonnes
FROM vessels v
LEFT JOIN trade_finance_facilities f ON f.vessel_id = v.vessel_id
WHERE f.facility_id IS NULL
ORDER BY v.dwt_tonnes DESC`,
epoch: 'Expert',
difficulty: 3,
},

{
id: 67,
title: 'Lending Whitespace: Depositors Without Loans (NOT EXISTS)',
description: `The Retail Lending desk wants a cross-sell target list: customers who keep deposits with LCB but have **never taken a loan** from us. Their balances tell us they trust the bank — the lending relationship is pure whitespace.

Find every customer who holds at least one account but has no row in \`loans\`. Return \`customer_name\`, \`segment\`, \`credit_score\`, and \`total_deposits\` — the sum of their account balances, rounded to 2 dp — ordered by \`total_deposits\` descending.

Use a correlated \`NOT EXISTS\` subquery: \`WHERE NOT EXISTS (SELECT 1 FROM loans l WHERE l.customer_id = c.customer_id)\`. The inner JOIN to \`accounts\` already restricts the list to account holders, so the aggregation and the anti-join compose in one pass.

Prefer \`NOT EXISTS\` over \`NOT IN\` for anti-joins: if the \`NOT IN\` subquery ever returns a NULL, the whole predicate goes unknown and you silently get **zero rows** — a classic production bug. \`NOT EXISTS\` has no such trap, and optimisers handle it well.`,
hint: "JOIN accounts for the deposit sum, GROUP BY the customer, and add WHERE NOT EXISTS (SELECT 1 FROM loans l WHERE l.customer_id = c.customer_id). The subquery is correlated — it references the outer customer row.",
seedQuery: `SELECT c.customer_name,
c.segment,
c.credit_score,
ROUND(SUM(a.balance), 2) AS total_deposits
FROM customers c
JOIN accounts a ON a.customer_id = c.customer_id
WHERE NOT EXISTS (
SELECT 1
FROM loans l
WHERE
)
GROUP BY
ORDER BY total_deposits DESC`,
solutionQuery: `SELECT c.customer_name,
c.segment,
c.credit_score,
ROUND(SUM(a.balance), 2) AS total_deposits
FROM customers c
JOIN accounts a ON a.customer_id = c.customer_id
WHERE NOT EXISTS (
SELECT 1
FROM loans l
WHERE l.customer_id = c.customer_id
)
GROUP BY c.customer_id, c.customer_name, c.segment, c.credit_score
ORDER BY total_deposits DESC`,
epoch: 'Expert',
difficulty: 4,
},
];
Loading