From 4fc0d851e7c0f2053fc0a53e18d45ad26d7e7399 Mon Sep 17 00:00:00 2001 From: Elizabeth W Date: Wed, 20 May 2026 17:42:01 -0600 Subject: [PATCH] added base examples and drills --- README.md | 42 ++++++++++++- kata/auth/original/README.md | 3 + kata/auth/original/authService.js | 36 +++++++++++ kata/auth/original/state.json | 11 ++++ kata/billing/original/README.md | 3 + kata/billing/original/invoiceService.js | 39 ++++++++++++ kata/billing/original/state.json | 14 +++++ kata/example-seam-drill/README.md | 18 ++++++ kata/example-seam-drill/original/README.md | 17 +++++ kata/example-seam-drill/original/package.json | 12 ++++ .../original/taskService.js | 62 +++++++++++++++++++ kata/example-seam-drill/original/tasks.json | 15 +++++ .../scenarios/break-dependency/code/README.md | 17 +++++ .../break-dependency/code/package.json | 12 ++++ .../break-dependency/code/taskService.js | 62 +++++++++++++++++++ .../break-dependency/code/tasks.json | 15 +++++ .../break-dependency/drill-prompt.md | 12 ++++ .../scenarios/break-dependency/notes.md | 9 +++ .../scenarios/characterization/code/README.md | 17 +++++ .../characterization/code/package.json | 12 ++++ .../characterization/code/taskService.js | 62 +++++++++++++++++++ .../characterization/code/tasks.json | 15 +++++ .../characterization/drill-prompt.md | 12 ++++ .../scenarios/characterization/notes.md | 9 +++ .../scenarios/extract-policy/code/README.md | 17 +++++ .../extract-policy/code/package.json | 12 ++++ .../extract-policy/code/taskService.js | 62 +++++++++++++++++++ .../scenarios/extract-policy/code/tasks.json | 15 +++++ .../scenarios/extract-policy/drill-prompt.md | 12 ++++ .../scenarios/extract-policy/notes.md | 9 +++ kata/moderation/original/README.md | 3 + kata/moderation/original/moderationService.js | 30 +++++++++ kata/moderation/original/state.json | 11 ++++ kata/notifications/original/README.md | 3 + .../original/notificationService.js | 33 ++++++++++ kata/notifications/original/state.json | 10 +++ kata/shipping/original/README.md | 3 + kata/shipping/original/shippingService.js | 37 +++++++++++ kata/shipping/original/state.json | 12 ++++ notes/README.md | 8 +++ 40 files changed, 802 insertions(+), 1 deletion(-) create mode 100644 kata/auth/original/README.md create mode 100644 kata/auth/original/authService.js create mode 100644 kata/auth/original/state.json create mode 100644 kata/billing/original/README.md create mode 100644 kata/billing/original/invoiceService.js create mode 100644 kata/billing/original/state.json create mode 100644 kata/example-seam-drill/README.md create mode 100644 kata/example-seam-drill/original/README.md create mode 100644 kata/example-seam-drill/original/package.json create mode 100644 kata/example-seam-drill/original/taskService.js create mode 100644 kata/example-seam-drill/original/tasks.json create mode 100644 kata/example-seam-drill/scenarios/break-dependency/code/README.md create mode 100644 kata/example-seam-drill/scenarios/break-dependency/code/package.json create mode 100644 kata/example-seam-drill/scenarios/break-dependency/code/taskService.js create mode 100644 kata/example-seam-drill/scenarios/break-dependency/code/tasks.json create mode 100644 kata/example-seam-drill/scenarios/break-dependency/drill-prompt.md create mode 100644 kata/example-seam-drill/scenarios/break-dependency/notes.md create mode 100644 kata/example-seam-drill/scenarios/characterization/code/README.md create mode 100644 kata/example-seam-drill/scenarios/characterization/code/package.json create mode 100644 kata/example-seam-drill/scenarios/characterization/code/taskService.js create mode 100644 kata/example-seam-drill/scenarios/characterization/code/tasks.json create mode 100644 kata/example-seam-drill/scenarios/characterization/drill-prompt.md create mode 100644 kata/example-seam-drill/scenarios/characterization/notes.md create mode 100644 kata/example-seam-drill/scenarios/extract-policy/code/README.md create mode 100644 kata/example-seam-drill/scenarios/extract-policy/code/package.json create mode 100644 kata/example-seam-drill/scenarios/extract-policy/code/taskService.js create mode 100644 kata/example-seam-drill/scenarios/extract-policy/code/tasks.json create mode 100644 kata/example-seam-drill/scenarios/extract-policy/drill-prompt.md create mode 100644 kata/example-seam-drill/scenarios/extract-policy/notes.md create mode 100644 kata/moderation/original/README.md create mode 100644 kata/moderation/original/moderationService.js create mode 100644 kata/moderation/original/state.json create mode 100644 kata/notifications/original/README.md create mode 100644 kata/notifications/original/notificationService.js create mode 100644 kata/notifications/original/state.json create mode 100644 kata/shipping/original/README.md create mode 100644 kata/shipping/original/shippingService.js create mode 100644 kata/shipping/original/state.json create mode 100644 notes/README.md diff --git a/README.md b/README.md index acb1590..5b116a7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,43 @@ # refactor-kata -These are various practice drills for practicing refactoring. Using "Working With Legacy Code" by Feathers and "Refactoring" by Fowler \ No newline at end of file +Practice drills for refactoring, especially with ideas from *Working Effectively with Legacy Code* by Michael Feathers and *Refactoring* by Martin Fowler. + +## Repository layout + +- `kata//original/` + - the frozen messy starting point + - copy from here when creating a new drill scenario +- `kata//scenarios//code/` + - the working copy for a specific drill attempt +- `kata//scenarios//drill-prompt.md` + - instructions and success conditions for that drill +- `kata//scenarios//notes.md` + - notes on seams found, tests added, techniques used, and lessons learned +- `notes/` + - cross-kata notes and recurring patterns + +## Intended workflow + +1. Start with code in `original/`. +2. Copy that code into a scenario's `code/` folder. +3. Do the refactor only inside that scenario copy. +4. Record what you observed and learned in `notes.md`. +5. Reset by making a fresh scenario from `original/`. + +## Current starter kata + +- `kata/example-seam-drill/` + - a placeholder kata for practicing: + - characterization tests + - breaking one dependency with a seam + - extracting one pure decision from mixed logic + +## Goal + +The point is not to build a production app. The point is to practice: + +- finding seams in messy code +- adding characterization tests safely +- making small reviewable refactors +- separating decisions from mechanics +- gradually moving code toward stronger boundaries diff --git a/kata/auth/original/README.md b/kata/auth/original/README.md new file mode 100644 index 0000000..93c6c45 --- /dev/null +++ b/kata/auth/original/README.md @@ -0,0 +1,3 @@ +# auth original + +Messy login logic mixing lockout policy, clock handling, token generation, mutation, logging, and file I/O. diff --git a/kata/auth/original/authService.js b/kata/auth/original/authService.js new file mode 100644 index 0000000..c512cf3 --- /dev/null +++ b/kata/auth/original/authService.js @@ -0,0 +1,36 @@ +const fs = require("fs"); + +function login(filePath, username, password) { + const raw = fs.readFileSync(filePath, "utf8"); + const state = JSON.parse(raw); + const user = state.users.find((item) => item.username === username); + + if (!user) { + throw new Error("User not found"); + } + + if (user.lockedUntil && new Date(user.lockedUntil).getTime() > Date.now()) { + return { ok: false, reason: "locked" }; + } + + if (user.password !== password) { + user.failedAttempts = user.failedAttempts + 1; + + if (user.failedAttempts >= 3) { + user.lockedUntil = new Date(Date.now() + 15 * 60 * 1000).toISOString(); + } + + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + return { ok: false, reason: "invalid-password" }; + } + + user.failedAttempts = 0; + user.lockedUntil = null; + user.lastLoginAt = new Date().toISOString(); + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + console.log("Logged in", username); + + return { ok: true, token: "token-" + username + "-" + Date.now() }; +} + +module.exports = { login }; diff --git a/kata/auth/original/state.json b/kata/auth/original/state.json new file mode 100644 index 0000000..74e7d16 --- /dev/null +++ b/kata/auth/original/state.json @@ -0,0 +1,11 @@ +{ + "users": [ + { + "username": "ada", + "password": "secret", + "failedAttempts": 0, + "lockedUntil": null, + "lastLoginAt": null + } + ] +} diff --git a/kata/billing/original/README.md b/kata/billing/original/README.md new file mode 100644 index 0000000..ea9a3b5 --- /dev/null +++ b/kata/billing/original/README.md @@ -0,0 +1,3 @@ +# billing original + +Messy invoice finalization logic mixing pricing rules, time, logging, mutation, and file I/O. diff --git a/kata/billing/original/invoiceService.js b/kata/billing/original/invoiceService.js new file mode 100644 index 0000000..110c74e --- /dev/null +++ b/kata/billing/original/invoiceService.js @@ -0,0 +1,39 @@ +const fs = require("fs"); + +function finalizeInvoice(filePath, invoiceId, customerType) { + const raw = fs.readFileSync(filePath, "utf8"); + const state = JSON.parse(raw); + const invoice = state.invoices.find((item) => item.id === invoiceId); + + if (!invoice) { + throw new Error("Invoice not found"); + } + + let subtotal = 0; + + for (const line of invoice.lines) { + subtotal = subtotal + line.quantity * line.unitPrice; + } + + let discount = 0; + + if (customerType === "vip") { + discount = subtotal * 0.15; + } + + if (subtotal > 200) { + discount = discount + 10; + } + + const taxed = (subtotal - discount) * 1.1; + invoice.status = "finalized"; + invoice.total = Math.round(taxed * 100) / 100; + invoice.finalizedAt = new Date().toISOString(); + + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + console.log("Invoice finalized", invoiceId); + + return invoice; +} + +module.exports = { finalizeInvoice }; diff --git a/kata/billing/original/state.json b/kata/billing/original/state.json new file mode 100644 index 0000000..7ec8e0a --- /dev/null +++ b/kata/billing/original/state.json @@ -0,0 +1,14 @@ +{ + "invoices": [ + { + "id": "inv-1", + "status": "draft", + "total": 0, + "finalizedAt": null, + "lines": [ + { "description": "Hosting", "quantity": 2, "unitPrice": 60 }, + { "description": "Support", "quantity": 1, "unitPrice": 95 } + ] + } + ] +} diff --git a/kata/example-seam-drill/README.md b/kata/example-seam-drill/README.md new file mode 100644 index 0000000..d90282b --- /dev/null +++ b/kata/example-seam-drill/README.md @@ -0,0 +1,18 @@ +# example-seam-drill + +This kata is a placeholder example showing the intended practice layout. + +## Structure + +- `original/`: the frozen messy source you copy from for each drill +- `scenarios//code/`: a working copy for one drill attempt +- `scenarios//drill-prompt.md`: the drill instructions +- `scenarios//notes.md`: what you observed, tried, and learned + +## Suggested workflow + +1. Put the messy starting code in `original/`. +2. Copy it into a scenario's `code/` directory. +3. Do the drill only inside that scenario copy. +4. Record seam choices, tests added, and refactor steps in `notes.md`. +5. Reset by creating a new scenario from `original/`. diff --git a/kata/example-seam-drill/original/README.md b/kata/example-seam-drill/original/README.md new file mode 100644 index 0000000..2cd3830 --- /dev/null +++ b/kata/example-seam-drill/original/README.md @@ -0,0 +1,17 @@ +# original + +This is intentionally messy starter code. + +## Why it is messy + +- mixes file I/O, business rules, time, and logging in one method +- uses raw strings and generic objects instead of stronger primitives +- hard-codes reward logic inline +- depends directly on `new Date()` and `fs` +- mutates loaded state directly + +## Suggested drill targets + +- characterize current completion behavior +- break the file-system or clock dependency with a seam +- extract the points or badge decision into a pure function diff --git a/kata/example-seam-drill/original/package.json b/kata/example-seam-drill/original/package.json new file mode 100644 index 0000000..0d885b6 --- /dev/null +++ b/kata/example-seam-drill/original/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-seam-drill", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "test": "jest" + }, + "devDependencies": { + "jest": "^29.7.0" + } +} diff --git a/kata/example-seam-drill/original/taskService.js b/kata/example-seam-drill/original/taskService.js new file mode 100644 index 0000000..600b294 --- /dev/null +++ b/kata/example-seam-drill/original/taskService.js @@ -0,0 +1,62 @@ +const fs = require("fs"); + +class TaskService { + constructor(filePath) { + this.filePath = filePath; + } + + completeTask(taskName, userName) { + const raw = fs.readFileSync(this.filePath, "utf8"); + const state = JSON.parse(raw); + const task = state.tasks.find((item) => item.name === taskName); + + if (!task) { + throw new Error("Task not found"); + } + + if (task.completed) { + return { + message: "Task already complete", + pointsAwarded: 0, + badge: null + }; + } + + task.completed = true; + task.completedAt = new Date().toISOString(); + + let pointsAwarded = 10; + + if (userName && userName.toLowerCase() === "ada") { + pointsAwarded = pointsAwarded + 5; + } + + if (new Date().getDay() === 5) { + pointsAwarded = pointsAwarded * 2; + } + + state.points = state.points + pointsAwarded; + + let badge = null; + + if (state.points > 50) { + badge = "consistency-star"; + } + + fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2)); + + if (badge) { + console.log("Awarded badge:", badge); + } + + return { + message: "Task completed", + pointsAwarded, + badge + }; + } +} + +module.exports = { + TaskService +}; diff --git a/kata/example-seam-drill/original/tasks.json b/kata/example-seam-drill/original/tasks.json new file mode 100644 index 0000000..31e454e --- /dev/null +++ b/kata/example-seam-drill/original/tasks.json @@ -0,0 +1,15 @@ +{ + "points": 45, + "tasks": [ + { + "name": "Write daily summary", + "completed": false, + "completedAt": null + }, + { + "name": "Review inbox", + "completed": true, + "completedAt": "2026-05-18T08:00:00.000Z" + } + ] +} diff --git a/kata/example-seam-drill/scenarios/break-dependency/code/README.md b/kata/example-seam-drill/scenarios/break-dependency/code/README.md new file mode 100644 index 0000000..2cd3830 --- /dev/null +++ b/kata/example-seam-drill/scenarios/break-dependency/code/README.md @@ -0,0 +1,17 @@ +# original + +This is intentionally messy starter code. + +## Why it is messy + +- mixes file I/O, business rules, time, and logging in one method +- uses raw strings and generic objects instead of stronger primitives +- hard-codes reward logic inline +- depends directly on `new Date()` and `fs` +- mutates loaded state directly + +## Suggested drill targets + +- characterize current completion behavior +- break the file-system or clock dependency with a seam +- extract the points or badge decision into a pure function diff --git a/kata/example-seam-drill/scenarios/break-dependency/code/package.json b/kata/example-seam-drill/scenarios/break-dependency/code/package.json new file mode 100644 index 0000000..0d885b6 --- /dev/null +++ b/kata/example-seam-drill/scenarios/break-dependency/code/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-seam-drill", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "test": "jest" + }, + "devDependencies": { + "jest": "^29.7.0" + } +} diff --git a/kata/example-seam-drill/scenarios/break-dependency/code/taskService.js b/kata/example-seam-drill/scenarios/break-dependency/code/taskService.js new file mode 100644 index 0000000..600b294 --- /dev/null +++ b/kata/example-seam-drill/scenarios/break-dependency/code/taskService.js @@ -0,0 +1,62 @@ +const fs = require("fs"); + +class TaskService { + constructor(filePath) { + this.filePath = filePath; + } + + completeTask(taskName, userName) { + const raw = fs.readFileSync(this.filePath, "utf8"); + const state = JSON.parse(raw); + const task = state.tasks.find((item) => item.name === taskName); + + if (!task) { + throw new Error("Task not found"); + } + + if (task.completed) { + return { + message: "Task already complete", + pointsAwarded: 0, + badge: null + }; + } + + task.completed = true; + task.completedAt = new Date().toISOString(); + + let pointsAwarded = 10; + + if (userName && userName.toLowerCase() === "ada") { + pointsAwarded = pointsAwarded + 5; + } + + if (new Date().getDay() === 5) { + pointsAwarded = pointsAwarded * 2; + } + + state.points = state.points + pointsAwarded; + + let badge = null; + + if (state.points > 50) { + badge = "consistency-star"; + } + + fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2)); + + if (badge) { + console.log("Awarded badge:", badge); + } + + return { + message: "Task completed", + pointsAwarded, + badge + }; + } +} + +module.exports = { + TaskService +}; diff --git a/kata/example-seam-drill/scenarios/break-dependency/code/tasks.json b/kata/example-seam-drill/scenarios/break-dependency/code/tasks.json new file mode 100644 index 0000000..31e454e --- /dev/null +++ b/kata/example-seam-drill/scenarios/break-dependency/code/tasks.json @@ -0,0 +1,15 @@ +{ + "points": 45, + "tasks": [ + { + "name": "Write daily summary", + "completed": false, + "completedAt": null + }, + { + "name": "Review inbox", + "completed": true, + "completedAt": "2026-05-18T08:00:00.000Z" + } + ] +} diff --git a/kata/example-seam-drill/scenarios/break-dependency/drill-prompt.md b/kata/example-seam-drill/scenarios/break-dependency/drill-prompt.md new file mode 100644 index 0000000..306d612 --- /dev/null +++ b/kata/example-seam-drill/scenarios/break-dependency/drill-prompt.md @@ -0,0 +1,12 @@ +# Drill: break-dependency + +## Goal +Introduce one seam that reduces coupling to a hard dependency. + +## Prompt +Find one hard dependency in `code/` such as time, randomness, persistence, network, UI, or global state. Introduce the smallest seam you can so that behavior can be controlled more easily in tests. + +## Success conditions +- One dependency is isolated behind a seam +- The change is small and reviewable +- You can explain the enabling point and why it is safer to change now diff --git a/kata/example-seam-drill/scenarios/break-dependency/notes.md b/kata/example-seam-drill/scenarios/break-dependency/notes.md new file mode 100644 index 0000000..4a0cf2a --- /dev/null +++ b/kata/example-seam-drill/scenarios/break-dependency/notes.md @@ -0,0 +1,9 @@ +# Notes + +## Dependency chosen + +## Seam introduced + +## Why this seam + +## What I learned diff --git a/kata/example-seam-drill/scenarios/characterization/code/README.md b/kata/example-seam-drill/scenarios/characterization/code/README.md new file mode 100644 index 0000000..2cd3830 --- /dev/null +++ b/kata/example-seam-drill/scenarios/characterization/code/README.md @@ -0,0 +1,17 @@ +# original + +This is intentionally messy starter code. + +## Why it is messy + +- mixes file I/O, business rules, time, and logging in one method +- uses raw strings and generic objects instead of stronger primitives +- hard-codes reward logic inline +- depends directly on `new Date()` and `fs` +- mutates loaded state directly + +## Suggested drill targets + +- characterize current completion behavior +- break the file-system or clock dependency with a seam +- extract the points or badge decision into a pure function diff --git a/kata/example-seam-drill/scenarios/characterization/code/package.json b/kata/example-seam-drill/scenarios/characterization/code/package.json new file mode 100644 index 0000000..0d885b6 --- /dev/null +++ b/kata/example-seam-drill/scenarios/characterization/code/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-seam-drill", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "test": "jest" + }, + "devDependencies": { + "jest": "^29.7.0" + } +} diff --git a/kata/example-seam-drill/scenarios/characterization/code/taskService.js b/kata/example-seam-drill/scenarios/characterization/code/taskService.js new file mode 100644 index 0000000..600b294 --- /dev/null +++ b/kata/example-seam-drill/scenarios/characterization/code/taskService.js @@ -0,0 +1,62 @@ +const fs = require("fs"); + +class TaskService { + constructor(filePath) { + this.filePath = filePath; + } + + completeTask(taskName, userName) { + const raw = fs.readFileSync(this.filePath, "utf8"); + const state = JSON.parse(raw); + const task = state.tasks.find((item) => item.name === taskName); + + if (!task) { + throw new Error("Task not found"); + } + + if (task.completed) { + return { + message: "Task already complete", + pointsAwarded: 0, + badge: null + }; + } + + task.completed = true; + task.completedAt = new Date().toISOString(); + + let pointsAwarded = 10; + + if (userName && userName.toLowerCase() === "ada") { + pointsAwarded = pointsAwarded + 5; + } + + if (new Date().getDay() === 5) { + pointsAwarded = pointsAwarded * 2; + } + + state.points = state.points + pointsAwarded; + + let badge = null; + + if (state.points > 50) { + badge = "consistency-star"; + } + + fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2)); + + if (badge) { + console.log("Awarded badge:", badge); + } + + return { + message: "Task completed", + pointsAwarded, + badge + }; + } +} + +module.exports = { + TaskService +}; diff --git a/kata/example-seam-drill/scenarios/characterization/code/tasks.json b/kata/example-seam-drill/scenarios/characterization/code/tasks.json new file mode 100644 index 0000000..31e454e --- /dev/null +++ b/kata/example-seam-drill/scenarios/characterization/code/tasks.json @@ -0,0 +1,15 @@ +{ + "points": 45, + "tasks": [ + { + "name": "Write daily summary", + "completed": false, + "completedAt": null + }, + { + "name": "Review inbox", + "completed": true, + "completedAt": "2026-05-18T08:00:00.000Z" + } + ] +} diff --git a/kata/example-seam-drill/scenarios/characterization/drill-prompt.md b/kata/example-seam-drill/scenarios/characterization/drill-prompt.md new file mode 100644 index 0000000..313e7fe --- /dev/null +++ b/kata/example-seam-drill/scenarios/characterization/drill-prompt.md @@ -0,0 +1,12 @@ +# Drill: characterization + +## Goal +Capture current behavior before changing the code. + +## Prompt +Add characterization tests for the current behavior of the code in `code/` without intentionally changing behavior. + +## Success conditions +- At least one important behavior is captured in tests +- You can describe what the code currently does, even if the behavior is awkward +- No production behavior changes are introduced on purpose diff --git a/kata/example-seam-drill/scenarios/characterization/notes.md b/kata/example-seam-drill/scenarios/characterization/notes.md new file mode 100644 index 0000000..c975483 --- /dev/null +++ b/kata/example-seam-drill/scenarios/characterization/notes.md @@ -0,0 +1,9 @@ +# Notes + +## Observed behavior + +## Seams discovered + +## Tests added + +## What I learned diff --git a/kata/example-seam-drill/scenarios/extract-policy/code/README.md b/kata/example-seam-drill/scenarios/extract-policy/code/README.md new file mode 100644 index 0000000..2cd3830 --- /dev/null +++ b/kata/example-seam-drill/scenarios/extract-policy/code/README.md @@ -0,0 +1,17 @@ +# original + +This is intentionally messy starter code. + +## Why it is messy + +- mixes file I/O, business rules, time, and logging in one method +- uses raw strings and generic objects instead of stronger primitives +- hard-codes reward logic inline +- depends directly on `new Date()` and `fs` +- mutates loaded state directly + +## Suggested drill targets + +- characterize current completion behavior +- break the file-system or clock dependency with a seam +- extract the points or badge decision into a pure function diff --git a/kata/example-seam-drill/scenarios/extract-policy/code/package.json b/kata/example-seam-drill/scenarios/extract-policy/code/package.json new file mode 100644 index 0000000..0d885b6 --- /dev/null +++ b/kata/example-seam-drill/scenarios/extract-policy/code/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-seam-drill", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "test": "jest" + }, + "devDependencies": { + "jest": "^29.7.0" + } +} diff --git a/kata/example-seam-drill/scenarios/extract-policy/code/taskService.js b/kata/example-seam-drill/scenarios/extract-policy/code/taskService.js new file mode 100644 index 0000000..600b294 --- /dev/null +++ b/kata/example-seam-drill/scenarios/extract-policy/code/taskService.js @@ -0,0 +1,62 @@ +const fs = require("fs"); + +class TaskService { + constructor(filePath) { + this.filePath = filePath; + } + + completeTask(taskName, userName) { + const raw = fs.readFileSync(this.filePath, "utf8"); + const state = JSON.parse(raw); + const task = state.tasks.find((item) => item.name === taskName); + + if (!task) { + throw new Error("Task not found"); + } + + if (task.completed) { + return { + message: "Task already complete", + pointsAwarded: 0, + badge: null + }; + } + + task.completed = true; + task.completedAt = new Date().toISOString(); + + let pointsAwarded = 10; + + if (userName && userName.toLowerCase() === "ada") { + pointsAwarded = pointsAwarded + 5; + } + + if (new Date().getDay() === 5) { + pointsAwarded = pointsAwarded * 2; + } + + state.points = state.points + pointsAwarded; + + let badge = null; + + if (state.points > 50) { + badge = "consistency-star"; + } + + fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2)); + + if (badge) { + console.log("Awarded badge:", badge); + } + + return { + message: "Task completed", + pointsAwarded, + badge + }; + } +} + +module.exports = { + TaskService +}; diff --git a/kata/example-seam-drill/scenarios/extract-policy/code/tasks.json b/kata/example-seam-drill/scenarios/extract-policy/code/tasks.json new file mode 100644 index 0000000..31e454e --- /dev/null +++ b/kata/example-seam-drill/scenarios/extract-policy/code/tasks.json @@ -0,0 +1,15 @@ +{ + "points": 45, + "tasks": [ + { + "name": "Write daily summary", + "completed": false, + "completedAt": null + }, + { + "name": "Review inbox", + "completed": true, + "completedAt": "2026-05-18T08:00:00.000Z" + } + ] +} diff --git a/kata/example-seam-drill/scenarios/extract-policy/drill-prompt.md b/kata/example-seam-drill/scenarios/extract-policy/drill-prompt.md new file mode 100644 index 0000000..079c47e --- /dev/null +++ b/kata/example-seam-drill/scenarios/extract-policy/drill-prompt.md @@ -0,0 +1,12 @@ +# Drill: extract-policy + +## Goal +Move one mixed rule out of orchestration code into a pure decision. + +## Prompt +Find logic in `code/` that mixes decision-making with mechanics. Extract the decision into a pure function or policy with explicit inputs and outputs. + +## Success conditions +- The decision is separated from mechanics +- Inputs and outputs are explicit +- You can describe the command, event, and workflow role of the affected code diff --git a/kata/example-seam-drill/scenarios/extract-policy/notes.md b/kata/example-seam-drill/scenarios/extract-policy/notes.md new file mode 100644 index 0000000..af0f7ef --- /dev/null +++ b/kata/example-seam-drill/scenarios/extract-policy/notes.md @@ -0,0 +1,9 @@ +# Notes + +## Mixed logic found + +## Pure decision extracted + +## Command / event / workflow mapping + +## What I learned diff --git a/kata/moderation/original/README.md b/kata/moderation/original/README.md new file mode 100644 index 0000000..29320ac --- /dev/null +++ b/kata/moderation/original/README.md @@ -0,0 +1,3 @@ +# moderation original + +Messy moderation logic mixing policy rules, time, logging, mutation, and file I/O. diff --git a/kata/moderation/original/moderationService.js b/kata/moderation/original/moderationService.js new file mode 100644 index 0000000..9c8e799 --- /dev/null +++ b/kata/moderation/original/moderationService.js @@ -0,0 +1,30 @@ +const fs = require("fs"); + +function reviewPost(filePath, postId) { + const raw = fs.readFileSync(filePath, "utf8"); + const state = JSON.parse(raw); + const post = state.posts.find((item) => item.id === postId); + + if (!post) { + throw new Error("Post not found"); + } + + let action = "approve"; + + if (post.text.includes("buy now") || post.text.includes("free money")) { + action = "reject"; + } + + if (post.reports > 3) { + action = "escalate"; + } + + post.status = action; + post.reviewedAt = new Date().toISOString(); + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + console.log("Moderation action", postId, action); + + return { action }; +} + +module.exports = { reviewPost }; diff --git a/kata/moderation/original/state.json b/kata/moderation/original/state.json new file mode 100644 index 0000000..8480ee6 --- /dev/null +++ b/kata/moderation/original/state.json @@ -0,0 +1,11 @@ +{ + "posts": [ + { + "id": "post-1", + "text": "free money if you buy now", + "reports": 1, + "status": "pending", + "reviewedAt": null + } + ] +} diff --git a/kata/notifications/original/README.md b/kata/notifications/original/README.md new file mode 100644 index 0000000..4333a42 --- /dev/null +++ b/kata/notifications/original/README.md @@ -0,0 +1,3 @@ +# notifications original + +Messy reminder logic mixing quiet-hours rules, channel checks, time, logging, mutation, and file I/O. diff --git a/kata/notifications/original/notificationService.js b/kata/notifications/original/notificationService.js new file mode 100644 index 0000000..4b8fdf6 --- /dev/null +++ b/kata/notifications/original/notificationService.js @@ -0,0 +1,33 @@ +const fs = require("fs"); + +function sendReminder(filePath, userId, channel) { + const raw = fs.readFileSync(filePath, "utf8"); + const state = JSON.parse(raw); + const user = state.users.find((item) => item.id === userId); + + if (!user) { + throw new Error("User not found"); + } + + if (!user.remindersEnabled) { + return { sent: false, reason: "disabled" }; + } + + const hour = new Date().getHours(); + + if (hour < 8 || hour > 20) { + return { sent: false, reason: "quiet-hours" }; + } + + if (channel === "sms" && !user.phoneNumber) { + return { sent: false, reason: "missing-phone" }; + } + + user.lastReminderAt = new Date().toISOString(); + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + console.log("Sent reminder", userId, channel); + + return { sent: true, channel }; +} + +module.exports = { sendReminder }; diff --git a/kata/notifications/original/state.json b/kata/notifications/original/state.json new file mode 100644 index 0000000..5fbdc57 --- /dev/null +++ b/kata/notifications/original/state.json @@ -0,0 +1,10 @@ +{ + "users": [ + { + "id": "user-1", + "remindersEnabled": true, + "phoneNumber": "+15555555555", + "lastReminderAt": null + } + ] +} diff --git a/kata/shipping/original/README.md b/kata/shipping/original/README.md new file mode 100644 index 0000000..3c06e95 --- /dev/null +++ b/kata/shipping/original/README.md @@ -0,0 +1,3 @@ +# shipping original + +Messy shipping logic mixing eligibility, cost calculation, randomness, time, logging, and file I/O. diff --git a/kata/shipping/original/shippingService.js b/kata/shipping/original/shippingService.js new file mode 100644 index 0000000..cc2191e --- /dev/null +++ b/kata/shipping/original/shippingService.js @@ -0,0 +1,37 @@ +const fs = require("fs"); + +function shipOrder(filePath, orderId, destinationCountry) { + const raw = fs.readFileSync(filePath, "utf8"); + const state = JSON.parse(raw); + const order = state.orders.find((item) => item.id === orderId); + + if (!order) { + throw new Error("Order not found"); + } + + if (order.status === "shipped") { + return { message: "Already shipped", trackingNumber: order.trackingNumber }; + } + + let shippingCost = 5; + + if (destinationCountry !== "US") { + shippingCost = shippingCost + 15; + } + + if (order.weightKg > 10) { + shippingCost = shippingCost + 20; + } + + order.status = "shipped"; + order.shippingCost = shippingCost; + order.trackingNumber = "trk-" + Math.floor(Math.random() * 100000); + order.shippedAt = new Date().toISOString(); + + fs.writeFileSync(filePath, JSON.stringify(state, null, 2)); + console.log("Order shipped", orderId); + + return { message: "Shipped", trackingNumber: order.trackingNumber, shippingCost }; +} + +module.exports = { shipOrder }; diff --git a/kata/shipping/original/state.json b/kata/shipping/original/state.json new file mode 100644 index 0000000..fa5232d --- /dev/null +++ b/kata/shipping/original/state.json @@ -0,0 +1,12 @@ +{ + "orders": [ + { + "id": "ord-1", + "status": "paid", + "weightKg": 12, + "shippingCost": 0, + "trackingNumber": null, + "shippedAt": null + } + ] +} diff --git a/notes/README.md b/notes/README.md new file mode 100644 index 0000000..b3c9a24 --- /dev/null +++ b/notes/README.md @@ -0,0 +1,8 @@ +# Practice Notes + +Use this directory for cross-kata notes: + +- patterns you keep noticing +- seams that worked well +- refactor techniques that felt natural or hard +- mapping from legacy code moves to effect-template ideas