Skip to content
Open
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
2 changes: 1 addition & 1 deletion .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
set -euo pipefail

# Install the AI CLI
npm install -g @anthropic-ai/claude-code
pnpm add -g @anthropic-ai/claude-code

# Fix ownership of named volumes (Docker creates them as root, container runs as the dev user)
sudo chown -R node:node /home/node/.claude /home/node/.config/gh
Expand Down
88 changes: 77 additions & 11 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
# =============================================================================
#
# WHAT IT DOES
# Before every commit, if you staged any test files
# (leetcode-playground/tests/*.test.js), this hook runs the matching
# reference solution against those tests to confirm the expected values
# Before every commit, if you staged any supported test files, this hook
# runs the matching reference solution against those tests to confirm the
# expected values
# are correct. If any test fails, the commit is blocked.
#
# This catches the most common mistake: forgetting to replace the
Expand Down Expand Up @@ -40,7 +40,7 @@ CHECKED=0 # How many test files were actually run.
# -----------------------------------------------------------------------------
discover_staged_tests() {
git diff --cached --name-only --diff-filter=ACMR \
| grep "^leetcode-playground/tests/.*\.test\.js$" \
| grep -E "^(leetcode-playground/tests|interview-questions/tests)/.*\.test\.js$" \
|| true # grep exits 1 on no match; swallow to keep `set -e` happy.
}

Expand Down Expand Up @@ -85,7 +85,7 @@ build_injected_file() {
# -----------------------------------------------------------------------------
run_vitest_and_check() {
local test_file="$1"
npx vitest run "$test_file" 2>&1 || true
pnpm exec vitest run "$test_file" 2>&1 || true
}


Expand Down Expand Up @@ -127,6 +127,12 @@ verify_one_test() {
local test_file="$1"
local basename
basename=$(basename "$test_file" .test.js)

if [[ "$test_file" == interview-questions/tests/* ]]; then
verify_interview_test "$test_file" "$basename"
return
fi

local playground_file="leetcode-playground/${basename}.js"
local ref_file
ref_file=$(ls leetcode-solutions/${basename}.js 2>/dev/null || true)
Expand All @@ -137,14 +143,22 @@ verify_one_test() {
# NOTE FOR CONTRIBUTORS: you don't need to add a file to leetcode-solutions/.
# Just verify manually before committing:
# 1. Write a real working solution in the playground file
# 2. Run: npx vitest run <test-file>
# 2. Run: pnpm exec vitest run <test-file>
# 3. Copy the actual values into your .toEqual() assertions
# 4. Restore the stub: git checkout -- <playground-file>
if [ -z "$ref_file" ] || [ ! -f "$ref_file" ]; then
echo ""
echo " WARNING: $basename — no reference solution found."
echo " Expected values MUST be verified manually before this commit."
echo " Implement a temporary solution, run npx vitest run, confirm outputs, restore stub."
echo " BLOCKED: $test_file"
echo " No LeetCode reference solution was found at: leetcode-solutions/${basename}.js"
if [ ! -f "$playground_file" ]; then
echo " No matching implementation was found at: $playground_file"
echo " This looks like an interview-question test in the LeetCode test folder."
echo " Move it to interview-questions/tests/ and keep its implementation in interview-questions/."
else
echo " Expected values must be verified manually for this test."
echo " Temporarily implement $playground_file, run: pnpm exec vitest run $test_file"
echo " Then restore the stub before committing."
fi
echo ""
FAILED=$((FAILED + 1))
return
Expand Down Expand Up @@ -186,6 +200,57 @@ verify_one_test() {
}


# -----------------------------------------------------------------------------
# verify_interview_test <test_file> <basename>
# Interview-question solutions are colocated separately from LeetCode files.
# Their tests use the implementation in interview-questions/ and the
# corresponding reference in interview-questions/solution/.
# -----------------------------------------------------------------------------
verify_interview_test() {
local test_file="$1"
local basename="$2"
local implementation_name="$basename"

# The test is conventionally named currying.test.js while the function
# implementation is named curry.js.
if [ "$basename" = "currying" ]; then
implementation_name="curry"
fi

local implementation_file="interview-questions/${implementation_name}.js"
local ref_file="interview-questions/solution/${implementation_name}.js"

if [ ! -f "$implementation_file" ] || [ ! -f "$ref_file" ]; then
echo ""
echo " BLOCKED: $test_file"
echo " Interview-question tests must have these colocated files:"
echo " implementation: $implementation_file"
echo " reference: $ref_file"
echo " Add or rename the missing file, or remove this test from the commit."
echo ""
FAILED=$((FAILED + 1))
return
fi

CHECKED=$((CHECKED + 1))
local original_content
original_content=$(cat "$implementation_file")
cp "$ref_file" "$implementation_file"

local result
result=$(run_vitest_and_check "$test_file")

printf '%s\n' "$original_content" > "$implementation_file"

if vitest_failed "$result" || echo "$result" | grep -qE "Failed Suites|Test Files.*failed"; then
report_blocked "$basename" "$result"
FAILED=$((FAILED + 1))
else
echo " OK: $basename (interview-question reference)"
fi
}


# -----------------------------------------------------------------------------
# print_summary
# Print the final result and exit with the right code.
Expand All @@ -194,8 +259,9 @@ print_summary() {
if [ "$FAILED" -gt 0 ]; then
echo ""
echo "Commit blocked: $FAILED test file(s) unverified or failed."
echo "For ref solutions: run verification workflow (CLAUDE.md)."
echo "For no ref solution: implement temp solution, run vitest, confirm outputs, restore stub."
echo "For each BLOCKED file above, either:"
echo " - fix its path/name so it matches the expected implementation and reference layout; or"
echo " - verify its expected values using the command shown above, then restore any temporary stub."
exit 1
fi

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ At least 2-3 meaningful test cases with descriptive names. `describe` block name

## Verifying Test Correctness with Reference Solutions

**Only proof a test file is correct: `npm run test` with reference solution injected returns 0 failures. Nothing else counts — not reasoning, not manual tracing, not "obvious" cases.**
**Only proof a test file is correct: `pnpm run test` with reference solution injected returns 0 failures. Nothing else counts — not reasoning, not manual tracing, not "obvious" cases.**

**When `leetcode-solutions/NNNN-*.js` exists, this workflow is required before committing. No skipping.**

### Workflow

1. **Inject reference solution**: Copy from `leetcode-solutions/` into `leetcode-playground/`, append `export { functionName }` (source files have no exports)
2. **Run tests**: `npm run test -- NNNN-problem-name.test.js` — read actual values from failure messages
2. **Run tests**: `pnpm run test -- NNNN-problem-name.test.js` — read actual values from failure messages
3. **Update assertions**: Replace each placeholder with value reference solution returned (shown as `expected X to equal null`)
4. **Re-run**: Must show **0 failures**. If any still fail, repeat step 3.
5. **Restore stub**: `git checkout -- leetcode-playground/NNNN-problem-name.js`
Expand Down
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ If you find a bug in an existing solution or test case, please open an issue or

1. **Install dependencies**:
```bash
npm install
pnpm install
```

2. **Run tests**:
```bash
# Run all tests
npm run test
pnpm run test

# Run tests in watch mode
npm run test:watch
pnpm run test:watch

# Run a specific test
npx vitest tests/0001-two-sum.test.js
pnpm exec vitest tests/0001-two-sum.test.js
```

## Coding Guidelines
Expand Down
2 changes: 1 addition & 1 deletion data-structure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ These are NOT meant to replace `Map`/`Set`/`Array` in production. They exist to
## Run tests

```bash
npm run test -- data-structure
pnpm run test -- data-structure
```

## Style guide for adding new structures
Expand Down
8 changes: 8 additions & 0 deletions interview-questions/curry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function curry(func) {
function curried(...args) {
if (args.length >= func.length) return func.apply(this, args);

return (...nextArgs) => curried.apply(this, [...args, ...nextArgs]);
}
return curried;
}
37 changes: 37 additions & 0 deletions interview-questions/flatten.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Flatten a value into a single-level array.
*
* If the input is not an array, wrap it in an array.
* If the input is an array, flatten nested arrays at any depth.
*
* @param {Array<*|Array>|*} value
* @return {Array}
*/
export default function flatten(value) {
//
if (!Array.isArray(value)) {
return [value];

// flatten(1);
// [1]

// flatten("hello");
// ["hello"]

// flatten(null);
// [null]
}

// otherwise, must be array, we start collecting all of the item and push to result
return value.reduce((acc, item) => {
// is the individual item array as well?
if (Array.isArray(item)) {
acc.push(...flatten(item)); // delegate that ...flatten(item) to flatten all, then spread push to acc
} else {
// its just normal single item, can just push.
acc.push(item);
}

return acc;
}, []); // initial [] array is important, so first iteration of acc.push will not error
}
10 changes: 10 additions & 0 deletions interview-questions/solution/curry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function curry(func) {
function curried(...args) {
if (func.length === 0) return func.call(this);
if (args.length >= func.length) return func.apply(this, args);

return (...nextArgs) => curried.apply(this, [...args, ...nextArgs]);
}

return curried;
}
15 changes: 15 additions & 0 deletions interview-questions/solution/flatten.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function flatten(value) {
if (!Array.isArray(value)) {
return [value];
}

return value.reduce((acc, item) => {
if (Array.isArray(item)) {
acc.push(...flatten(item));
} else {
acc.push(item);
}

return acc;
}, []);
}
75 changes: 75 additions & 0 deletions interview-questions/tests/currying.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import curry from "../../interview-questions/curry";

const empty = () => 0;
const square = (a) => a * a;
const mul = (a, b) => a * b;
const mulThree = (a, b, c) => a * b * c;

describe("curry", () => {
test("returns function", () => {
const curried = curry(square);
expect(curried).toBeInstanceOf(Function);
});

test("empty function", () => {
const curried = curry(empty);
expect(curried()).toBe(0);
});

test("single argument", () => {
const curried = curry(square);
expect(curried()).toBeInstanceOf(Function);
expect(curried(2)).toBe(4);
});

test("two arguments", () => {
const curried = curry(mul);
expect(curried()).toBeInstanceOf(Function);
expect(curried(7)(3)).toBe(21);
});

test("multiple arguments", () => {
const curried = curry(mulThree);
expect(curried()).toBeInstanceOf(Function);
expect(curried(7)(3)(2)).toBe(42);
});

test("can be reused", () => {
const curried = curry(square);
expect(curried()).toBeInstanceOf(Function);
expect(curried(2)).toBe(4);
expect(curried(3)).toBe(9);
});

test("ignores empty args", () => {
const curried = curry(mulThree);
expect(curried()(4)()(3)()(2)).toBe(24);
expect(curried()()()()(4)(2)(3)).toBe(24);
});

describe("can access this", () => {
test("single parameter", () => {
const curried = curry(function fn(val) {
return this.multiplier * val;
});

const obj = { multiplier: 5, mul: curried };
expect(obj.mul()).toBeInstanceOf(Function);
expect(obj.mul(7)).toBe(35);
});

describe("multiple arguments", () => {
test("preserves this across partial applications", () => {
const curried = curry(function fn(foo, bar) {
return this.base * foo + bar;
});

const obj = { base: 5, mul: curried };
expect(obj.mul()).toBeInstanceOf(Function);
expect(obj.mul(3)(2)).toBe(17);
expect(obj.mul(3)()(2)).toBe(17);
expect(obj.mul()(3)()(2)).toBe(17);
});
});
});
});
38 changes: 38 additions & 0 deletions interview-questions/tests/flatten.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import flatten from "../flatten";

describe("flatten", () => {
test("returns a new array for a flat array", () => {
expect(flatten([1, 2, 3])).toEqual([1, 2, 3]);
});

test("flattens one level of nested arrays", () => {
expect(flatten([1, [2, 3], 4])).toEqual([1, 2, 3, 4]);
});

test("flattens deeply nested arrays", () => {
expect(flatten([1, [2, [3, [4]]]])).toEqual([1, 2, 3, 4]);
});

test("handles empty arrays", () => {
expect(flatten([])).toEqual([]);
});

test("removes nested empty arrays", () => {
expect(flatten([[], [1, []], [[2]]])).toEqual([1, 2]);
});

test("keeps falsy and nullish values", () => {
expect(flatten([null, [undefined, [false, 0, ""]]])).toEqual([
null,
undefined,
false,
0,
"",
]);
});

test("wraps non-array values", () => {
expect(flatten("hello")).toEqual(["hello"]);
expect(flatten(42)).toEqual([42]);
});
});
Loading