lat.md 0.1.0 → 0.1.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/LICENSE +21 -0
- package/README.md +58 -8
- package/dist/src/cli/check.d.ts +18 -0
- package/dist/src/cli/check.js +122 -0
- package/dist/src/cli/context.d.ts +10 -0
- package/dist/src/cli/context.js +14 -0
- package/dist/src/cli/gen.d.ts +2 -0
- package/dist/src/cli/gen.js +14 -0
- package/dist/src/cli/index.js +126 -17
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +67 -0
- package/dist/src/cli/locate.d.ts +2 -1
- package/dist/src/cli/locate.js +8 -20
- package/dist/src/cli/prompt.d.ts +2 -0
- package/dist/src/cli/prompt.js +62 -0
- package/dist/src/cli/refs.d.ts +4 -1
- package/dist/src/cli/refs.js +36 -109
- package/dist/src/cli/search.d.ts +5 -0
- package/dist/src/cli/search.js +55 -0
- package/dist/src/cli/templates.d.ts +1 -0
- package/dist/src/cli/templates.js +15 -0
- package/dist/src/code-refs.d.ts +13 -0
- package/dist/src/code-refs.js +63 -0
- package/dist/src/format.d.ts +7 -1
- package/dist/src/format.js +26 -4
- package/dist/src/lattice.d.ts +6 -0
- package/dist/src/lattice.js +98 -11
- package/dist/src/search/db.d.ts +4 -0
- package/dist/src/search/db.js +31 -0
- package/dist/src/search/embeddings.d.ts +2 -0
- package/dist/src/search/embeddings.js +25 -0
- package/dist/src/search/index.d.ts +9 -0
- package/dist/src/search/index.js +66 -0
- package/dist/src/search/provider.d.ts +8 -0
- package/dist/src/search/provider.js +40 -0
- package/dist/src/search/search.d.ts +9 -0
- package/dist/src/search/search.js +17 -0
- package/package.json +28 -4
- package/templates/AGENTS.md +56 -0
- package/templates/README +1 -0
- package/templates/init/README.md +5 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { loadAllSections, flattenSections } from '../lattice.js';
|
|
5
|
+
import { embed } from './embeddings.js';
|
|
6
|
+
function hashContent(text) {
|
|
7
|
+
return createHash('sha256').update(text).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
async function sectionContent(section, latDir) {
|
|
10
|
+
const filePath = join(latDir, section.file + '.md');
|
|
11
|
+
const content = await readFile(filePath, 'utf-8');
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
return lines.slice(section.startLine - 1, section.endLine).join('\n');
|
|
14
|
+
}
|
|
15
|
+
export async function indexSections(latDir, db, provider, key) {
|
|
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, latDir);
|
|
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 embed(texts, provider, key);
|
|
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,40 @@
|
|
|
1
|
+
const openai = {
|
|
2
|
+
name: 'openai',
|
|
3
|
+
apiBase: 'https://api.openai.com/v1',
|
|
4
|
+
model: 'text-embedding-3-small',
|
|
5
|
+
dimensions: 1536,
|
|
6
|
+
headers: (key) => ({
|
|
7
|
+
Authorization: `Bearer ${key}`,
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
const vercel = {
|
|
12
|
+
name: 'vercel',
|
|
13
|
+
apiBase: 'https://ai-gateway.vercel.sh/v1',
|
|
14
|
+
model: 'openai/text-embedding-3-small',
|
|
15
|
+
dimensions: 1536,
|
|
16
|
+
headers: (key) => ({
|
|
17
|
+
Authorization: `Bearer ${key}`,
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
export function detectProvider(key) {
|
|
22
|
+
if (key.startsWith('REPLAY_LAT_LLM_KEY::')) {
|
|
23
|
+
const replayUrl = key.slice('REPLAY_LAT_LLM_KEY::'.length);
|
|
24
|
+
return {
|
|
25
|
+
name: 'replay',
|
|
26
|
+
apiBase: replayUrl,
|
|
27
|
+
model: 'replay',
|
|
28
|
+
dimensions: 1536,
|
|
29
|
+
headers: () => ({ 'Content-Type': 'application/json' }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (key.startsWith('sk-ant-')) {
|
|
33
|
+
throw new Error("Anthropic doesn't offer an embedding model. Set LAT_LLM_KEY to an OpenAI (sk-...) or Vercel AI (vck_...) key.");
|
|
34
|
+
}
|
|
35
|
+
if (key.startsWith('vck_'))
|
|
36
|
+
return vercel;
|
|
37
|
+
if (key.startsWith('sk-'))
|
|
38
|
+
return openai;
|
|
39
|
+
throw new Error(`Unrecognized LAT_LLM_KEY prefix. Supported: OpenAI (sk-...), Vercel AI (vck_...).`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from '@libsql/client';
|
|
2
|
+
import type { EmbeddingProvider } from './provider.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, provider: EmbeddingProvider, key: string, limit?: number): Promise<SearchResult[]>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { embed } from './embeddings.js';
|
|
2
|
+
export async function searchSections(db, query, provider, key, limit = 5) {
|
|
3
|
+
const [queryVec] = await embed([query], provider, key);
|
|
4
|
+
const vecJson = JSON.stringify(queryVec);
|
|
5
|
+
const rows = await db.execute({
|
|
6
|
+
sql: `SELECT s.id, s.file, s.heading, s.content
|
|
7
|
+
FROM vector_top_k('sections_vec_idx', vector(?), ?) AS v
|
|
8
|
+
JOIN sections AS s ON s.rowid = v.id`,
|
|
9
|
+
args: [vecJson, limit],
|
|
10
|
+
});
|
|
11
|
+
return rows.rows.map((row) => ({
|
|
12
|
+
id: row.id,
|
|
13
|
+
file: row.file,
|
|
14
|
+
heading: row.heading,
|
|
15
|
+
content: row.content,
|
|
16
|
+
}));
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lat.md",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "A knowledge graph for your codebase, written in markdown",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@10.30.2",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Yury Selivanov",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/1st1/lat.md.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/1st1/lat.md",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"markdown",
|
|
16
|
+
"knowledge-graph",
|
|
17
|
+
"documentation",
|
|
18
|
+
"agents",
|
|
19
|
+
"cli",
|
|
20
|
+
"wiki-links",
|
|
21
|
+
"codebase"
|
|
22
|
+
],
|
|
6
23
|
"bin": {
|
|
7
24
|
"lat": "./dist/src/cli/index.js"
|
|
8
25
|
},
|
|
9
26
|
"files": [
|
|
10
|
-
"dist/src"
|
|
27
|
+
"dist/src",
|
|
28
|
+
"templates"
|
|
11
29
|
],
|
|
12
30
|
"scripts": {
|
|
13
31
|
"build": "tsc",
|
|
@@ -15,7 +33,8 @@
|
|
|
15
33
|
"test:watch": "vitest",
|
|
16
34
|
"typecheck": "tsc --noEmit",
|
|
17
35
|
"format": "prettier --write 'src/**/*.ts'",
|
|
18
|
-
"format:check": "prettier --check 'src/**/*.ts'"
|
|
36
|
+
"format:check": "prettier --check 'src/**/*.ts'",
|
|
37
|
+
"cook-test-rag": "tsx scripts/cook-test-rag.ts"
|
|
19
38
|
},
|
|
20
39
|
"devDependencies": {
|
|
21
40
|
"@types/mdast": "^4.0.4",
|
|
@@ -23,10 +42,15 @@
|
|
|
23
42
|
"mdast-util-from-markdown": "^2.0.3",
|
|
24
43
|
"micromark-util-types": "^2.0.2",
|
|
25
44
|
"prettier": "^3.8.1",
|
|
45
|
+
"tsx": "^4.21.0",
|
|
26
46
|
"typescript": "^5.7.0",
|
|
27
47
|
"vitest": "^3.0.0"
|
|
28
48
|
},
|
|
29
49
|
"dependencies": {
|
|
50
|
+
"@libsql/client": "^0.17.0",
|
|
51
|
+
"chalk": "^5.6.2",
|
|
52
|
+
"commander": "^14.0.3",
|
|
53
|
+
"ignore-walk": "^8.0.0",
|
|
30
54
|
"mdast-util-to-markdown": "^2.1.0",
|
|
31
55
|
"remark-parse": "^11.0.0",
|
|
32
56
|
"remark-stringify": "^11.0.0",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Before starting work
|
|
2
|
+
|
|
3
|
+
- Run `lat search` to find sections relevant to your task. Read them to understand the design intent before writing code.
|
|
4
|
+
- Run `lat prompt` on user prompts to expand any `[[refs]]` — this resolves section names to file locations and provides context.
|
|
5
|
+
|
|
6
|
+
# Post-task checklist (REQUIRED — do not skip)
|
|
7
|
+
|
|
8
|
+
After EVERY task, before responding to the user:
|
|
9
|
+
|
|
10
|
+
- [ ] Update `lat.md/` if you added or changed any functionality, architecture, tests, or behavior
|
|
11
|
+
- [ ] Run `lat check` — all wiki links and code refs must pass
|
|
12
|
+
- [ ] Do not skip these steps. Do not consider your task done until both are complete.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# What is lat.md?
|
|
17
|
+
|
|
18
|
+
This project uses [lat.md](https://www.npmjs.com/package/lat.md) to maintain a structured knowledge graph of its architecture, design decisions, and test specs in the `lat.md/` directory. It is a set of cross-linked markdown files that describe **what** this project does and **why** — the domain concepts, key design decisions, business logic, and test specifications. Use it to ground your work in the actual architecture rather than guessing.
|
|
19
|
+
|
|
20
|
+
# Commands
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
lat locate "Section Name" # find a section by name (exact, fuzzy)
|
|
24
|
+
lat refs "file#Section" # find what references a section
|
|
25
|
+
lat search "natural language" # semantic search across all sections
|
|
26
|
+
lat prompt "user prompt text" # expand [[refs]] to resolved locations
|
|
27
|
+
lat check # validate all links and code refs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run `lat --help` when in doubt about available commands or options.
|
|
31
|
+
|
|
32
|
+
If `lat search` fails because `LAT_LLM_KEY` is not set, explain to the user that semantic search requires an API key (`export LAT_LLM_KEY=sk-...` for OpenAI or `export LAT_LLM_KEY=vck_...` for Vercel). If the user doesn't want to set it up, use `lat locate` for direct lookups instead.
|
|
33
|
+
|
|
34
|
+
# Syntax primer
|
|
35
|
+
|
|
36
|
+
- **Section ids**: `file-stem#Heading#SubHeading` (e.g. `cli#search#Indexing`)
|
|
37
|
+
- **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections
|
|
38
|
+
- **Code refs**: `// @lat: [[section-id]]` (JS/TS) or `# @lat: [[section-id]]` (Python) — ties source code to concepts
|
|
39
|
+
|
|
40
|
+
# Test specs
|
|
41
|
+
|
|
42
|
+
Key tests can be described as sections in `lat.md/` files (e.g. `tests.md`). Add frontmatter to require that every leaf section is referenced by a `// @lat:` or `# @lat:` comment in test code:
|
|
43
|
+
|
|
44
|
+
```markdown
|
|
45
|
+
---
|
|
46
|
+
lat:
|
|
47
|
+
require-code-mention: true
|
|
48
|
+
---
|
|
49
|
+
# Tests
|
|
50
|
+
|
|
51
|
+
## User login
|
|
52
|
+
### Rejects expired tokens
|
|
53
|
+
### Handles missing password
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each test in code should reference its spec: `// @lat: [[tests#User login#Rejects expired tokens]]`. Running `lat check` will flag any spec section not covered by a code reference, and any code reference pointing to a nonexistent section.
|
package/templates/README
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This directory is used as source of templates for commands like `lat init`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
This directory defines the high-level concepts, business logic, and architecture of this project using markdown.
|
|
2
|
+
|
|
3
|
+
It is managed by [lat.md](https://www.npmjs.com/package/lat.md) — a tool that anchors source code to these definitions.
|
|
4
|
+
|
|
5
|
+
Install the `lat` command with `npm i -g lat.md` and run `lat --help`.
|