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.
Files changed (82) hide show
  1. package/package.json +1 -1
  2. package/.mcp.json +0 -9
  3. package/MCP-DESCRIPTIONS-REVIEW.md +0 -122
  4. package/bun.lock +0 -512
  5. package/dist/cnotes +0 -0
  6. package/examples/sdlc-queries.ts +0 -161
  7. package/public/web-demo/index.html +0 -813
  8. package/scripts/examples/01-collection-setup.ts +0 -46
  9. package/scripts/examples/02-querying.ts +0 -67
  10. package/scripts/examples/03-sections.ts +0 -36
  11. package/scripts/examples/04-relationships.ts +0 -54
  12. package/scripts/examples/05-document-api.ts +0 -54
  13. package/scripts/examples/06-extract-sections.ts +0 -55
  14. package/scripts/examples/07-validation.ts +0 -46
  15. package/scripts/examples/08-serialization.ts +0 -51
  16. package/scripts/examples/lib/format.ts +0 -87
  17. package/scripts/examples/lib/setup.ts +0 -21
  18. package/scripts/examples/run-all.ts +0 -43
  19. package/showcases/national-parks/models.ts +0 -74
  20. package/showcases/national-parks/parks/acadia.mdx +0 -40
  21. package/showcases/national-parks/parks/yosemite.mdx +0 -44
  22. package/showcases/national-parks/parks/zion.mdx +0 -44
  23. package/showcases/national-parks/queries.ts +0 -103
  24. package/showcases/national-parks/trails/angels-landing.mdx +0 -19
  25. package/showcases/national-parks/trails/cathedral-lakes.mdx +0 -19
  26. package/showcases/national-parks/trails/half-dome.mdx +0 -19
  27. package/showcases/national-parks/trails/jordan-pond-path.mdx +0 -19
  28. package/showcases/national-parks/trails/mist-trail.mdx +0 -19
  29. package/showcases/national-parks/trails/observation-point.mdx +0 -19
  30. package/showcases/national-parks/trails/precipice-trail.mdx +0 -19
  31. package/showcases/national-parks/trails/the-narrows.mdx +0 -19
  32. package/showcases/node_modules/.cache/.repl_history +0 -3
  33. package/showcases/recipes/cuisines/chinese.mdx +0 -28
  34. package/showcases/recipes/cuisines/italian.mdx +0 -32
  35. package/showcases/recipes/cuisines/mexican.mdx +0 -28
  36. package/showcases/recipes/models.ts +0 -77
  37. package/showcases/recipes/queries.ts +0 -89
  38. package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +0 -43
  39. package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +0 -47
  40. package/showcases/recipes/recipes/italian/bruschetta.mdx +0 -38
  41. package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +0 -39
  42. package/showcases/recipes/recipes/italian/tiramisu.mdx +0 -43
  43. package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +0 -44
  44. package/showcases/recipes/recipes/mexican/guacamole.mdx +0 -39
  45. package/showcases/vinyl-collection/albums/bitches-brew.mdx +0 -36
  46. package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +0 -35
  47. package/showcases/vinyl-collection/albums/in-rainbows.mdx +0 -35
  48. package/showcases/vinyl-collection/albums/kind-of-blue.mdx +0 -32
  49. package/showcases/vinyl-collection/albums/ok-computer.mdx +0 -37
  50. package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +0 -35
  51. package/showcases/vinyl-collection/artists/miles-davis.mdx +0 -27
  52. package/showcases/vinyl-collection/artists/nina-simone.mdx +0 -26
  53. package/showcases/vinyl-collection/artists/radiohead.mdx +0 -27
  54. package/showcases/vinyl-collection/models.ts +0 -73
  55. package/showcases/vinyl-collection/queries.ts +0 -87
  56. package/src/__tests__/semantic-search.integration.test.ts +0 -284
  57. package/test/ast-query.test.ts +0 -128
  58. package/test/collection.test.ts +0 -215
  59. package/test/define-model.test.ts +0 -78
  60. package/test/document.test.ts +0 -225
  61. package/test/extract-sections.test.ts +0 -356
  62. package/test/fixtures/sdlc/MODELS.md +0 -106
  63. package/test/fixtures/sdlc/SKILL.md +0 -404
  64. package/test/fixtures/sdlc/epics/authentication.mdx +0 -42
  65. package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +0 -21
  66. package/test/fixtures/sdlc/index.ts +0 -9
  67. package/test/fixtures/sdlc/models.ts +0 -91
  68. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +0 -20
  69. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +0 -20
  70. package/test/fixtures/sdlc/templates/epic.md +0 -23
  71. package/test/fixtures/sdlc/templates/story.md +0 -19
  72. package/test/helpers.ts +0 -21
  73. package/test/model-instance.test.ts +0 -197
  74. package/test/pattern.test.ts +0 -191
  75. package/test/query-dsl.test.ts +0 -431
  76. package/test/query.test.ts +0 -196
  77. package/test/relationships.test.ts +0 -214
  78. package/test/section.test.ts +0 -160
  79. package/test/table-of-contents.test.ts +0 -135
  80. package/test/validator.test.ts +0 -62
  81. package/tsconfig.json +0 -18
  82. 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
- });
@@ -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
- });