From b40b2c2805f8dad62b4e9ae2bfa7b4fc77fd350c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 14:06:56 +0000 Subject: [PATCH] =?UTF-8?q?feat(iter-8):=20anti-join=20levels=2066-67=20?= =?UTF-8?q?=E2=80=94=20LEFT=20JOIN=E2=80=A6IS=20NULL=20and=20NOT=20EXISTS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the anti-join syllabus gap (IMPROVEMENT_PLAN.md 4.5): the 65-level catalog had one EXISTS and no LEFT JOIN…IS NULL drill. - Level 66 (Expert, d3): Unfinanced Fleet Exposure — LEFT JOIN anti-join, NULL test on the right table's primary key. - Level 67 (Expert, d4): Depositors Without Loans — correlated NOT EXISTS with aggregation; teaches the NOT IN NULL trap. - LevelUpModal next-hint map extended; sentinel 65 -> 67. - Plan status and evolution log updated. Both results verified non-empty and deterministic against the seed; the generic integrity/determinism suites cover the new levels (375 tests). https://claude.ai/code/session_014xvARbMypxXekwHGyWRcYc --- IMPROVEMENT_PLAN.md | 15 +++++-- evolution_log.md | 26 +++++++++++ src/components/LevelUpModal.tsx | 4 +- src/data/levels/expert.ts | 77 +++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/IMPROVEMENT_PLAN.md b/IMPROVEMENT_PLAN.md index 855a291..b5a4265 100644 --- a/IMPROVEMENT_PLAN.md +++ b/IMPROVEMENT_PLAN.md @@ -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). --- diff --git a/evolution_log.md b/evolution_log.md index 4ae27b6..b62078d 100644 --- a/evolution_log.md +++ b/evolution_log.md @@ -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] diff --git a/src/components/LevelUpModal.tsx b/src/components/LevelUpModal.tsx index c71d659..4e70d66 100644 --- a/src/components/LevelUpModal.tsx +++ b/src/components/LevelUpModal.tsx @@ -21,6 +21,8 @@ const EPOCH_NEXT_HINT: Record = { 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() { @@ -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 ( diff --git a/src/data/levels/expert.ts b/src/data/levels/expert.ts index b8831cc..a66e490 100644 --- a/src/data/levels/expert.ts +++ b/src/data/levels/expert.ts @@ -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, + }, ];