@plebeiantech/lat.md 0.12.3-fork.1
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/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/src/cli/check-frontmatter.d.ts +12 -0
- package/dist/src/cli/check-frontmatter.js +44 -0
- package/dist/src/cli/check-mode.d.ts +6 -0
- package/dist/src/cli/check-mode.js +303 -0
- package/dist/src/cli/check-status.d.ts +74 -0
- package/dist/src/cli/check-status.js +217 -0
- package/dist/src/cli/check.d.ts +45 -0
- package/dist/src/cli/check.js +777 -0
- package/dist/src/cli/checklist-menu.d.ts +12 -0
- package/dist/src/cli/checklist-menu.js +111 -0
- package/dist/src/cli/comment-guard.d.ts +16 -0
- package/dist/src/cli/comment-guard.js +111 -0
- package/dist/src/cli/comment-reminder.d.ts +62 -0
- package/dist/src/cli/comment-reminder.js +210 -0
- package/dist/src/cli/context.d.ts +11 -0
- package/dist/src/cli/context.js +58 -0
- package/dist/src/cli/expand.d.ts +8 -0
- package/dist/src/cli/expand.js +94 -0
- package/dist/src/cli/gen-index.d.ts +45 -0
- package/dist/src/cli/gen-index.js +235 -0
- package/dist/src/cli/gen.d.ts +6 -0
- package/dist/src/cli/gen.js +42 -0
- package/dist/src/cli/graph.d.ts +7 -0
- package/dist/src/cli/graph.js +47 -0
- package/dist/src/cli/hook.d.ts +1 -0
- package/dist/src/cli/hook.js +432 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +307 -0
- package/dist/src/cli/init.d.ts +9 -0
- package/dist/src/cli/init.js +1031 -0
- package/dist/src/cli/link-scheme.d.ts +54 -0
- package/dist/src/cli/link-scheme.js +101 -0
- package/dist/src/cli/locate.d.ts +2 -0
- package/dist/src/cli/locate.js +17 -0
- package/dist/src/cli/refs.d.ts +22 -0
- package/dist/src/cli/refs.js +229 -0
- package/dist/src/cli/reindex.d.ts +13 -0
- package/dist/src/cli/reindex.js +136 -0
- package/dist/src/cli/search.d.ts +36 -0
- package/dist/src/cli/search.js +217 -0
- package/dist/src/cli/section.d.ts +43 -0
- package/dist/src/cli/section.js +252 -0
- package/dist/src/cli/select-menu.d.ts +13 -0
- package/dist/src/cli/select-menu.js +100 -0
- package/dist/src/cli/templates.d.ts +1 -0
- package/dist/src/cli/templates.js +15 -0
- package/dist/src/cli/ui.d.ts +11 -0
- package/dist/src/cli/ui.js +34 -0
- package/dist/src/code-refs.d.ts +17 -0
- package/dist/src/code-refs.js +277 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +83 -0
- package/dist/src/context.d.ts +23 -0
- package/dist/src/context.js +11 -0
- package/dist/src/extensions/wiki-link/from-markdown.d.ts +5 -0
- package/dist/src/extensions/wiki-link/from-markdown.js +34 -0
- package/dist/src/extensions/wiki-link/index.d.ts +4 -0
- package/dist/src/extensions/wiki-link/index.js +3 -0
- package/dist/src/extensions/wiki-link/syntax.d.ts +14 -0
- package/dist/src/extensions/wiki-link/syntax.js +121 -0
- package/dist/src/extensions/wiki-link/to-markdown.d.ts +5 -0
- package/dist/src/extensions/wiki-link/to-markdown.js +28 -0
- package/dist/src/extensions/wiki-link/types.d.ts +34 -0
- package/dist/src/extensions/wiki-link/types.js +1 -0
- package/dist/src/format.d.ts +8 -0
- package/dist/src/format.js +43 -0
- package/dist/src/graph-export.d.ts +60 -0
- package/dist/src/graph-export.js +309 -0
- package/dist/src/init-version.d.ts +10 -0
- package/dist/src/init-version.js +49 -0
- package/dist/src/knowledge/bd.d.ts +2 -0
- package/dist/src/knowledge/bd.js +107 -0
- package/dist/src/knowledge/claude-memory.d.ts +13 -0
- package/dist/src/knowledge/claude-memory.js +204 -0
- package/dist/src/knowledge/cq.d.ts +2 -0
- package/dist/src/knowledge/cq.js +74 -0
- package/dist/src/knowledge/index.d.ts +49 -0
- package/dist/src/knowledge/index.js +271 -0
- package/dist/src/knowledge/ranking.d.ts +17 -0
- package/dist/src/knowledge/ranking.js +62 -0
- package/dist/src/knowledge/session.d.ts +46 -0
- package/dist/src/knowledge/session.js +203 -0
- package/dist/src/knowledge/types.d.ts +47 -0
- package/dist/src/knowledge/types.js +10 -0
- package/dist/src/lattice.d.ts +108 -0
- package/dist/src/lattice.js +816 -0
- package/dist/src/mcp/server.d.ts +1 -0
- package/dist/src/mcp/server.js +58 -0
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +18 -0
- package/dist/src/search/db.d.ts +16 -0
- package/dist/src/search/db.js +54 -0
- package/dist/src/search/embedder.d.ts +25 -0
- package/dist/src/search/embedder.js +58 -0
- package/dist/src/search/index.d.ts +9 -0
- package/dist/src/search/index.js +66 -0
- package/dist/src/search/search.d.ts +9 -0
- package/dist/src/search/search.js +16 -0
- package/dist/src/source-parser.d.ts +27 -0
- package/dist/src/source-parser.js +916 -0
- package/dist/src/untrusted.d.ts +24 -0
- package/dist/src/untrusted.js +97 -0
- package/dist/src/version.d.ts +7 -0
- package/dist/src/version.js +39 -0
- package/dist/src/view/client/assets/index-B0n0u03y.css +1 -0
- package/dist/src/view/client/assets/index-xm2_px4q.js +23 -0
- package/dist/src/view/client/index.html +14 -0
- package/dist/src/view/diagnostics.d.ts +5 -0
- package/dist/src/view/diagnostics.js +171 -0
- package/dist/src/view/git-diff.d.ts +3 -0
- package/dist/src/view/git-diff.js +255 -0
- package/dist/src/view/git.d.ts +22 -0
- package/dist/src/view/git.js +193 -0
- package/dist/src/view/highlight.d.ts +2 -0
- package/dist/src/view/highlight.js +3767 -0
- package/dist/src/view/markdown.d.ts +22 -0
- package/dist/src/view/markdown.js +322 -0
- package/dist/src/view/protocol.d.ts +89 -0
- package/dist/src/view/protocol.js +1 -0
- package/dist/src/view/references.d.ts +62 -0
- package/dist/src/view/references.js +345 -0
- package/dist/src/view/repository.d.ts +12 -0
- package/dist/src/view/repository.js +167 -0
- package/dist/src/view/search.d.ts +14 -0
- package/dist/src/view/search.js +64 -0
- package/dist/src/view/server.d.ts +20 -0
- package/dist/src/view/server.js +233 -0
- package/dist/src/view/store.d.ts +57 -0
- package/dist/src/view/store.js +404 -0
- package/dist/src/walk.d.ts +18 -0
- package/dist/src/walk.js +27 -0
- package/package.json +85 -0
- package/templates/AGENTS.md +118 -0
- package/templates/README +1 -0
- package/templates/cursor-rules.md +77 -0
- package/templates/init/lat.md +1 -0
- package/templates/logo-dark.svg +182 -0
- package/templates/logo-light.svg +182 -0
- package/templates/logo.txt +7 -0
- package/templates/opencode-plugin.ts +180 -0
- package/templates/pi-extension.ts +364 -0
- package/templates/skill/SKILL.md +189 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startMcpServer(): Promise<void>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { findLatticeDir } from '../lattice.js';
|
|
6
|
+
import { plainStyler } from '../context.js';
|
|
7
|
+
import { locateCommand } from '../cli/locate.js';
|
|
8
|
+
import { sectionCommand } from '../cli/section.js';
|
|
9
|
+
import { searchCommand } from '../cli/search.js';
|
|
10
|
+
import { expandCommand } from '../cli/expand.js';
|
|
11
|
+
import { checkAllCommand } from '../cli/check.js';
|
|
12
|
+
import { refsCommand } from '../cli/refs.js';
|
|
13
|
+
function toMcp(result) {
|
|
14
|
+
const content = [{ type: 'text', text: result.output }];
|
|
15
|
+
return result.isError ? { content, isError: true } : { content };
|
|
16
|
+
}
|
|
17
|
+
export async function startMcpServer() {
|
|
18
|
+
const latDir = findLatticeDir();
|
|
19
|
+
if (!latDir) {
|
|
20
|
+
process.stderr.write('No lat.md directory found\n');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const projectRoot = dirname(latDir);
|
|
24
|
+
const ctx = {
|
|
25
|
+
latDir,
|
|
26
|
+
projectRoot,
|
|
27
|
+
styler: plainStyler,
|
|
28
|
+
mode: 'mcp',
|
|
29
|
+
};
|
|
30
|
+
const server = new McpServer({
|
|
31
|
+
name: 'lat',
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
});
|
|
34
|
+
server.tool('lat_locate', 'Find sections by name (exact, fuzzy, subsequence matching)', { query: z.string().describe('Section name or id to search for') }, async ({ query }) => toMcp(await locateCommand(ctx, query)));
|
|
35
|
+
server.tool('lat_section', 'Show a section with its content, outgoing wiki link targets, and incoming references', {
|
|
36
|
+
query: z.string().describe('Section id to look up (short or full form)'),
|
|
37
|
+
}, async ({ query }) => toMcp(await sectionCommand(ctx, query)));
|
|
38
|
+
server.tool('lat_search', 'Semantic search across lat.md sections using embeddings', {
|
|
39
|
+
query: z.string().describe('Search query in natural language'),
|
|
40
|
+
limit: z
|
|
41
|
+
.number()
|
|
42
|
+
.optional()
|
|
43
|
+
.default(5)
|
|
44
|
+
.describe('Max results (default 5)'),
|
|
45
|
+
}, async ({ query, limit }) => toMcp(await searchCommand(ctx, query, { limit })));
|
|
46
|
+
server.tool('lat_expand', 'Expand [[refs]] in text to resolved lat.md section paths with context', { text: z.string().describe('Text containing [[refs]] to expand') }, async ({ text: input }) => toMcp(await expandCommand(ctx, input)));
|
|
47
|
+
server.tool('lat_check', 'Run full lat.md validation: links, code references, indexes, and section structure', {}, async () => toMcp(await checkAllCommand(ctx)));
|
|
48
|
+
server.tool('lat_refs', 'Find sections that reference a given section via wiki links or @lat code comments', {
|
|
49
|
+
query: z.string().describe('Section id to find references for'),
|
|
50
|
+
scope: z
|
|
51
|
+
.enum(['md', 'code', 'md+code'])
|
|
52
|
+
.optional()
|
|
53
|
+
.default('md+code')
|
|
54
|
+
.describe('Where to search: md, code, or md+code'),
|
|
55
|
+
}, async ({ query, scope }) => toMcp(await refsCommand(ctx, query, scope)));
|
|
56
|
+
const transport = new StdioServerTransport();
|
|
57
|
+
await server.connect(transport);
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkStringify from 'remark-stringify';
|
|
4
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
5
|
+
import { wikiLinkSyntax, wikiLinkFromMarkdown, wikiLinkToMarkdown, } from './extensions/wiki-link/index.js';
|
|
6
|
+
const processor = unified()
|
|
7
|
+
.use(remarkParse)
|
|
8
|
+
.use(remarkFrontmatter)
|
|
9
|
+
.use(remarkStringify)
|
|
10
|
+
.data('micromarkExtensions', [wikiLinkSyntax()])
|
|
11
|
+
.data('fromMarkdownExtensions', [wikiLinkFromMarkdown()])
|
|
12
|
+
.data('toMarkdownExtensions', [wikiLinkToMarkdown()]);
|
|
13
|
+
export function parse(markdown) {
|
|
14
|
+
return processor.parse(markdown);
|
|
15
|
+
}
|
|
16
|
+
export function toMarkdown(tree) {
|
|
17
|
+
return processor.stringify(tree);
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Client } from '@libsql/client';
|
|
2
|
+
export declare function openDb(latDir: string): Client;
|
|
3
|
+
/** Create the `meta` key/value table if absent. */
|
|
4
|
+
export declare function ensureMeta(db: Client): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* The embedding backend this index was built with, e.g. `local:minilm-l6-v2:384`
|
|
7
|
+
* or `openai:1536`. This record is *authoritative*: it governs which backend
|
|
8
|
+
* `lat search` uses. Null when the index has never been built.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getStoredModel(db: Client): Promise<string | null>;
|
|
11
|
+
export declare function setStoredModel(db: Client, value: string): Promise<void>;
|
|
12
|
+
/** Create the `sections` table (fixed-width vector column) + index if absent. */
|
|
13
|
+
export declare function ensureSectionsSchema(db: Client, dimensions: number): Promise<void>;
|
|
14
|
+
/** Drop the vector table (used by `lat reindex` before a full rebuild). */
|
|
15
|
+
export declare function dropSections(db: Client): Promise<void>;
|
|
16
|
+
export declare function closeDb(db: Client): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createClient } from '@libsql/client';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function openDb(latDir) {
|
|
5
|
+
const cacheDir = join(latDir, '.cache');
|
|
6
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
7
|
+
const client = createClient({
|
|
8
|
+
url: `file:${join(cacheDir, 'vectors.db')}`,
|
|
9
|
+
});
|
|
10
|
+
return client;
|
|
11
|
+
}
|
|
12
|
+
/** Create the `meta` key/value table if absent. */
|
|
13
|
+
export async function ensureMeta(db) {
|
|
14
|
+
await db.execute(`CREATE TABLE IF NOT EXISTS meta (
|
|
15
|
+
key TEXT PRIMARY KEY,
|
|
16
|
+
value TEXT NOT NULL
|
|
17
|
+
)`);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The embedding backend this index was built with, e.g. `local:minilm-l6-v2:384`
|
|
21
|
+
* or `openai:1536`. This record is *authoritative*: it governs which backend
|
|
22
|
+
* `lat search` uses. Null when the index has never been built.
|
|
23
|
+
*/
|
|
24
|
+
export async function getStoredModel(db) {
|
|
25
|
+
const rows = await db.execute("SELECT value FROM meta WHERE key = 'embedding_model'");
|
|
26
|
+
return rows.rows[0]?.value ?? null;
|
|
27
|
+
}
|
|
28
|
+
export async function setStoredModel(db, value) {
|
|
29
|
+
await db.execute({
|
|
30
|
+
sql: "INSERT OR REPLACE INTO meta (key, value) VALUES ('embedding_model', ?)",
|
|
31
|
+
args: [value],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Create the `sections` table (fixed-width vector column) + index if absent. */
|
|
35
|
+
export async function ensureSectionsSchema(db, dimensions) {
|
|
36
|
+
await db.execute(`CREATE TABLE IF NOT EXISTS sections (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
file TEXT NOT NULL,
|
|
39
|
+
heading TEXT NOT NULL,
|
|
40
|
+
content TEXT NOT NULL,
|
|
41
|
+
content_hash TEXT NOT NULL,
|
|
42
|
+
embedding F32_BLOB(${dimensions}),
|
|
43
|
+
updated_at INTEGER NOT NULL
|
|
44
|
+
)`);
|
|
45
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS sections_vec_idx
|
|
46
|
+
ON sections (libsql_vector_idx(embedding))`);
|
|
47
|
+
}
|
|
48
|
+
/** Drop the vector table (used by `lat reindex` before a full rebuild). */
|
|
49
|
+
export async function dropSections(db) {
|
|
50
|
+
await db.execute('DROP TABLE IF EXISTS sections');
|
|
51
|
+
}
|
|
52
|
+
export async function closeDb(db) {
|
|
53
|
+
db.close();
|
|
54
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { EmbeddingAuthError, type Embedder } from '@lat.md/embed';
|
|
2
|
+
export type { Embedder };
|
|
3
|
+
export { EmbeddingAuthError };
|
|
4
|
+
/** `meta.embedding_model` value for an embedder, e.g. `local:minilm-l6-v2:384`. */
|
|
5
|
+
export declare function modelKey(embedder: Embedder): string;
|
|
6
|
+
/** Thrown when the stored index can't be served by the current environment and
|
|
7
|
+
* a `lat reindex` is required (wrong/absent key, or model mismatch). */
|
|
8
|
+
export declare class ReindexRequiredError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
/** Build an embedder from the environment (key → remote, else local). Used for
|
|
12
|
+
* a fresh index and by `lat reindex` when re-deciding the backend. */
|
|
13
|
+
export declare function embedderFromEnv(): Promise<Embedder>;
|
|
14
|
+
/** Local (offline) embedder, ignoring any configured key. */
|
|
15
|
+
export declare function localEmbedder(): Promise<Embedder>;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the embedder `lat search` must use, given the index's recorded model
|
|
18
|
+
* and the repo dir. The stored model is authoritative — the env var never
|
|
19
|
+
* silently flips the backend. A local index ignores the key; a remote index
|
|
20
|
+
* requires a matching key (used to embed every query) or throws
|
|
21
|
+
* {@link ReindexRequiredError}. On a fresh index (no stored model — e.g. the
|
|
22
|
+
* regenerable `.cache` was wiped) the durable per-repo preference wins over the
|
|
23
|
+
* env, so a repo switched to local stays local across cache loss.
|
|
24
|
+
*/
|
|
25
|
+
export declare function embedderForIndex(storedModel: string | null, latDir: string): Promise<Embedder>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createEmbedder, EmbeddingAuthError, } from '@lat.md/embed';
|
|
2
|
+
import minilm from '@lat.md/embed-minilm-fp16';
|
|
3
|
+
import { getLlmKey, getRepoEmbedding } from '../config.js';
|
|
4
|
+
export { EmbeddingAuthError };
|
|
5
|
+
/** `meta.embedding_model` value for an embedder, e.g. `local:minilm-l6-v2:384`. */
|
|
6
|
+
export function modelKey(embedder) {
|
|
7
|
+
return `${embedder.name}:${embedder.dimensions}`;
|
|
8
|
+
}
|
|
9
|
+
/** Thrown when the stored index can't be served by the current environment and
|
|
10
|
+
* a `lat reindex` is required (wrong/absent key, or model mismatch). */
|
|
11
|
+
export class ReindexRequiredError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'ReindexRequiredError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Build an embedder from the environment (key → remote, else local). Used for
|
|
18
|
+
* a fresh index and by `lat reindex` when re-deciding the backend. */
|
|
19
|
+
export async function embedderFromEnv() {
|
|
20
|
+
const key = getLlmKey();
|
|
21
|
+
return key ? createEmbedder({ key }) : createEmbedder({ model: minilm });
|
|
22
|
+
}
|
|
23
|
+
/** Local (offline) embedder, ignoring any configured key. */
|
|
24
|
+
export function localEmbedder() {
|
|
25
|
+
return createEmbedder({ model: minilm });
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the embedder `lat search` must use, given the index's recorded model
|
|
29
|
+
* and the repo dir. The stored model is authoritative — the env var never
|
|
30
|
+
* silently flips the backend. A local index ignores the key; a remote index
|
|
31
|
+
* requires a matching key (used to embed every query) or throws
|
|
32
|
+
* {@link ReindexRequiredError}. On a fresh index (no stored model — e.g. the
|
|
33
|
+
* regenerable `.cache` was wiped) the durable per-repo preference wins over the
|
|
34
|
+
* env, so a repo switched to local stays local across cache loss.
|
|
35
|
+
*/
|
|
36
|
+
export async function embedderForIndex(storedModel, latDir) {
|
|
37
|
+
if (storedModel === null) {
|
|
38
|
+
if (getRepoEmbedding(latDir) === 'local')
|
|
39
|
+
return localEmbedder();
|
|
40
|
+
// No durable preference — decide from the environment, record it later.
|
|
41
|
+
return embedderFromEnv();
|
|
42
|
+
}
|
|
43
|
+
if (storedModel.startsWith('local:')) {
|
|
44
|
+
return localEmbedder(); // ignores LAT_LLM_KEY entirely
|
|
45
|
+
}
|
|
46
|
+
// Remote-pinned index: needs a key that resolves to the same model.
|
|
47
|
+
const key = getLlmKey();
|
|
48
|
+
if (!key) {
|
|
49
|
+
throw new ReindexRequiredError(`This index was built with '${storedModel}', but no LAT_LLM_KEY is set. ` +
|
|
50
|
+
`Run 'lat reindex' to switch to the local model or restore the key.`);
|
|
51
|
+
}
|
|
52
|
+
const embedder = await createEmbedder({ key });
|
|
53
|
+
if (modelKey(embedder) !== storedModel) {
|
|
54
|
+
throw new ReindexRequiredError(`This index was built with '${storedModel}', but the current key resolves ` +
|
|
55
|
+
`to '${modelKey(embedder)}'. Run 'lat reindex' to rebuild.`);
|
|
56
|
+
}
|
|
57
|
+
return embedder;
|
|
58
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from '@libsql/client';
|
|
2
|
+
import type { Embedder } from './embedder.js';
|
|
3
|
+
export type IndexStats = {
|
|
4
|
+
added: number;
|
|
5
|
+
updated: number;
|
|
6
|
+
removed: number;
|
|
7
|
+
unchanged: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function indexSections(latDir: string, db: Client, embedder: Embedder, onProgress?: (done: number, total: number) => void): Promise<IndexStats>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { loadAllSections, flattenSections } from '../lattice.js';
|
|
5
|
+
function hashContent(text) {
|
|
6
|
+
return createHash('sha256').update(text).digest('hex');
|
|
7
|
+
}
|
|
8
|
+
async function sectionContent(section, projectRoot) {
|
|
9
|
+
const filePath = join(projectRoot, section.filePath);
|
|
10
|
+
const content = await readFile(filePath, 'utf-8');
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
return lines.slice(section.startLine - 1, section.endLine).join('\n');
|
|
13
|
+
}
|
|
14
|
+
export async function indexSections(latDir, db, embedder, onProgress) {
|
|
15
|
+
const projectRoot = dirname(latDir);
|
|
16
|
+
const allSections = await loadAllSections(latDir);
|
|
17
|
+
const flat = flattenSections(allSections);
|
|
18
|
+
// Build current state: id -> { section, content, hash }
|
|
19
|
+
const current = new Map();
|
|
20
|
+
for (const s of flat) {
|
|
21
|
+
const text = await sectionContent(s, projectRoot);
|
|
22
|
+
current.set(s.id, { section: s, content: text, hash: hashContent(text) });
|
|
23
|
+
}
|
|
24
|
+
// Get existing hashes from DB
|
|
25
|
+
const existing = new Map();
|
|
26
|
+
const rows = await db.execute('SELECT id, content_hash FROM sections');
|
|
27
|
+
for (const row of rows.rows) {
|
|
28
|
+
existing.set(row.id, row.content_hash);
|
|
29
|
+
}
|
|
30
|
+
// Partition into new, changed, unchanged, deleted
|
|
31
|
+
const toEmbed = [];
|
|
32
|
+
let unchanged = 0;
|
|
33
|
+
for (const [id, entry] of current) {
|
|
34
|
+
const existingHash = existing.get(id);
|
|
35
|
+
if (existingHash === entry.hash) {
|
|
36
|
+
unchanged++;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
toEmbed.push({ id, content: entry.content, section: entry.section });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const toDelete = [...existing.keys()].filter((id) => !current.has(id));
|
|
43
|
+
// Embed new/changed sections
|
|
44
|
+
if (toEmbed.length > 0) {
|
|
45
|
+
const texts = toEmbed.map((e) => e.content);
|
|
46
|
+
const vectors = await embedder.embed(texts, onProgress);
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
for (let i = 0; i < toEmbed.length; i++) {
|
|
49
|
+
const { id, content, section } = toEmbed[i];
|
|
50
|
+
const hash = current.get(id).hash;
|
|
51
|
+
const vecJson = JSON.stringify(vectors[i]);
|
|
52
|
+
await db.execute({
|
|
53
|
+
sql: `INSERT OR REPLACE INTO sections (id, file, heading, content, content_hash, embedding, updated_at)
|
|
54
|
+
VALUES (?, ?, ?, ?, ?, vector(?), ?)`,
|
|
55
|
+
args: [id, section.file, section.heading, content, hash, vecJson, now],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Delete removed sections
|
|
60
|
+
for (const id of toDelete) {
|
|
61
|
+
await db.execute({ sql: 'DELETE FROM sections WHERE id = ?', args: [id] });
|
|
62
|
+
}
|
|
63
|
+
const added = toEmbed.filter((e) => !existing.has(e.id)).length;
|
|
64
|
+
const updated = toEmbed.filter((e) => existing.has(e.id)).length;
|
|
65
|
+
return { added, updated, removed: toDelete.length, unchanged };
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from '@libsql/client';
|
|
2
|
+
import type { Embedder } from './embedder.js';
|
|
3
|
+
export type SearchResult = {
|
|
4
|
+
id: string;
|
|
5
|
+
file: string;
|
|
6
|
+
heading: string;
|
|
7
|
+
content: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function searchSections(db: Client, query: string, embedder: Embedder, limit?: number): Promise<SearchResult[]>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export async function searchSections(db, query, embedder, limit = 5) {
|
|
2
|
+
const [queryVec] = await embedder.embed([query]);
|
|
3
|
+
const vecJson = JSON.stringify(queryVec);
|
|
4
|
+
const rows = await db.execute({
|
|
5
|
+
sql: `SELECT s.id, s.file, s.heading, s.content
|
|
6
|
+
FROM vector_top_k('sections_vec_idx', vector(?), ?) AS v
|
|
7
|
+
JOIN sections AS s ON s.rowid = v.id`,
|
|
8
|
+
args: [vecJson, limit],
|
|
9
|
+
});
|
|
10
|
+
return rows.rows.map((row) => ({
|
|
11
|
+
id: row.id,
|
|
12
|
+
file: row.file,
|
|
13
|
+
heading: row.heading,
|
|
14
|
+
content: row.content,
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Section } from './lattice.js';
|
|
2
|
+
export type SourceSymbol = {
|
|
3
|
+
name: string;
|
|
4
|
+
kind: 'function' | 'class' | 'const' | 'type' | 'interface' | 'method' | 'variable';
|
|
5
|
+
parent?: string;
|
|
6
|
+
startLine: number;
|
|
7
|
+
endLine: number;
|
|
8
|
+
signature: string;
|
|
9
|
+
};
|
|
10
|
+
/** All source file extensions that lat can parse (derived from grammarMap). */
|
|
11
|
+
export declare const SOURCE_EXTENSIONS: ReadonlySet<string>;
|
|
12
|
+
export declare function parseSourceSymbols(filePath: string, content: string): Promise<SourceSymbol[]>;
|
|
13
|
+
/** Clear the symbol cache. Call between top-level operations. */
|
|
14
|
+
export declare function clearSymbolCache(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Check whether a source file path (relative to projectRoot) has a given symbol.
|
|
17
|
+
* Used by lat check to validate source code wiki links lazily.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveSourceSymbol(filePath: string, symbolPath: string, projectRoot: string): Promise<{
|
|
20
|
+
found: boolean;
|
|
21
|
+
symbols: SourceSymbol[];
|
|
22
|
+
error?: string;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Convert source symbols to Section objects for uniform handling.
|
|
26
|
+
*/
|
|
27
|
+
export declare function sourceSymbolsToSections(symbols: SourceSymbol[], filePath: string): Section[];
|