@rekal/mem 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{db-BMh1OP4b.mjs → db-CHpq7OOi.mjs} +46 -15
- package/dist/db-CHpq7OOi.mjs.map +1 -0
- package/dist/doc-DnYN4jAU.mjs +2 -0
- package/dist/doc-DnYN4jAU.mjs.map +1 -0
- package/dist/{embed-rUMZxqed.mjs → embed-CZI5Dz1q.mjs} +3 -1
- package/dist/embed-CZI5Dz1q.mjs.map +1 -0
- package/dist/frecency-CiaqPIOy.mjs +30 -0
- package/dist/frecency-CiaqPIOy.mjs.map +1 -0
- package/dist/fs-DMp26Byo.mjs +2 -0
- package/dist/fs-DMp26Byo.mjs.map +1 -0
- package/dist/glob.d.mts +2 -1
- package/dist/glob.d.mts.map +1 -0
- package/dist/glob.mjs +2 -0
- package/dist/glob.mjs.map +1 -0
- package/dist/index.d.mts +21 -11
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +7 -5
- package/dist/index.mjs.map +1 -0
- package/dist/{llama-CT3dc9Cn.mjs → llama-CpNV7Lh9.mjs} +3 -1
- package/dist/llama-CpNV7Lh9.mjs.map +1 -0
- package/dist/{models-DFQSgBNr.mjs → models-Bo6czhQe.mjs} +5 -3
- package/dist/models-Bo6czhQe.mjs.map +1 -0
- package/dist/{openai-j2_2GM4J.mjs → openai-ALl6_YhI.mjs} +3 -1
- package/dist/openai-ALl6_YhI.mjs.map +1 -0
- package/dist/progress-B1JdNapX.mjs +2 -0
- package/dist/progress-B1JdNapX.mjs.map +1 -0
- package/dist/query-VFSpErTB.mjs +2 -0
- package/dist/query-VFSpErTB.mjs.map +1 -0
- package/dist/runtime.node-DlQPaGrV.mjs +2 -0
- package/dist/runtime.node-DlQPaGrV.mjs.map +1 -0
- package/dist/{search-BllHWtZF.mjs → search-DsVjB-9f.mjs} +2 -0
- package/dist/search-DsVjB-9f.mjs.map +1 -0
- package/dist/{store-DE7S35SS.mjs → store-I5nVEYxK.mjs} +10 -6
- package/dist/store-I5nVEYxK.mjs.map +1 -0
- package/dist/{transformers-CJ3QA2PK.mjs → transformers-Df56Nq9G.mjs} +3 -1
- package/dist/transformers-Df56Nq9G.mjs.map +1 -0
- package/dist/uri-CehXVDGB.mjs +2 -0
- package/dist/uri-CehXVDGB.mjs.map +1 -0
- package/dist/util-DNyrmcA3.mjs +2 -0
- package/dist/util-DNyrmcA3.mjs.map +1 -0
- package/dist/{vfs-CNQbkhsf.mjs → vfs-QUP1rnSI.mjs} +2 -0
- package/dist/vfs-QUP1rnSI.mjs.map +1 -0
- package/package.json +25 -25
- package/src/db.ts +73 -23
- package/src/frecency.ts +29 -46
- package/src/store.ts +13 -7
- package/foo.ts +0 -3
- package/foo2.ts +0 -20
- package/test/doc.test.ts +0 -61
- package/test/fixtures/ignore-test/keep.md +0 -0
- package/test/fixtures/ignore-test/skip.log +0 -0
- package/test/fixtures/ignore-test/sub/keep.md +0 -0
- package/test/fixtures/store/agent/index.md +0 -9
- package/test/fixtures/store/agent/lessons.md +0 -21
- package/test/fixtures/store/agent/soul.md +0 -28
- package/test/fixtures/store/agent/tools.md +0 -25
- package/test/fixtures/store/concepts/frecency.md +0 -30
- package/test/fixtures/store/concepts/index.md +0 -9
- package/test/fixtures/store/concepts/memory-coherence.md +0 -33
- package/test/fixtures/store/concepts/rag.md +0 -27
- package/test/fixtures/store/index.md +0 -9
- package/test/fixtures/store/projects/index.md +0 -9
- package/test/fixtures/store/projects/rekall-inc/architecture.md +0 -41
- package/test/fixtures/store/projects/rekall-inc/decisions/index.md +0 -9
- package/test/fixtures/store/projects/rekall-inc/decisions/no-military.md +0 -20
- package/test/fixtures/store/projects/rekall-inc/index.md +0 -28
- package/test/fixtures/store/user/family.md +0 -13
- package/test/fixtures/store/user/index.md +0 -9
- package/test/fixtures/store/user/preferences.md +0 -29
- package/test/fixtures/store/user/profile.md +0 -29
- package/test/fs.test.ts +0 -15
- package/test/glob.test.ts +0 -190
- package/test/md.test.ts +0 -177
- package/test/query.test.ts +0 -105
- package/test/uri.test.ts +0 -46
- package/test/util.test.ts +0 -62
- package/test/vfs.test.ts +0 -164
- package/tsconfig.json +0 -3
- package/tsdown.config.ts +0 -8
package/test/glob.test.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url"
|
|
2
|
-
import { join } from "pathe"
|
|
3
|
-
import { describe, expect, test } from "vitest"
|
|
4
|
-
import { glob } from "../src/glob.ts"
|
|
5
|
-
|
|
6
|
-
const FIXTURES = join(fileURLToPath(import.meta.url), "..", "fixtures/store")
|
|
7
|
-
|
|
8
|
-
async function collect(opts: Partial<Parameters<typeof glob>[0]> = {}) {
|
|
9
|
-
const results: string[] = []
|
|
10
|
-
for await (const path of glob({ cwd: FIXTURES, ...opts })) {
|
|
11
|
-
results.push(path)
|
|
12
|
-
}
|
|
13
|
-
return results
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe("glob", () => {
|
|
17
|
-
test("yields files and directories", async () => {
|
|
18
|
-
const results = await collect()
|
|
19
|
-
expect(results.length).toBeGreaterThan(0)
|
|
20
|
-
expect(results.some((r) => r.endsWith(".md"))).toBe(true)
|
|
21
|
-
expect(results.some((r) => r.endsWith("/"))).toBe(true)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test("finds known fixture files", async () => {
|
|
25
|
-
const results = await collect()
|
|
26
|
-
expect(results).toContain("index.md")
|
|
27
|
-
expect(results).toContain("user/")
|
|
28
|
-
expect(results).toContain("user/family.md")
|
|
29
|
-
expect(results).toContain("user/index.md")
|
|
30
|
-
expect(results).toContain("projects/rekall-inc/decisions/no-military.md")
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test("results are sorted by name", async () => {
|
|
34
|
-
const results = await collect()
|
|
35
|
-
const topLevelFiles = results.filter((r) => !r.includes("/") && r.endsWith(".md"))
|
|
36
|
-
const sorted = [...topLevelFiles].toSorted()
|
|
37
|
-
expect(topLevelFiles).toEqual(sorted)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test("respects depth", async () => {
|
|
41
|
-
const results = await collect({ depth: 2 })
|
|
42
|
-
expect(results).toContain("index.md")
|
|
43
|
-
expect(results).toContain("user/")
|
|
44
|
-
expect(results.some((r) => r.startsWith("user/") && r.endsWith(".md"))).toBe(true)
|
|
45
|
-
expect(results.some((r) => r.includes("decisions/"))).toBe(false)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test("depth 1 only shows root contents", async () => {
|
|
49
|
-
const results = await collect({ depth: 1 })
|
|
50
|
-
expect(results).toContain("index.md")
|
|
51
|
-
expect(results).toContain("user/")
|
|
52
|
-
expect(results.every((r) => !r.includes("/") || r.endsWith("/"))).toBe(true)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test("skips hidden files and dirs by default", async () => {
|
|
56
|
-
const results = await collect()
|
|
57
|
-
expect(results.every((r) => !r.startsWith("."))).toBe(true)
|
|
58
|
-
expect(results.every((r) => !r.includes("/."))).toBe(true)
|
|
59
|
-
expect(results.some((r) => r.includes(".rekal"))).toBe(false)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test("includes hidden files when hidden is true", async () => {
|
|
63
|
-
const results = await collect({ depth: 1, hidden: true })
|
|
64
|
-
expect(results.length).toBeGreaterThan(0)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test("type=files excludes directories from output", async () => {
|
|
68
|
-
const results = await collect({ type: "file" })
|
|
69
|
-
expect(results.every((r) => !r.endsWith("/"))).toBe(true)
|
|
70
|
-
expect(results.some((r) => r.includes("/"))).toBe(true)
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe("glob patterns", () => {
|
|
75
|
-
test("filters to matching files only", async () => {
|
|
76
|
-
const results = await collect({ glob: "**/*.md" })
|
|
77
|
-
const files = results.filter((r) => !r.endsWith("/"))
|
|
78
|
-
expect(files.length).toBeGreaterThan(0)
|
|
79
|
-
expect(files.every((r) => r.endsWith(".md"))).toBe(true)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test("excludes non-matching files", async () => {
|
|
83
|
-
const results = await collect({ glob: "**/*.md" })
|
|
84
|
-
expect(results.some((r) => r.endsWith(".yaml"))).toBe(false)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test("still includes directories for traversal", async () => {
|
|
88
|
-
const results = await collect({ glob: "**/*.md" })
|
|
89
|
-
expect(results.some((r) => r.endsWith("/"))).toBe(true)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test("supports multiple glob patterns", async () => {
|
|
93
|
-
const results = await collect({ glob: ["**/family.md", "**/profile.md"] })
|
|
94
|
-
expect(results.some((r) => r.endsWith("family.md"))).toBe(true)
|
|
95
|
-
expect(results.some((r) => r.endsWith("profile.md"))).toBe(true)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test("narrow glob reduces results", async () => {
|
|
99
|
-
const allMd = await collect({ glob: "**/*.md" })
|
|
100
|
-
const userOnly = await collect({ glob: "user/*.md" })
|
|
101
|
-
expect(userOnly.length).toBeLessThan(allMd.length)
|
|
102
|
-
const files = userOnly.filter((r) => !r.endsWith("/"))
|
|
103
|
-
expect(files.every((r) => r.startsWith("user/"))).toBe(true)
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
describe("glob empty directories", () => {
|
|
108
|
-
test("excludes empty directories by default", async () => {
|
|
109
|
-
const results = await collect()
|
|
110
|
-
expect(results.some((r) => r === "empty-dir/")).toBe(false)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
test("includes empty directories when empty is true", async () => {
|
|
114
|
-
const results = await collect({ empty: true })
|
|
115
|
-
expect(results.some((r) => r === "empty-dir/")).toBe(true)
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
describe("glob onVisit", () => {
|
|
120
|
-
test("calls onVisit for entries", async () => {
|
|
121
|
-
const visited: string[] = []
|
|
122
|
-
await collect({ onVisit: (rel) => visited.push(rel) })
|
|
123
|
-
expect(visited.length).toBeGreaterThan(0)
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
describe("glob onError", () => {
|
|
128
|
-
test("calls onError for inaccessible directories", async () => {
|
|
129
|
-
const errors: { path: string; error: Error }[] = []
|
|
130
|
-
await collect({
|
|
131
|
-
cwd: "/nonexistent-path-that-does-not-exist",
|
|
132
|
-
onError: (path, error) => errors.push({ error, path }),
|
|
133
|
-
})
|
|
134
|
-
expect(errors.length).toBeGreaterThan(0)
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
describe("glob sort", () => {
|
|
139
|
-
test("type sort puts directories before files", async () => {
|
|
140
|
-
const results = await collect({ depth: 1, sort: "type" })
|
|
141
|
-
const firstFile = results.findIndex((r) => !r.endsWith("/"))
|
|
142
|
-
const lastDir = results.findLastIndex((r) => r.endsWith("/"))
|
|
143
|
-
if (firstFile !== -1 && lastDir !== -1) {
|
|
144
|
-
expect(lastDir).toBeLessThan(firstFile)
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test("name sort is alphabetical", async () => {
|
|
149
|
-
const results = await collect({ depth: 1, sort: "name" })
|
|
150
|
-
const sorted = [...results].toSorted()
|
|
151
|
-
expect(results).toEqual(sorted)
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
describe("glob ignore", () => {
|
|
156
|
-
test("respects exclude rules", async () => {
|
|
157
|
-
const results = await collect({ exclude: ["user/"] })
|
|
158
|
-
expect(results.some((r) => r.startsWith("user/"))).toBe(false)
|
|
159
|
-
expect(results.some((r) => r === "user/")).toBe(false)
|
|
160
|
-
expect(results.some((r) => r.startsWith("projects/"))).toBe(true)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
test("ignore=false skips all ignore file processing", async () => {
|
|
164
|
-
const withIgnore = await collect()
|
|
165
|
-
const withoutIgnore = await collect({ hidden: true, ignore: false })
|
|
166
|
-
expect(withoutIgnore.length).toBeGreaterThanOrEqual(withIgnore.length)
|
|
167
|
-
})
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
describe("glob nested ignore files", () => {
|
|
171
|
-
const IGNORE_FIXTURES = join(fileURLToPath(import.meta.url), "..", "fixtures/ignore-test")
|
|
172
|
-
|
|
173
|
-
async function collectIgnore(opts: Partial<Parameters<typeof glob>[0]> = {}) {
|
|
174
|
-
const results: string[] = []
|
|
175
|
-
for await (const path of glob({ cwd: IGNORE_FIXTURES, ...opts })) {
|
|
176
|
-
results.push(path)
|
|
177
|
-
}
|
|
178
|
-
return results
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
test("nested gitignore with / prefix only applies relative to its directory", async () => {
|
|
182
|
-
// sub/.gitignore contains "/skip.log"
|
|
183
|
-
// This should ignore sub/skip.log but NOT skip.log at the root
|
|
184
|
-
const results = await collectIgnore({ hidden: false })
|
|
185
|
-
expect(results).toContain("skip.log") // root skip.log should NOT be ignored
|
|
186
|
-
expect(results).not.toContain("sub/skip.log") // sub/skip.log SHOULD be ignored
|
|
187
|
-
expect(results).toContain("keep.md")
|
|
188
|
-
expect(results).toContain("sub/keep.md")
|
|
189
|
-
})
|
|
190
|
-
})
|
package/test/md.test.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest"
|
|
2
|
-
import { chunkMarkdown, parseSections } from "../src/md.ts"
|
|
3
|
-
|
|
4
|
-
describe("parse", () => {
|
|
5
|
-
test("parses sections by heading", () => {
|
|
6
|
-
const sections = parseSections(`# Title
|
|
7
|
-
|
|
8
|
-
Intro paragraph.
|
|
9
|
-
|
|
10
|
-
## Section One
|
|
11
|
-
|
|
12
|
-
Content one.
|
|
13
|
-
|
|
14
|
-
## Section Two
|
|
15
|
-
|
|
16
|
-
Content two.
|
|
17
|
-
`)
|
|
18
|
-
expect(sections).toHaveLength(3)
|
|
19
|
-
expect(sections[0].headingText).toBe("# Title")
|
|
20
|
-
expect(sections[0].level).toBe(1)
|
|
21
|
-
expect(sections[1].headingText).toBe("## Section One")
|
|
22
|
-
expect(sections[1].level).toBe(2)
|
|
23
|
-
expect(sections[2].headingText).toBe("## Section Two")
|
|
24
|
-
expect(sections[2].level).toBe(2)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test("handles content before first heading", () => {
|
|
28
|
-
const sections = parseSections(`Some preamble text.
|
|
29
|
-
|
|
30
|
-
# First Heading
|
|
31
|
-
|
|
32
|
-
Content.
|
|
33
|
-
`)
|
|
34
|
-
expect(sections).toHaveLength(2)
|
|
35
|
-
expect(sections[0].headingText).toBe("")
|
|
36
|
-
expect(sections[0].level).toBe(0)
|
|
37
|
-
expect(sections[0].content.join("\n")).toContain("preamble")
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test("ignores headings inside code blocks", () => {
|
|
41
|
-
const sections = parseSections(`# Real Heading
|
|
42
|
-
|
|
43
|
-
\`\`\`markdown
|
|
44
|
-
# Not a heading
|
|
45
|
-
## Also not a heading
|
|
46
|
-
\`\`\`
|
|
47
|
-
|
|
48
|
-
More content.
|
|
49
|
-
`)
|
|
50
|
-
expect(sections).toHaveLength(1)
|
|
51
|
-
expect(sections[0].headingText).toBe("# Real Heading")
|
|
52
|
-
expect(sections[0].content.join("\n")).toContain("# Not a heading")
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test("handles tilde code fences", () => {
|
|
56
|
-
const sections = parseSections(`# Title
|
|
57
|
-
|
|
58
|
-
~~~python
|
|
59
|
-
# comment not a heading
|
|
60
|
-
~~~
|
|
61
|
-
|
|
62
|
-
## Next Section
|
|
63
|
-
|
|
64
|
-
Content.
|
|
65
|
-
`)
|
|
66
|
-
expect(sections).toHaveLength(2)
|
|
67
|
-
expect(sections[0].headingText).toBe("# Title")
|
|
68
|
-
expect(sections[1].headingText).toBe("## Next Section")
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test("discards empty sections", () => {
|
|
72
|
-
// A section with a heading but no content lines (immediately followed by another heading)
|
|
73
|
-
const sections = parseSections(`# First
|
|
74
|
-
## Second
|
|
75
|
-
|
|
76
|
-
Content here.
|
|
77
|
-
`)
|
|
78
|
-
// "First" has "## Second" as its only content line before being replaced,
|
|
79
|
-
// but parse() keeps sections that have the heading line itself as content
|
|
80
|
-
expect(sections).toHaveLength(2)
|
|
81
|
-
expect(sections[0].headingText).toBe("# First")
|
|
82
|
-
expect(sections[1].headingText).toBe("## Second")
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test("handles nested heading levels", () => {
|
|
86
|
-
const sections = parseSections(`# H1
|
|
87
|
-
|
|
88
|
-
## H2
|
|
89
|
-
|
|
90
|
-
### H3
|
|
91
|
-
|
|
92
|
-
Content.
|
|
93
|
-
|
|
94
|
-
## Another H2
|
|
95
|
-
|
|
96
|
-
More content.
|
|
97
|
-
`)
|
|
98
|
-
expect(sections.map((s) => s.headingText)).toEqual(["# H1", "## H2", "### H3", "## Another H2"])
|
|
99
|
-
expect(sections.map((s) => s.level)).toEqual([1, 2, 3, 2])
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe("chunk", () => {
|
|
104
|
-
// Simple mock tokenizer that counts words as tokens
|
|
105
|
-
const mockTokenizer = {
|
|
106
|
-
toks: (text: string) => text.split(/\s+/).filter(Boolean).length,
|
|
107
|
-
} as Parameters<typeof chunkMarkdown>[1]
|
|
108
|
-
|
|
109
|
-
test("small document produces single chunk", () => {
|
|
110
|
-
const result = chunkMarkdown("# Title\n\nShort content.", mockTokenizer, 100)
|
|
111
|
-
expect(result).toHaveLength(1)
|
|
112
|
-
expect(result[0]).toContain("Title")
|
|
113
|
-
expect(result[0]).toContain("Short content.")
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test("splits at section boundaries", () => {
|
|
117
|
-
const md = `# Title
|
|
118
|
-
|
|
119
|
-
First section with enough words to take some space in the chunk budget.
|
|
120
|
-
|
|
121
|
-
## Second Section
|
|
122
|
-
|
|
123
|
-
Second section content that should go into a new chunk because of size limits.
|
|
124
|
-
`
|
|
125
|
-
const result = chunkMarkdown(md, mockTokenizer, 15)
|
|
126
|
-
expect(result.length).toBeGreaterThan(1)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
test("includes parent headings in child chunks", () => {
|
|
130
|
-
const md = `# Parent
|
|
131
|
-
|
|
132
|
-
## Child
|
|
133
|
-
|
|
134
|
-
Child content that is long enough to be its own chunk separate from the parent section.
|
|
135
|
-
`
|
|
136
|
-
const result = chunkMarkdown(md, mockTokenizer, 15)
|
|
137
|
-
// The child chunk should include "# Parent" for context
|
|
138
|
-
const childChunk = result.find((c) => c.includes("Child content"))
|
|
139
|
-
expect(childChunk).toContain("# Parent")
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
test("handles deeply nested headings", () => {
|
|
143
|
-
const md = `# Level 1
|
|
144
|
-
|
|
145
|
-
## Level 2
|
|
146
|
-
|
|
147
|
-
### Level 3
|
|
148
|
-
|
|
149
|
-
Deep content that needs all its parent headings for context.
|
|
150
|
-
`
|
|
151
|
-
const result = chunkMarkdown(md, mockTokenizer, 20)
|
|
152
|
-
const deepChunk = result.find((c) => c.includes("Deep content"))
|
|
153
|
-
expect(deepChunk).toBeDefined()
|
|
154
|
-
if (deepChunk) {
|
|
155
|
-
expect(deepChunk).toContain("# Level 1")
|
|
156
|
-
expect(deepChunk).toContain("## Level 2")
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
test("does not duplicate headings when packing siblings", () => {
|
|
161
|
-
const md = `# Parent
|
|
162
|
-
|
|
163
|
-
## Child A
|
|
164
|
-
|
|
165
|
-
Short A.
|
|
166
|
-
|
|
167
|
-
## Child B
|
|
168
|
-
|
|
169
|
-
Short B.
|
|
170
|
-
`
|
|
171
|
-
const result = chunkMarkdown(md, mockTokenizer, 100)
|
|
172
|
-
// Both children fit in one chunk, parent heading should appear only once
|
|
173
|
-
expect(result).toHaveLength(1)
|
|
174
|
-
const count = result[0].split("# Parent").length - 1
|
|
175
|
-
expect(count).toBe(1)
|
|
176
|
-
})
|
|
177
|
-
})
|
package/test/query.test.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// oxlint-disable sort-keys
|
|
2
|
-
import { describe, expect, test } from "vitest"
|
|
3
|
-
import { toFts, tokenize } from "../src/query.ts"
|
|
4
|
-
|
|
5
|
-
describe("tokenize", () => {
|
|
6
|
-
const cases: Record<string, ReturnType<typeof tokenize>> = {
|
|
7
|
-
"children family": [
|
|
8
|
-
{ type: "term", value: "children" },
|
|
9
|
-
{ type: "term", value: "family" },
|
|
10
|
-
],
|
|
11
|
-
'"my children"': [{ type: "term", value: "my children" }],
|
|
12
|
-
"'my children'": [{ type: "term", value: "my children" }],
|
|
13
|
-
"-sports": [{ type: "term", value: "sports", neg: true }],
|
|
14
|
-
"children | kids": [
|
|
15
|
-
{ type: "term", value: "children" },
|
|
16
|
-
{ type: "op", value: "OR" },
|
|
17
|
-
{ type: "term", value: "kids" },
|
|
18
|
-
],
|
|
19
|
-
"(children | kids) family": [
|
|
20
|
-
{ type: "paren", value: "(" },
|
|
21
|
-
{ type: "term", value: "children" },
|
|
22
|
-
{ type: "op", value: "OR" },
|
|
23
|
-
{ type: "term", value: "kids" },
|
|
24
|
-
{ type: "paren", value: ")" },
|
|
25
|
-
{ type: "term", value: "family" },
|
|
26
|
-
],
|
|
27
|
-
'"my children': [{ type: "term", value: "my children" }],
|
|
28
|
-
'""': [],
|
|
29
|
-
"what's up": [
|
|
30
|
-
{ type: "term", value: "what's" },
|
|
31
|
-
{ type: "term", value: "up" },
|
|
32
|
-
],
|
|
33
|
-
"child*": [{ type: "term", value: "child*" }],
|
|
34
|
-
"entities:Lars": [{ type: "term", value: "Lars", field: "entities" }],
|
|
35
|
-
"tags:family children": [
|
|
36
|
-
{ type: "term", value: "family", field: "tags" },
|
|
37
|
-
{ type: "term", value: "children" },
|
|
38
|
-
],
|
|
39
|
-
"-tags:sports": [{ type: "term", value: "sports", field: "tags", neg: true }],
|
|
40
|
-
"+children": [{ type: "term", value: "children", req: true }],
|
|
41
|
-
"+tags:family": [{ type: "term", value: "family", field: "tags", req: true }],
|
|
42
|
-
"unknown:foo": [{ type: "term", value: "unknown:foo" }],
|
|
43
|
-
'children -sports "my family" | (kids age)': [
|
|
44
|
-
{ type: "term", value: "children" },
|
|
45
|
-
{ type: "term", value: "sports", neg: true },
|
|
46
|
-
{ type: "term", value: "my family" },
|
|
47
|
-
{ type: "op", value: "OR" },
|
|
48
|
-
{ type: "paren", value: "(" },
|
|
49
|
-
{ type: "term", value: "kids" },
|
|
50
|
-
{ type: "term", value: "age" },
|
|
51
|
-
{ type: "paren", value: ")" },
|
|
52
|
-
],
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const [input, expected] of Object.entries(cases)) {
|
|
56
|
-
test(input, () => expect(tokenize(input)).toEqual(expected))
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
describe("toFts", () => {
|
|
61
|
-
const or: Record<string, string> = {
|
|
62
|
-
"children family": '"children" OR "family"',
|
|
63
|
-
"children | kids": '"children" OR "kids"',
|
|
64
|
-
"children -sports": '"children" OR NOT "sports"',
|
|
65
|
-
'"my children"': '"my children"',
|
|
66
|
-
"'my children'": '"my children"',
|
|
67
|
-
"(children | kids) family": '("children" OR "kids") OR "family"',
|
|
68
|
-
"what's children?": '"what\'s" OR "children"',
|
|
69
|
-
"": "",
|
|
70
|
-
kids: '"kids"',
|
|
71
|
-
"child*": '"child"*',
|
|
72
|
-
"child* family": '"child"* OR "family"',
|
|
73
|
-
"child* famil*": '"child"* OR "famil"*',
|
|
74
|
-
"-sport*": 'NOT "sport"*',
|
|
75
|
-
"entities:Lars": 'entities : "Lars"',
|
|
76
|
-
"tags:family children": 'tags : "family" OR "children"',
|
|
77
|
-
"-tags:sports": 'NOT tags : "sports"',
|
|
78
|
-
"entities:Lars tags:family": 'entities : "Lars" OR tags : "family"',
|
|
79
|
-
"entities:Lars* tags:fam*": 'entities : "Lars"* OR tags : "fam"*',
|
|
80
|
-
"unknown:foo": '"unknown:foo"',
|
|
81
|
-
"folke +children": '"children" AND ("folke" OR "children")',
|
|
82
|
-
"+children folke": '"children" AND ("children" OR "folke")',
|
|
83
|
-
"folke +children +family": '"children" AND "family" AND ("folke" OR "children" OR "family")',
|
|
84
|
-
"+tags:family children": 'tags : "family" AND (tags : "family" OR "children")',
|
|
85
|
-
"+children": '"children"',
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const and: Record<string, string> = {
|
|
89
|
-
"children family": '"children" AND "family"',
|
|
90
|
-
"children | kids": '"children" OR "kids"',
|
|
91
|
-
"children -sports": '"children" AND NOT "sports"',
|
|
92
|
-
"(children | kids) family": '("children" OR "kids") AND "family"',
|
|
93
|
-
"(children kids) family": '("children" AND "kids") AND "family"',
|
|
94
|
-
"child* family": '"child"* AND "family"',
|
|
95
|
-
"what's my name?": '"what\'s" AND "my" AND "name"',
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
for (const [input, expected] of Object.entries(or)) {
|
|
99
|
-
test(`OR: ${input}`, () => expect(toFts(input)).toBe(expected))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
for (const [input, expected] of Object.entries(and)) {
|
|
103
|
-
test(`AND: ${input}`, () => expect(toFts(input, "AND")).toBe(expected))
|
|
104
|
-
}
|
|
105
|
-
})
|
package/test/uri.test.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// oxlint-disable sort-keys
|
|
2
|
-
import { describe, expect, test } from "vitest"
|
|
3
|
-
import { normUri, parentUri } from "../src/uri.ts"
|
|
4
|
-
|
|
5
|
-
describe("normUri", () => {
|
|
6
|
-
const cases: { input?: string; dir?: boolean; expected: string }[] = [
|
|
7
|
-
{ expected: "rekal://" },
|
|
8
|
-
{ input: "", expected: "rekal://" },
|
|
9
|
-
{ input: " ", expected: "rekal://" },
|
|
10
|
-
{ input: "foo", expected: "rekal://foo" },
|
|
11
|
-
{ input: "foo/bar", expected: "rekal://foo/bar" },
|
|
12
|
-
{ input: "foo/bar/index.md", expected: "rekal://foo/bar/" },
|
|
13
|
-
{ input: "rekal://notes", expected: "rekal://notes" },
|
|
14
|
-
{ input: "rekal://notes/", expected: "rekal://notes/" },
|
|
15
|
-
{ input: "rekall://test", expected: "rekal://test" },
|
|
16
|
-
{ input: "a//b///c", expected: "rekal://a/b/c" },
|
|
17
|
-
{ input: "///a/b", expected: "rekal://a/b" },
|
|
18
|
-
{ input: "a\\b\\c", expected: "rekal://a/b/c" },
|
|
19
|
-
{ input: "foo", dir: true, expected: "rekal://foo/" },
|
|
20
|
-
{ input: "foo/bar", dir: true, expected: "rekal://foo/bar/" },
|
|
21
|
-
{ input: "foo/bar/", dir: true, expected: "rekal://foo/bar/" },
|
|
22
|
-
{ input: "foo", dir: false, expected: "rekal://foo" },
|
|
23
|
-
{ input: "foo/bar/", dir: false, expected: "rekal://foo/bar" },
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
for (const { input, dir, expected } of cases) {
|
|
27
|
-
test(`normUri(${JSON.stringify(input)}, ${dir}) => ${expected}`, () =>
|
|
28
|
-
expect(normUri(input, dir)).toBe(expected))
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
describe("parentUri", () => {
|
|
33
|
-
const cases: { input: string; expected: string | undefined }[] = [
|
|
34
|
-
{ input: "rekal://", expected: undefined },
|
|
35
|
-
{ input: "rekal://foo", expected: "rekal://" },
|
|
36
|
-
{ input: "rekal://foo/", expected: "rekal://" },
|
|
37
|
-
{ input: "rekal://foo\\bar", expected: "rekal://foo/" },
|
|
38
|
-
{ input: "rekal://foo/bar/", expected: "rekal://foo/" },
|
|
39
|
-
{ input: "rekal://foo/bar/baz.md", expected: "rekal://foo/bar/" },
|
|
40
|
-
{ input: "rekal://a/b/c/d", expected: "rekal://a/b/c/" },
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
for (const { input, expected } of cases) {
|
|
44
|
-
test(`parentUri(${input}) => ${expected}`, () => expect(parentUri(input)).toBe(expected))
|
|
45
|
-
}
|
|
46
|
-
})
|
package/test/util.test.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest"
|
|
2
|
-
import { parseFrontmatter } from "../src/md.ts"
|
|
3
|
-
|
|
4
|
-
describe("parseFrontmatter", () => {
|
|
5
|
-
test("parses yaml frontmatter", () => {
|
|
6
|
-
const { frontmatter, body } = parseFrontmatter(`---
|
|
7
|
-
summary: "hello world"
|
|
8
|
-
tags: [a, b]
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Title
|
|
12
|
-
|
|
13
|
-
Body text.
|
|
14
|
-
`)
|
|
15
|
-
expect(frontmatter).toEqual({ summary: "hello world", tags: ["a", "b"] })
|
|
16
|
-
expect(body).toContain("# Title")
|
|
17
|
-
expect(body).toContain("Body text.")
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test("returns empty data when no frontmatter", () => {
|
|
21
|
-
const { frontmatter, body } = parseFrontmatter("Just plain text.")
|
|
22
|
-
expect(frontmatter).toEqual({})
|
|
23
|
-
expect(body).toBe("Just plain text.")
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test("handles missing closing delimiter gracefully", () => {
|
|
27
|
-
const { frontmatter, body } = parseFrontmatter(`---
|
|
28
|
-
summary: "oops"
|
|
29
|
-
No closing delimiter here.
|
|
30
|
-
`)
|
|
31
|
-
// Should not crash — returns empty frontmatter, full content as body
|
|
32
|
-
expect(frontmatter).toEqual({})
|
|
33
|
-
expect(body).toContain("summary")
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test("handles empty frontmatter", () => {
|
|
37
|
-
const { frontmatter, body } = parseFrontmatter(`---
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
Content after empty frontmatter.
|
|
41
|
-
`)
|
|
42
|
-
expect(frontmatter).toEqual({})
|
|
43
|
-
expect(body).toContain("Content after empty frontmatter.")
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("handles complex yaml values", () => {
|
|
47
|
-
const { frontmatter } = parseFrontmatter(`---
|
|
48
|
-
entities: [Folke, "Lars Lemaitre", snacks.nvim]
|
|
49
|
-
tags:
|
|
50
|
-
- user
|
|
51
|
-
- family
|
|
52
|
-
nested:
|
|
53
|
-
key: value
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
Body.
|
|
57
|
-
`)
|
|
58
|
-
expect(frontmatter.entities).toEqual(["Folke", "Lars Lemaitre", "snacks.nvim"])
|
|
59
|
-
expect(frontmatter.tags).toEqual(["user", "family"])
|
|
60
|
-
expect(frontmatter.nested).toEqual({ key: "value" })
|
|
61
|
-
})
|
|
62
|
-
})
|