Forráskód Böngészése

fix: preserve original case in handelize()

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.
Kim Junmo 1 hónapja
szülő
commit
9fb9de4fd2
3 módosított fájl, 15 hozzáadás és 16 törlés
  1. 1 2
      src/store.ts
  2. 4 4
      test/cli.test.ts
  3. 10 10
      test/store.helpers.unit.test.ts

+ 1 - 2
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;

+ 4 - 4
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 () => {

+ 10 - 10
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", () => {