contentbase 0.0.4 → 0.0.5
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/package.json +1 -1
- package/.mcp.json +0 -9
- package/MCP-DESCRIPTIONS-REVIEW.md +0 -122
- package/bun.lock +0 -512
- package/dist/cnotes +0 -0
- package/examples/sdlc-queries.ts +0 -161
- package/public/web-demo/index.html +0 -813
- package/scripts/examples/01-collection-setup.ts +0 -46
- package/scripts/examples/02-querying.ts +0 -67
- package/scripts/examples/03-sections.ts +0 -36
- package/scripts/examples/04-relationships.ts +0 -54
- package/scripts/examples/05-document-api.ts +0 -54
- package/scripts/examples/06-extract-sections.ts +0 -55
- package/scripts/examples/07-validation.ts +0 -46
- package/scripts/examples/08-serialization.ts +0 -51
- package/scripts/examples/lib/format.ts +0 -87
- package/scripts/examples/lib/setup.ts +0 -21
- package/scripts/examples/run-all.ts +0 -43
- package/showcases/national-parks/models.ts +0 -74
- package/showcases/national-parks/parks/acadia.mdx +0 -40
- package/showcases/national-parks/parks/yosemite.mdx +0 -44
- package/showcases/national-parks/parks/zion.mdx +0 -44
- package/showcases/national-parks/queries.ts +0 -103
- package/showcases/national-parks/trails/angels-landing.mdx +0 -19
- package/showcases/national-parks/trails/cathedral-lakes.mdx +0 -19
- package/showcases/national-parks/trails/half-dome.mdx +0 -19
- package/showcases/national-parks/trails/jordan-pond-path.mdx +0 -19
- package/showcases/national-parks/trails/mist-trail.mdx +0 -19
- package/showcases/national-parks/trails/observation-point.mdx +0 -19
- package/showcases/national-parks/trails/precipice-trail.mdx +0 -19
- package/showcases/national-parks/trails/the-narrows.mdx +0 -19
- package/showcases/node_modules/.cache/.repl_history +0 -3
- package/showcases/recipes/cuisines/chinese.mdx +0 -28
- package/showcases/recipes/cuisines/italian.mdx +0 -32
- package/showcases/recipes/cuisines/mexican.mdx +0 -28
- package/showcases/recipes/models.ts +0 -77
- package/showcases/recipes/queries.ts +0 -89
- package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +0 -43
- package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +0 -47
- package/showcases/recipes/recipes/italian/bruschetta.mdx +0 -38
- package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +0 -39
- package/showcases/recipes/recipes/italian/tiramisu.mdx +0 -43
- package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +0 -44
- package/showcases/recipes/recipes/mexican/guacamole.mdx +0 -39
- package/showcases/vinyl-collection/albums/bitches-brew.mdx +0 -36
- package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +0 -35
- package/showcases/vinyl-collection/albums/in-rainbows.mdx +0 -35
- package/showcases/vinyl-collection/albums/kind-of-blue.mdx +0 -32
- package/showcases/vinyl-collection/albums/ok-computer.mdx +0 -37
- package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +0 -35
- package/showcases/vinyl-collection/artists/miles-davis.mdx +0 -27
- package/showcases/vinyl-collection/artists/nina-simone.mdx +0 -26
- package/showcases/vinyl-collection/artists/radiohead.mdx +0 -27
- package/showcases/vinyl-collection/models.ts +0 -73
- package/showcases/vinyl-collection/queries.ts +0 -87
- package/src/__tests__/semantic-search.integration.test.ts +0 -284
- package/test/ast-query.test.ts +0 -128
- package/test/collection.test.ts +0 -215
- package/test/define-model.test.ts +0 -78
- package/test/document.test.ts +0 -225
- package/test/extract-sections.test.ts +0 -356
- package/test/fixtures/sdlc/MODELS.md +0 -106
- package/test/fixtures/sdlc/SKILL.md +0 -404
- package/test/fixtures/sdlc/epics/authentication.mdx +0 -42
- package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +0 -21
- package/test/fixtures/sdlc/index.ts +0 -9
- package/test/fixtures/sdlc/models.ts +0 -91
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +0 -20
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +0 -20
- package/test/fixtures/sdlc/templates/epic.md +0 -23
- package/test/fixtures/sdlc/templates/story.md +0 -19
- package/test/helpers.ts +0 -21
- package/test/model-instance.test.ts +0 -197
- package/test/pattern.test.ts +0 -191
- package/test/query-dsl.test.ts +0 -431
- package/test/query.test.ts +0 -196
- package/test/relationships.test.ts +0 -214
- package/test/section.test.ts +0 -160
- package/test/table-of-contents.test.ts +0 -135
- package/test/validator.test.ts +0 -62
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -11
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example queries for the Vinyl Collection showcase.
|
|
3
|
-
*
|
|
4
|
-
* Run with: bun showcases/vinyl-collection/queries.ts
|
|
5
|
-
*/
|
|
6
|
-
import { Collection } from "../../src/index";
|
|
7
|
-
import { Artist, Album } from "./models";
|
|
8
|
-
|
|
9
|
-
const collection = new Collection({
|
|
10
|
-
rootPath: new URL(".", import.meta.url).pathname,
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
collection.register(Artist);
|
|
14
|
-
collection.register(Album);
|
|
15
|
-
await collection.load();
|
|
16
|
-
|
|
17
|
-
// ── Full collection ──
|
|
18
|
-
const allAlbums = await collection.query(Album).fetchAll();
|
|
19
|
-
console.log(`Total albums in collection: ${allAlbums.length}`);
|
|
20
|
-
|
|
21
|
-
// ── Filter by genre ──
|
|
22
|
-
const jazzAlbums = await collection
|
|
23
|
-
.query(Album)
|
|
24
|
-
.where("meta.genre", "contains", "Jazz")
|
|
25
|
-
.fetchAll();
|
|
26
|
-
console.log(
|
|
27
|
-
`\nJazz albums: ${jazzAlbums.map((a) => `${a.title} (${a.meta.year})`).join(", ")}`
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// ── Filter by decade (computed) ──
|
|
31
|
-
console.log(`\nAlbums by decade:`);
|
|
32
|
-
for (const album of allAlbums) {
|
|
33
|
-
console.log(` ${album.title} — ${album.computed.decade}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── 5-star records ──
|
|
37
|
-
const topRated = await collection
|
|
38
|
-
.query(Album)
|
|
39
|
-
.where("meta.rating", 5)
|
|
40
|
-
.fetchAll();
|
|
41
|
-
console.log(
|
|
42
|
-
`\n5-star albums: ${topRated.map((a) => a.title).join(", ")}`
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
// ── Classics (pre-1980) ──
|
|
46
|
-
console.log(`\nClassics (pre-1980):`);
|
|
47
|
-
for (const album of allAlbums) {
|
|
48
|
-
if (album.computed.isClassic) {
|
|
49
|
-
console.log(` ${album.title} (${album.meta.year})`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── Condition report ──
|
|
54
|
-
const mint = await collection
|
|
55
|
-
.query(Album)
|
|
56
|
-
.whereIn("meta.condition", ["mint", "near-mint"])
|
|
57
|
-
.fetchAll();
|
|
58
|
-
console.log(
|
|
59
|
-
`\nMint / near-mint condition: ${mint.map((a) => a.title).join(", ")}`
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// ── Tracklist as structured data ──
|
|
63
|
-
const kob = collection.getModel("albums/kind-of-blue", Album);
|
|
64
|
-
console.log(`\n--- ${kob.title} ---`);
|
|
65
|
-
console.log("Tracklist:", kob.sections.tracklist);
|
|
66
|
-
console.log("Personnel:", kob.sections.personnel);
|
|
67
|
-
|
|
68
|
-
// ── Artist → Albums relationship ──
|
|
69
|
-
const miles = collection.getModel("artists/miles-davis", Artist);
|
|
70
|
-
console.log(`\n--- ${miles.title} ---`);
|
|
71
|
-
console.log("Influences:", miles.sections.influences);
|
|
72
|
-
const milesAlbums = miles.relationships.albums.fetchAll();
|
|
73
|
-
console.log(
|
|
74
|
-
`Albums in discography: ${milesAlbums.map((a) => a.title).join(", ")}`
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// ── Album → Artist relationship ──
|
|
78
|
-
const okc = collection.getModel("albums/ok-computer", Album);
|
|
79
|
-
const artist = okc.relationships.artist.fetch();
|
|
80
|
-
console.log(`\n${okc.title} by ${artist.title} (${artist.meta.origin})`);
|
|
81
|
-
|
|
82
|
-
// ── Serialize ──
|
|
83
|
-
const json = kob.toJSON({
|
|
84
|
-
sections: ["tracklist", "personnel"],
|
|
85
|
-
computed: ["isClassic", "decade"],
|
|
86
|
-
});
|
|
87
|
-
console.log(`\nKind of Blue as JSON:`, JSON.stringify(json, null, 2));
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Semantic Search Integration Tests
|
|
3
|
-
*
|
|
4
|
-
* Run with: bun test src/__tests__/semantic-search.integration.test.ts
|
|
5
|
-
*
|
|
6
|
-
* Requires:
|
|
7
|
-
* - bun runtime (uses bun:sqlite)
|
|
8
|
-
* - OPENAI_API_KEY env var for embedding tests
|
|
9
|
-
*
|
|
10
|
-
* Tests the full pipeline: collection → index → search → results
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
13
|
-
import path from "path";
|
|
14
|
-
import fs from "fs/promises";
|
|
15
|
-
import { existsSync } from "fs";
|
|
16
|
-
import { Collection } from "../../src/collection";
|
|
17
|
-
|
|
18
|
-
const dir = import.meta.dirname ?? new URL(".", import.meta.url).pathname;
|
|
19
|
-
const FIXTURES_PATH = path.resolve(dir, "../../test/fixtures/sdlc");
|
|
20
|
-
const DB_DIR = path.join(FIXTURES_PATH, ".contentbase-test");
|
|
21
|
-
const DB_PATH = path.join(DB_DIR, "search.sqlite");
|
|
22
|
-
|
|
23
|
-
const HAS_API_KEY = !!process.env.OPENAI_API_KEY;
|
|
24
|
-
|
|
25
|
-
function collectDocumentInputs(collection: Collection) {
|
|
26
|
-
const inputs: any[] = [];
|
|
27
|
-
for (const pathId of collection.available) {
|
|
28
|
-
const doc = collection.document(pathId);
|
|
29
|
-
const modelDef = (collection as any).findModelDefinition?.(pathId);
|
|
30
|
-
|
|
31
|
-
const sections: any[] = [];
|
|
32
|
-
const lines = doc.content.split("\n");
|
|
33
|
-
let currentHeading: string | null = null;
|
|
34
|
-
let currentContent: string[] = [];
|
|
35
|
-
|
|
36
|
-
for (const line of lines) {
|
|
37
|
-
const h2Match = line.match(/^## (.+)/);
|
|
38
|
-
if (h2Match) {
|
|
39
|
-
if (currentHeading) {
|
|
40
|
-
sections.push({
|
|
41
|
-
heading: currentHeading,
|
|
42
|
-
headingPath: currentHeading,
|
|
43
|
-
content: currentContent.join("\n").trim(),
|
|
44
|
-
level: 2,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
currentHeading = h2Match[1].trim();
|
|
48
|
-
currentContent = [];
|
|
49
|
-
} else if (currentHeading) {
|
|
50
|
-
currentContent.push(line);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (currentHeading) {
|
|
54
|
-
sections.push({
|
|
55
|
-
heading: currentHeading,
|
|
56
|
-
headingPath: currentHeading,
|
|
57
|
-
content: currentContent.join("\n").trim(),
|
|
58
|
-
level: 2,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
inputs.push({
|
|
63
|
-
pathId,
|
|
64
|
-
model: modelDef?.name ?? undefined,
|
|
65
|
-
title: doc.title,
|
|
66
|
-
slug: (doc as any).slug,
|
|
67
|
-
meta: doc.meta,
|
|
68
|
-
content: doc.content,
|
|
69
|
-
sections: sections.length > 0 ? sections : undefined,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return inputs;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
describe("Semantic Search Integration", () => {
|
|
76
|
-
let collection: Collection;
|
|
77
|
-
let SemanticSearchClass: any;
|
|
78
|
-
let ss: any;
|
|
79
|
-
|
|
80
|
-
beforeAll(async () => {
|
|
81
|
-
// Import models
|
|
82
|
-
const { Epic, Story } = await import("../../test/fixtures/sdlc/models");
|
|
83
|
-
|
|
84
|
-
// Load collection
|
|
85
|
-
collection = new Collection({ rootPath: FIXTURES_PATH, name: "test-sdlc" });
|
|
86
|
-
collection.register(Epic);
|
|
87
|
-
collection.register(Story);
|
|
88
|
-
await collection.load();
|
|
89
|
-
|
|
90
|
-
// Import SemanticSearch
|
|
91
|
-
const mod = await import("@soederpop/luca/agi");
|
|
92
|
-
SemanticSearchClass = mod.SemanticSearch;
|
|
93
|
-
|
|
94
|
-
// Clean up any previous test index
|
|
95
|
-
if (existsSync(DB_DIR)) {
|
|
96
|
-
await fs.rm(DB_DIR, { recursive: true });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
afterAll(async () => {
|
|
101
|
-
if (ss) {
|
|
102
|
-
try { await ss.close(); } catch {}
|
|
103
|
-
}
|
|
104
|
-
if (existsSync(DB_DIR)) {
|
|
105
|
-
await fs.rm(DB_DIR, { recursive: true });
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("Database Layer", () => {
|
|
110
|
-
it("creates and initializes database tables", async () => {
|
|
111
|
-
// Use the class directly - create an instance through the static attach pattern
|
|
112
|
-
// We need a minimal container mock for the Feature constructor
|
|
113
|
-
const { Database } = await import("bun:sqlite");
|
|
114
|
-
|
|
115
|
-
// Clean first
|
|
116
|
-
if (existsSync(DB_DIR)) await fs.rm(DB_DIR, { recursive: true });
|
|
117
|
-
|
|
118
|
-
// Test the Database creation directly using bun:sqlite
|
|
119
|
-
const { mkdirSync } = await import("fs");
|
|
120
|
-
mkdirSync(DB_DIR, { recursive: true });
|
|
121
|
-
const dbPath = path.join(DB_DIR, "search.openai-text-embedding-3-small.sqlite");
|
|
122
|
-
const db = new Database(dbPath);
|
|
123
|
-
|
|
124
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
125
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
126
|
-
|
|
127
|
-
// Create the same tables as SemanticSearch
|
|
128
|
-
db.exec(`CREATE TABLE search_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);
|
|
129
|
-
db.exec(`CREATE TABLE documents (
|
|
130
|
-
path_id TEXT PRIMARY KEY, model TEXT, title TEXT, slug TEXT,
|
|
131
|
-
meta_json TEXT, content TEXT, sections_json TEXT,
|
|
132
|
-
content_hash TEXT, indexed_at TEXT
|
|
133
|
-
)`);
|
|
134
|
-
db.exec(`CREATE VIRTUAL TABLE documents_fts USING fts5(
|
|
135
|
-
path_id, title, content, sections_text, tokenize='porter unicode61'
|
|
136
|
-
)`);
|
|
137
|
-
db.exec(`CREATE TABLE chunks (
|
|
138
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
139
|
-
path_id TEXT NOT NULL, section TEXT, heading_path TEXT,
|
|
140
|
-
seq INTEGER NOT NULL, content TEXT NOT NULL,
|
|
141
|
-
content_hash TEXT NOT NULL, embedding BLOB,
|
|
142
|
-
FOREIGN KEY (path_id) REFERENCES documents(path_id) ON DELETE CASCADE
|
|
143
|
-
)`);
|
|
144
|
-
|
|
145
|
-
// Verify tables exist
|
|
146
|
-
const tables = db.query("SELECT name FROM sqlite_master WHERE type='table'").all() as any[];
|
|
147
|
-
const tableNames = tables.map((t: any) => t.name);
|
|
148
|
-
expect(tableNames).toContain("search_meta");
|
|
149
|
-
expect(tableNames).toContain("documents");
|
|
150
|
-
expect(tableNames).toContain("chunks");
|
|
151
|
-
|
|
152
|
-
db.close();
|
|
153
|
-
// Clean up for the real test
|
|
154
|
-
await fs.rm(DB_DIR, { recursive: true });
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe("Document Chunking", () => {
|
|
159
|
-
it("collectDocumentInputs extracts sections from fixture documents", () => {
|
|
160
|
-
const docs = collectDocumentInputs(collection);
|
|
161
|
-
expect(docs.length).toBeGreaterThan(0);
|
|
162
|
-
|
|
163
|
-
const authEpic = docs.find((d: any) => d.pathId === "epics/authentication");
|
|
164
|
-
expect(authEpic).toBeDefined();
|
|
165
|
-
expect(authEpic.title).toBe("Authentication");
|
|
166
|
-
expect(authEpic.model).toBe("Epic");
|
|
167
|
-
expect(authEpic.sections).toBeDefined();
|
|
168
|
-
expect(authEpic.sections.length).toBeGreaterThan(0);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("documents without h2 sections get no sections array", () => {
|
|
172
|
-
const docs = collectDocumentInputs(collection);
|
|
173
|
-
// Stories may or may not have h2 sections depending on fixture
|
|
174
|
-
for (const doc of docs) {
|
|
175
|
-
if (doc.sections) {
|
|
176
|
-
expect(doc.sections.length).toBeGreaterThan(0);
|
|
177
|
-
for (const section of doc.sections) {
|
|
178
|
-
expect(section).toHaveProperty("heading");
|
|
179
|
-
expect(section).toHaveProperty("content");
|
|
180
|
-
expect(section).toHaveProperty("level");
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe("Full Pipeline (requires OPENAI_API_KEY)", () => {
|
|
188
|
-
it.skipIf(!HAS_API_KEY)("indexes documents and generates embeddings", async () => {
|
|
189
|
-
// Create a container-like environment for SemanticSearch
|
|
190
|
-
const container = (await import("@soederpop/luca")).default;
|
|
191
|
-
|
|
192
|
-
if (!container.features.available.includes("semanticSearch")) {
|
|
193
|
-
SemanticSearchClass.attach(container);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
ss = container.feature("semanticSearch", {
|
|
197
|
-
dbPath: DB_PATH,
|
|
198
|
-
embeddingProvider: "openai",
|
|
199
|
-
embeddingModel: "text-embedding-3-small",
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
await ss.initDb();
|
|
203
|
-
expect(ss.state.get("dbReady")).toBe(true);
|
|
204
|
-
|
|
205
|
-
const docs = collectDocumentInputs(collection);
|
|
206
|
-
await ss.indexDocuments(docs);
|
|
207
|
-
|
|
208
|
-
const stats = ss.getStats();
|
|
209
|
-
expect(stats.documentCount).toBe(docs.length);
|
|
210
|
-
expect(stats.chunkCount).toBeGreaterThan(0);
|
|
211
|
-
expect(stats.embeddingCount).toBeGreaterThan(0);
|
|
212
|
-
expect(stats.provider).toBe("openai");
|
|
213
|
-
expect(stats.model).toBe("text-embedding-3-small");
|
|
214
|
-
expect(stats.dimensions).toBe(1536);
|
|
215
|
-
}, 60000);
|
|
216
|
-
|
|
217
|
-
it.skipIf(!HAS_API_KEY)("keyword search returns BM25-ranked results", async () => {
|
|
218
|
-
const results = await ss.search("authentication");
|
|
219
|
-
expect(results.length).toBeGreaterThan(0);
|
|
220
|
-
expect(results[0].score).toBeGreaterThan(0);
|
|
221
|
-
expect(results[0].snippet).toBeTruthy();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it.skipIf(!HAS_API_KEY)("vector search finds semantically related docs", async () => {
|
|
225
|
-
const results = await ss.vectorSearch("user login and registration");
|
|
226
|
-
expect(results.length).toBeGreaterThan(0);
|
|
227
|
-
const authResult = results.find((r: any) => r.pathId.includes("authentication"));
|
|
228
|
-
expect(authResult).toBeDefined();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it.skipIf(!HAS_API_KEY)("hybrid search combines both modes", async () => {
|
|
232
|
-
const results = await ss.hybridSearch("authentication login");
|
|
233
|
-
expect(results.length).toBeGreaterThan(0);
|
|
234
|
-
expect(results[0].score).toBeGreaterThan(0);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it.skipIf(!HAS_API_KEY)("model filter restricts results", async () => {
|
|
238
|
-
const epicResults = await ss.search("authentication", { model: "Epic" });
|
|
239
|
-
for (const r of epicResults) {
|
|
240
|
-
expect(r.model).toBe("Epic");
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it.skipIf(!HAS_API_KEY)("search results include citation fields", async () => {
|
|
245
|
-
const results = await ss.hybridSearch("authentication");
|
|
246
|
-
expect(results.length).toBeGreaterThan(0);
|
|
247
|
-
const r = results[0];
|
|
248
|
-
expect(r).toHaveProperty("pathId");
|
|
249
|
-
expect(r).toHaveProperty("model");
|
|
250
|
-
expect(r).toHaveProperty("title");
|
|
251
|
-
expect(r).toHaveProperty("score");
|
|
252
|
-
expect(r).toHaveProperty("snippet");
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it.skipIf(!HAS_API_KEY)("needsReindex returns false for unchanged docs", async () => {
|
|
256
|
-
const docs = collectDocumentInputs(collection);
|
|
257
|
-
for (const doc of docs) {
|
|
258
|
-
expect(ss.needsReindex(doc)).toBe(false);
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it.skipIf(!HAS_API_KEY)("search with no index throws actionable error", async () => {
|
|
263
|
-
// Create a fresh instance with different dbPath
|
|
264
|
-
const container = (await import("@soederpop/luca")).default;
|
|
265
|
-
const freshSs = container.feature("semanticSearch", {
|
|
266
|
-
dbPath: path.join(DB_DIR, "nonexistent.sqlite"),
|
|
267
|
-
embeddingProvider: "openai",
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// Searching without initDb should throw
|
|
271
|
-
expect(() => freshSs.search("test")).toThrow();
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it.skipIf(!HAS_API_KEY)("getStats returns correct index status", async () => {
|
|
275
|
-
const stats = ss.getStats();
|
|
276
|
-
expect(stats.documentCount).toBeGreaterThan(0);
|
|
277
|
-
expect(stats.chunkCount).toBeGreaterThan(0);
|
|
278
|
-
expect(stats.embeddingCount).toBe(stats.chunkCount);
|
|
279
|
-
expect(stats.lastIndexedAt).toBeTruthy();
|
|
280
|
-
expect(stats.dimensions).toBe(1536);
|
|
281
|
-
expect(stats.dbSizeBytes).toBeGreaterThan(0);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
});
|
package/test/ast-query.test.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { AstQuery } from "../src/ast-query";
|
|
3
|
-
import { Collection } from "../src/collection";
|
|
4
|
-
import { createTestCollection } from "./helpers";
|
|
5
|
-
import type { Heading } from "mdast";
|
|
6
|
-
|
|
7
|
-
describe("AstQuery", () => {
|
|
8
|
-
let collection: Collection;
|
|
9
|
-
let query: AstQuery;
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
collection = await createTestCollection();
|
|
13
|
-
const doc = collection.document("epics/authentication");
|
|
14
|
-
query = doc.astQuery;
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("select returns first matching node", () => {
|
|
18
|
-
const heading = query.select("heading");
|
|
19
|
-
expect(heading).toBeDefined();
|
|
20
|
-
expect(heading!.type).toBe("heading");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("selectAll returns all matching nodes", () => {
|
|
24
|
-
const headings = query.selectAll("heading");
|
|
25
|
-
expect(headings.length).toBeGreaterThan(1);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("selectAll with attribute filter", () => {
|
|
29
|
-
const h2s = query.selectAll('heading[depth="2"]');
|
|
30
|
-
expect(h2s.length).toBeGreaterThanOrEqual(1);
|
|
31
|
-
for (const h of h2s) {
|
|
32
|
-
expect((h as Heading).depth).toBe(2);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("visit walks all nodes", () => {
|
|
37
|
-
let count = 0;
|
|
38
|
-
query.visit(() => {
|
|
39
|
-
count++;
|
|
40
|
-
});
|
|
41
|
-
expect(count).toBeGreaterThan(0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("findBefore finds preceding node", () => {
|
|
45
|
-
const headings = query.selectAll("heading");
|
|
46
|
-
if (headings.length >= 2) {
|
|
47
|
-
const before = query.findBefore(headings[1]);
|
|
48
|
-
expect(before).toBeDefined();
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("findAfter finds following node", () => {
|
|
53
|
-
const heading = query.select("heading")!;
|
|
54
|
-
const after = query.findAfter(heading);
|
|
55
|
-
expect(after).toBeDefined();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("findAllAfter returns all following nodes", () => {
|
|
59
|
-
const heading = query.select("heading")!;
|
|
60
|
-
const after = query.findAllAfter(heading);
|
|
61
|
-
expect(after.length).toBeGreaterThan(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("findBetween returns nodes between two markers", () => {
|
|
65
|
-
const headings = query.selectAll("heading");
|
|
66
|
-
if (headings.length >= 2) {
|
|
67
|
-
const between = query.findBetween(headings[0], headings[1]);
|
|
68
|
-
expect(between).toBeDefined();
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("headingsAtDepth returns correct headings (bug fix)", () => {
|
|
73
|
-
const h1s = query.headingsAtDepth(1);
|
|
74
|
-
expect(h1s.length).toBe(1);
|
|
75
|
-
expect(h1s[0].depth).toBe(1);
|
|
76
|
-
|
|
77
|
-
const h3s = query.headingsAtDepth(3);
|
|
78
|
-
expect(h3s.length).toBeGreaterThanOrEqual(2);
|
|
79
|
-
for (const h of h3s) {
|
|
80
|
-
expect(h.depth).toBe(3);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("findHeadingByText finds exact match (case insensitive)", () => {
|
|
85
|
-
const heading = query.findHeadingByText("stories");
|
|
86
|
-
expect(heading).toBeDefined();
|
|
87
|
-
expect(heading!.depth).toBe(2);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("findHeadingByText with substring matching", () => {
|
|
91
|
-
const heading = query.findHeadingByText("register", false);
|
|
92
|
-
expect(heading).toBeDefined();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("findAllHeadingsByText returns multiple matches", () => {
|
|
96
|
-
const headings = query.findAllHeadingsByText("Acceptance Criteria");
|
|
97
|
-
expect(headings.length).toBeGreaterThanOrEqual(2);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("findNextSiblingHeadingTo finds same-depth sibling", () => {
|
|
101
|
-
const h3s = query.headingsAtDepth(3);
|
|
102
|
-
if (h3s.length >= 2) {
|
|
103
|
-
const next = query.findNextSiblingHeadingTo(h3s[0]);
|
|
104
|
-
expect(next).toBeDefined();
|
|
105
|
-
expect(next!.depth).toBe(3);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("findNextSiblingHeadingTo returns undefined at end", () => {
|
|
110
|
-
const h3s = query.headingsAtDepth(3);
|
|
111
|
-
const last = h3s[h3s.length - 1];
|
|
112
|
-
const next = query.findNextSiblingHeadingTo(last);
|
|
113
|
-
expect(next).toBeUndefined();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("atLine returns node at specific line", () => {
|
|
117
|
-
const first = query.ast.children[0];
|
|
118
|
-
if (first.position) {
|
|
119
|
-
const found = query.atLine(first.position.start.line);
|
|
120
|
-
expect(found).toBeDefined();
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("atLine returns undefined for non-existent line", () => {
|
|
125
|
-
const found = query.atLine(99999);
|
|
126
|
-
expect(found).toBeUndefined();
|
|
127
|
-
});
|
|
128
|
-
});
|