From 9fb9de4fd253896c308639b9f2ae8ff8b0a65fd0 Mon Sep 17 00:00:00 2001 From: Kim Junmo Date: Thu, 9 Apr 2026 07:59:22 +0900 Subject: [PATCH] fix: preserve original case in handelize() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The blanket .toLowerCase() in handelize() drops filename casing, which breaks path resolution on case-sensitive filesystems (Linux). Files like README.md, CHANGELOG.md, and SKILL.md become unreachable when the index stores them as readme.md, changelog.md, skill.md. Since FTS5 already performs case-insensitive matching via the unicode61 tokenizer, lowercasing the stored path provides no search benefit — it only corrupts the metadata used to locate files on disk. Remove .toLowerCase() and update all affected test expectations. --- src/store.ts | 3 +-- test/cli.test.ts | 8 ++++---- test/store.helpers.unit.test.ts | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/store.ts b/src/store.ts index ab4cbf4..2c26522 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1693,11 +1693,11 @@ export function getDocid(hash: string): string { /** * Handelize a filename to be more token-friendly. * - Convert triple underscore `___` to `/` (folder separator) - * - Convert to lowercase * - Replace sequences of non-word chars (except /) with single dash * - Remove leading/trailing dashes from path segments * - Preserve folder structure (a/b/c/d.md stays structured) * - Preserve file extension + * - Preserve original case (important for case-sensitive filesystems) */ /** Replace emoji/symbol codepoints with their hex representation (e.g. 🐘 → 1f418) */ function emojiToHex(str: string): string { @@ -1725,7 +1725,6 @@ export function handelize(path: string): string { const result = path .replace(/___/g, '/') // Triple underscore becomes folder separator - .toLowerCase() .split('/') .map((segment, idx, arr) => { const isLastSegment = idx === arr.length - 1; diff --git a/test/cli.test.ts b/test/cli.test.ts index c9c5644..f1acf8a 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -837,8 +837,8 @@ describe("CLI ls Command", () => { test("lists files in a collection", async () => { const { stdout, exitCode } = await runQmd(["ls", "fixtures"], { dbPath: localDbPath }); expect(exitCode).toBe(0); - // handelize converts to lowercase - expect(stdout).toContain("qmd://fixtures/readme.md"); + // handelize preserves original case + expect(stdout).toContain("qmd://fixtures/README.md"); expect(stdout).toContain("qmd://fixtures/notes/meeting.md"); }); @@ -847,8 +847,8 @@ describe("CLI ls Command", () => { expect(exitCode).toBe(0); expect(stdout).toContain("qmd://fixtures/notes/meeting.md"); expect(stdout).toContain("qmd://fixtures/notes/ideas.md"); - // Should not include files outside the prefix (handelize converts to lowercase) - expect(stdout).not.toContain("qmd://fixtures/readme.md"); + // Should not include files outside the prefix (case preserved) + expect(stdout).not.toContain("qmd://fixtures/README.md"); }); test("lists files with virtual path", async () => { diff --git a/test/store.helpers.unit.test.ts b/test/store.helpers.unit.test.ts index 99af680..9adefc9 100644 --- a/test/store.helpers.unit.test.ts +++ b/test/store.helpers.unit.test.ts @@ -119,14 +119,14 @@ describe("cleanupOrphanedVectors", () => { // ============================================================================= describe("handelize", () => { - test("converts to lowercase", () => { - expect(handelize("README.md")).toBe("readme.md"); - expect(handelize("MyFile.MD")).toBe("myfile.md"); + test("preserves original case", () => { + expect(handelize("README.md")).toBe("README.md"); + expect(handelize("MyFile.MD")).toBe("MyFile.MD"); }); test("preserves folder structure", () => { expect(handelize("a/b/c/d.md")).toBe("a/b/c/d.md"); - expect(handelize("docs/api/README.md")).toBe("docs/api/readme.md"); + expect(handelize("docs/api/README.md")).toBe("docs/api/README.md"); }); test("replaces non-word characters with dash", () => { @@ -156,7 +156,7 @@ describe("handelize", () => { test("handles complex real-world meeting notes", () => { const complexName = "Money Movement Licensing Review - 2025/11/19 10:25 EST - Notes by Gemini.md"; const result = handelize(complexName); - expect(result).toBe("money-movement-licensing-review-2025-11-19-10-25-est-notes-by-gemini.md"); + expect(result).toBe("Money-Movement-Licensing-Review-2025-11-19-10-25-EST-Notes-by-Gemini.md"); expect(result).not.toContain(" "); expect(result).not.toContain("/"); expect(result).not.toContain(":"); @@ -164,7 +164,7 @@ describe("handelize", () => { test("handles unicode characters", () => { expect(handelize("日本語.md")).toBe("日本語.md"); - expect(handelize("Зоны и проекты.md")).toBe("зоны-и-проекты.md"); + expect(handelize("Зоны и проекты.md")).toBe("Зоны-и-проекты.md"); expect(handelize("café-notes.md")).toBe("café-notes.md"); expect(handelize("naïve.md")).toBe("naïve.md"); expect(handelize("日本語-notes.md")).toBe("日本語-notes.md"); @@ -186,13 +186,13 @@ describe("handelize", () => { test("handles dates and times in filenames", () => { expect(handelize("meeting-2025-01-15.md")).toBe("meeting-2025-01-15.md"); expect(handelize("notes 2025/01/15.md")).toBe("notes-2025/01/15.md"); - expect(handelize("call_10:30_AM.md")).toBe("call-10-30-am.md"); + expect(handelize("call_10:30_AM.md")).toBe("call-10-30-AM.md"); }); test("handles special project naming patterns", () => { - expect(handelize("PROJECT_ABC_v2.0.md")).toBe("project-abc-v2-0.md"); - expect(handelize("[WIP] Feature Request.md")).toBe("wip-feature-request.md"); - expect(handelize("(DRAFT) Proposal v1.md")).toBe("draft-proposal-v1.md"); + expect(handelize("PROJECT_ABC_v2.0.md")).toBe("PROJECT-ABC-v2-0.md"); + expect(handelize("[WIP] Feature Request.md")).toBe("WIP-Feature-Request.md"); + expect(handelize("(DRAFT) Proposal v1.md")).toBe("DRAFT-Proposal-v1.md"); }); test("handles symbol-only route filenames", () => {