lat.md 0.5.0 → 0.7.0

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 (39) hide show
  1. package/dist/src/cli/check.d.ts +7 -5
  2. package/dist/src/cli/check.js +243 -74
  3. package/dist/src/cli/context.d.ts +3 -7
  4. package/dist/src/cli/context.js +15 -1
  5. package/dist/src/cli/expand.d.ts +7 -0
  6. package/dist/src/cli/expand.js +92 -0
  7. package/dist/src/cli/gen.js +11 -4
  8. package/dist/src/cli/hook.d.ts +1 -0
  9. package/dist/src/cli/hook.js +147 -0
  10. package/dist/src/cli/index.js +77 -28
  11. package/dist/src/cli/init.js +148 -120
  12. package/dist/src/cli/locate.d.ts +2 -2
  13. package/dist/src/cli/locate.js +9 -4
  14. package/dist/src/cli/refs.d.ts +20 -4
  15. package/dist/src/cli/refs.js +64 -42
  16. package/dist/src/cli/search.d.ts +25 -3
  17. package/dist/src/cli/search.js +87 -47
  18. package/dist/src/cli/section.d.ts +26 -0
  19. package/dist/src/cli/section.js +133 -0
  20. package/dist/src/code-refs.js +2 -1
  21. package/dist/src/config.js +3 -2
  22. package/dist/src/context.d.ts +21 -0
  23. package/dist/src/context.js +11 -0
  24. package/dist/src/format.d.ts +4 -3
  25. package/dist/src/format.js +16 -20
  26. package/dist/src/init-version.d.ts +10 -0
  27. package/dist/src/init-version.js +49 -0
  28. package/dist/src/lattice.d.ts +11 -5
  29. package/dist/src/lattice.js +87 -38
  30. package/dist/src/mcp/server.js +27 -279
  31. package/dist/src/search/index.js +5 -4
  32. package/dist/src/source-parser.d.ts +23 -0
  33. package/dist/src/source-parser.js +720 -0
  34. package/package.json +3 -1
  35. package/templates/AGENTS.md +38 -6
  36. package/templates/cursor-rules.md +11 -5
  37. package/templates/lat-prompt-hook.sh +2 -2
  38. package/dist/src/cli/prompt.d.ts +0 -2
  39. package/dist/src/cli/prompt.js +0 -60
@@ -1,67 +1,107 @@
1
- import chalk from 'chalk';
2
1
  import { openDb, ensureSchema, closeDb } from '../search/db.js';
3
2
  import { detectProvider } from '../search/provider.js';
4
3
  import { indexSections } from '../search/index.js';
5
4
  import { searchSections } from '../search/search.js';
6
- import { loadAllSections, flattenSections } from '../lattice.js';
5
+ import { loadAllSections, flattenSections, } from '../lattice.js';
7
6
  import { formatResultList } from '../format.js';
8
- import { getLlmKey, getConfigPath } from '../config.js';
9
- export async function searchCmd(ctx, query, opts) {
10
- let key;
11
- try {
12
- key = getLlmKey();
13
- }
14
- catch (err) {
15
- console.error(chalk.red(err.message));
16
- process.exit(1);
17
- }
18
- if (!key) {
19
- console.error(chalk.red('No API key configured.') +
20
- ' Provide a key via LAT_LLM_KEY, LAT_LLM_KEY_FILE, LAT_LLM_KEY_HELPER, or run ' +
21
- chalk.cyan('lat init') +
22
- ' to save one in ' +
23
- chalk.dim(getConfigPath()) +
24
- '.');
25
- process.exit(1);
26
- }
7
+ async function withDb(latDir, key, progress, fn) {
27
8
  const provider = detectProvider(key);
28
- const db = openDb(ctx.latDir);
9
+ const db = openDb(latDir);
29
10
  try {
30
11
  await ensureSchema(db, provider.dimensions);
31
- // Check if index needs updating
32
12
  const countResult = await db.execute('SELECT COUNT(*) as n FROM sections');
33
13
  const isEmpty = countResult.rows[0].n === 0;
34
- if (isEmpty || opts.reindex) {
35
- const label = opts.reindex ? 'Re-indexing' : 'Building index';
36
- process.stderr.write(chalk.dim(`${label}...`));
37
- const stats = await indexSections(ctx.latDir, db, provider, key);
38
- process.stderr.write(chalk.dim(` done (${stats.added} added, ${stats.updated} updated, ${stats.removed} removed)\n`));
39
- }
40
- else {
41
- // Incremental update
42
- const stats = await indexSections(ctx.latDir, db, provider, key);
43
- if (stats.added + stats.updated + stats.removed > 0) {
44
- process.stderr.write(chalk.dim(`Index updated: ${stats.added} added, ${stats.updated} updated, ${stats.removed} removed\n`));
45
- }
46
- }
47
- if (!query)
48
- return;
49
- const results = await searchSections(db, query, provider, key, opts.limit);
14
+ progress?.beforeIndex?.(isEmpty);
15
+ const stats = await indexSections(latDir, db, provider, key);
16
+ progress?.afterIndex?.(stats, isEmpty);
17
+ return await fn(db, provider);
18
+ }
19
+ finally {
20
+ await closeDb(db);
21
+ }
22
+ }
23
+ /**
24
+ * Run a semantic search across lat.md sections.
25
+ * Handles indexing (with optional progress callback). Returns matched sections.
26
+ */
27
+ export async function runSearch(latDir, query, key, limit, progress) {
28
+ return withDb(latDir, key, progress, async (db, provider) => {
29
+ const results = await searchSections(db, query, provider, key, limit);
50
30
  if (results.length === 0) {
51
- console.log(chalk.dim('No results found.'));
52
- return;
31
+ return { query, matches: [] };
53
32
  }
54
- // Load sections for formatting with location info
55
- const allSections = await loadAllSections(ctx.latDir);
33
+ const allSections = await loadAllSections(latDir);
56
34
  const flat = flattenSections(allSections);
57
35
  const byId = new Map(flat.map((s) => [s.id, s]));
58
- const matched = results
36
+ const matches = results
59
37
  .map((r) => byId.get(r.id))
60
38
  .filter((s) => !!s)
61
39
  .map((s) => ({ section: s, reason: 'semantic match' }));
62
- console.log(formatResultList(`Search results for "${query}":`, matched, ctx.latDir));
40
+ return { query, matches };
41
+ });
42
+ }
43
+ /**
44
+ * Index-only mode (no query). Used by `lat search --reindex`.
45
+ */
46
+ export async function runIndex(latDir, key, progress) {
47
+ await withDb(latDir, key, progress, async () => { });
48
+ }
49
+ export function cliProgress(reindex, s) {
50
+ return {
51
+ beforeIndex(isEmpty) {
52
+ if (isEmpty || reindex) {
53
+ const label = reindex ? 'Re-indexing' : 'Building index';
54
+ process.stderr.write(s.dim(`${label}...`));
55
+ }
56
+ },
57
+ afterIndex(stats, isEmpty) {
58
+ if (isEmpty || reindex) {
59
+ process.stderr.write(s.dim(` done (${stats.added} added, ${stats.updated} updated, ${stats.removed} removed)\n`));
60
+ }
61
+ else if (stats.added + stats.updated + stats.removed > 0) {
62
+ process.stderr.write(s.dim(`Index updated: ${stats.added} added, ${stats.updated} updated, ${stats.removed} removed\n`));
63
+ }
64
+ },
65
+ };
66
+ }
67
+ export async function searchCommand(ctx, query, opts, progress) {
68
+ const { getLlmKey, getConfigPath } = await import('../config.js');
69
+ let key;
70
+ try {
71
+ key = getLlmKey();
63
72
  }
64
- finally {
65
- await closeDb(db);
73
+ catch (err) {
74
+ return { output: err.message, isError: true };
75
+ }
76
+ if (!key) {
77
+ const s = ctx.styler;
78
+ return {
79
+ output: s.red('No API key configured.') +
80
+ ' Provide a key via LAT_LLM_KEY, LAT_LLM_KEY_FILE, LAT_LLM_KEY_HELPER, or run ' +
81
+ s.cyan('lat init') +
82
+ (ctx.mode === 'cli'
83
+ ? ' to save one in ' + s.dim(getConfigPath())
84
+ : '') +
85
+ '.',
86
+ isError: true,
87
+ };
88
+ }
89
+ if (!query) {
90
+ await runIndex(ctx.latDir, key, progress);
91
+ return { output: '' };
92
+ }
93
+ const result = await runSearch(ctx.latDir, query, key, opts.limit, progress);
94
+ if (result.matches.length === 0) {
95
+ return { output: 'No results found.' };
66
96
  }
97
+ const navHints = ctx.mode === 'cli'
98
+ ? '- `lat section "section#id"` \u2014 show full content with outgoing/incoming refs\n' +
99
+ '- `lat search "new query"` \u2014 search for something else'
100
+ : '- `lat_section` \u2014 show full content with outgoing/incoming refs\n' +
101
+ '- `lat_search` \u2014 search for something else';
102
+ return {
103
+ output: formatResultList(ctx, `Search results for "${query}":`, result.matches) +
104
+ '\nTo navigate further:\n' +
105
+ navHints,
106
+ };
67
107
  }
@@ -0,0 +1,26 @@
1
+ import { type Section, type SectionMatch } from '../lattice.js';
2
+ import type { CmdContext, CmdResult } from '../context.js';
3
+ export type SectionFound = {
4
+ kind: 'found';
5
+ section: Section;
6
+ content: string;
7
+ outgoingRefs: {
8
+ target: string;
9
+ resolved: Section;
10
+ }[];
11
+ incomingRefs: SectionMatch[];
12
+ };
13
+ export type SectionResult = SectionFound | {
14
+ kind: 'no-match';
15
+ suggestions: SectionMatch[];
16
+ };
17
+ /**
18
+ * Look up a section by id, return its content, outgoing wiki link targets,
19
+ * and incoming references from other sections.
20
+ */
21
+ export declare function getSection(ctx: CmdContext, query: string): Promise<SectionResult>;
22
+ /**
23
+ * Format a successful section result with styling.
24
+ */
25
+ export declare function formatSectionOutput(ctx: CmdContext, result: SectionFound): string;
26
+ export declare function sectionCommand(ctx: CmdContext, query: string): Promise<CmdResult>;
@@ -0,0 +1,133 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ import { loadAllSections, findSections, flattenSections, extractRefs, buildFileIndex, resolveRef, listLatticeFiles, } from '../lattice.js';
4
+ import { formatSectionId } from '../format.js';
5
+ /**
6
+ * Look up a section by id, return its content, outgoing wiki link targets,
7
+ * and incoming references from other sections.
8
+ */
9
+ export async function getSection(ctx, query) {
10
+ query = query.replace(/^\[\[|\]\]$/g, '');
11
+ const allSections = await loadAllSections(ctx.latDir);
12
+ const matches = findSections(allSections, query);
13
+ if (matches.length === 0) {
14
+ return { kind: 'no-match', suggestions: [] };
15
+ }
16
+ // Accept the top match if confident
17
+ const top = matches[0];
18
+ const isConfident = top.reason === 'exact match' ||
19
+ top.reason.startsWith('file stem expanded') ||
20
+ top.reason === 'section name match';
21
+ if (!isConfident) {
22
+ return { kind: 'no-match', suggestions: matches };
23
+ }
24
+ const section = top.section;
25
+ // Read raw content between startLine and endLine
26
+ const absPath = join(ctx.projectRoot, section.filePath);
27
+ const fileContent = await readFile(absPath, 'utf-8');
28
+ const lines = fileContent.split('\n');
29
+ const content = lines
30
+ .slice(section.startLine - 1, section.endLine)
31
+ .join('\n');
32
+ // Find outgoing wiki link targets within this section's content
33
+ const flat = flattenSections(allSections);
34
+ const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
35
+ const fileIndex = buildFileIndex(allSections);
36
+ const sectionRefs = extractRefs(absPath, fileContent, ctx.projectRoot);
37
+ const sectionId = section.id.toLowerCase();
38
+ const outgoingRefs = [];
39
+ const seen = new Set();
40
+ for (const ref of sectionRefs) {
41
+ if (ref.fromSection.toLowerCase() !== sectionId)
42
+ continue;
43
+ const { resolved } = resolveRef(ref.target, sectionIds, fileIndex);
44
+ const resolvedLower = resolved.toLowerCase();
45
+ if (seen.has(resolvedLower))
46
+ continue;
47
+ seen.add(resolvedLower);
48
+ const targetSection = flat.find((s) => s.id.toLowerCase() === resolvedLower);
49
+ if (targetSection) {
50
+ outgoingRefs.push({ target: ref.target, resolved: targetSection });
51
+ }
52
+ }
53
+ // Find incoming references: other sections that link to this one
54
+ const incomingRefs = [];
55
+ const files = await listLatticeFiles(ctx.latDir);
56
+ const incomingSections = new Set();
57
+ for (const file of files) {
58
+ const fc = await readFile(file, 'utf-8');
59
+ const fileRefs = extractRefs(file, fc, ctx.projectRoot);
60
+ for (const ref of fileRefs) {
61
+ const { resolved } = resolveRef(ref.target, sectionIds, fileIndex);
62
+ if (resolved.toLowerCase() === sectionId &&
63
+ ref.fromSection.toLowerCase() !== sectionId) {
64
+ if (!incomingSections.has(ref.fromSection.toLowerCase())) {
65
+ incomingSections.add(ref.fromSection.toLowerCase());
66
+ const fromSection = flat.find((s) => s.id.toLowerCase() === ref.fromSection.toLowerCase());
67
+ if (fromSection) {
68
+ incomingRefs.push({ section: fromSection, reason: 'wiki link' });
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return { kind: 'found', section, content, outgoingRefs, incomingRefs };
75
+ }
76
+ function truncate(s, max) {
77
+ return s.length > max ? s.slice(0, max) + '...' : s;
78
+ }
79
+ /**
80
+ * Format a successful section result with styling.
81
+ */
82
+ export function formatSectionOutput(ctx, result) {
83
+ const s = ctx.styler;
84
+ const { section, content, outgoingRefs, incomingRefs } = result;
85
+ const relPath = relative(process.cwd(), join(ctx.projectRoot, section.filePath));
86
+ const loc = `${s.cyan(relPath)}${s.dim(`:${section.startLine}-${section.endLine}`)}`;
87
+ const parts = [
88
+ `${s.bold('[[' + formatSectionId(section.id, s) + ']]')} (${loc})`,
89
+ '',
90
+ content,
91
+ ];
92
+ if (outgoingRefs.length > 0) {
93
+ parts.push('', s.bold('This section references:'), '');
94
+ for (const ref of outgoingRefs) {
95
+ const body = ref.resolved.firstParagraph
96
+ ? ` ${s.dim('—')} ${truncate(ref.resolved.firstParagraph, 120)}`
97
+ : '';
98
+ parts.push(`${s.dim('*')} [[${formatSectionId(ref.resolved.id, s)}]]${body}`);
99
+ }
100
+ }
101
+ if (incomingRefs.length > 0) {
102
+ parts.push('', s.bold('Referenced by:'), '');
103
+ for (const ref of incomingRefs) {
104
+ const body = ref.section.firstParagraph
105
+ ? ` ${s.dim('—')} ${truncate(ref.section.firstParagraph, 120)}`
106
+ : '';
107
+ parts.push(`${s.dim('*')} [[${formatSectionId(ref.section.id, s)}]]${body}`);
108
+ }
109
+ }
110
+ return parts.join('\n');
111
+ }
112
+ export async function sectionCommand(ctx, query) {
113
+ const result = await getSection(ctx, query);
114
+ if (result.kind === 'no-match') {
115
+ const s = ctx.styler;
116
+ if (result.suggestions.length > 0) {
117
+ const suggestions = result.suggestions
118
+ .map((m) => ` ${s.dim('*')} ${s.white(m.section.id)} ${s.dim(`(${m.reason})`)}`)
119
+ .join('\n');
120
+ return {
121
+ output: s.red(`No section "${query}" found.`) +
122
+ ' Did you mean:\n' +
123
+ suggestions,
124
+ isError: true,
125
+ };
126
+ }
127
+ return {
128
+ output: s.red(`No sections matching "${query}"`),
129
+ isError: true,
130
+ };
131
+ }
132
+ return { output: formatSectionOutput(ctx, result) };
133
+ }
@@ -39,7 +39,8 @@ export async function scanCodeRefs(projectRoot) {
39
39
  try {
40
40
  content = await readFile(file, 'utf-8');
41
41
  }
42
- catch {
42
+ catch (err) {
43
+ process.stderr.write(`Error: failed to read ${file}: ${err.message}\n`);
43
44
  continue;
44
45
  }
45
46
  const lines = content.split('\n');
@@ -16,8 +16,9 @@ export function readConfig() {
16
16
  try {
17
17
  return JSON.parse(readFileSync(configPath, 'utf-8'));
18
18
  }
19
- catch {
20
- return {};
19
+ catch (err) {
20
+ process.stderr.write(`Error: failed to parse config ${configPath}: ${err.message}\n`);
21
+ process.exit(1);
21
22
  }
22
23
  }
23
24
  export function writeConfig(config) {
@@ -0,0 +1,21 @@
1
+ export type Styler = {
2
+ bold: (s: string) => string;
3
+ dim: (s: string) => string;
4
+ red: (s: string) => string;
5
+ cyan: (s: string) => string;
6
+ white: (s: string) => string;
7
+ green: (s: string) => string;
8
+ yellow: (s: string) => string;
9
+ boldWhite: (s: string) => string;
10
+ };
11
+ export declare const plainStyler: Styler;
12
+ export type CmdContext = {
13
+ latDir: string;
14
+ projectRoot: string;
15
+ styler: Styler;
16
+ mode: 'cli' | 'mcp';
17
+ };
18
+ export type CmdResult = {
19
+ output: string;
20
+ isError?: boolean;
21
+ };
@@ -0,0 +1,11 @@
1
+ const identity = (s) => s;
2
+ export const plainStyler = {
3
+ bold: identity,
4
+ dim: identity,
5
+ red: identity,
6
+ cyan: identity,
7
+ white: identity,
8
+ green: identity,
9
+ yellow: identity,
10
+ boldWhite: identity,
11
+ };
@@ -1,6 +1,7 @@
1
1
  import type { Section, SectionMatch } from './lattice.js';
2
- export declare function formatSectionId(id: string): string;
3
- export declare function formatSectionPreview(section: Section, latticeDir: string, opts?: {
2
+ import type { CmdContext, Styler } from './context.js';
3
+ export declare function formatSectionId(id: string, s: Styler): string;
4
+ export declare function formatSectionPreview(ctx: CmdContext, section: Section, opts?: {
4
5
  reason?: string;
5
6
  }): string;
6
- export declare function formatResultList(header: string, matches: SectionMatch[], latticeDir: string): string;
7
+ export declare function formatResultList(ctx: CmdContext, header: string, matches: SectionMatch[]): string;
@@ -1,35 +1,31 @@
1
- import { relative } from 'node:path';
2
- import chalk from 'chalk';
3
- export function formatSectionId(id) {
1
+ import { join, relative } from 'node:path';
2
+ export function formatSectionId(id, s) {
4
3
  const parts = id.split('#');
5
4
  return parts.length === 1
6
- ? chalk.bold.white(parts[0])
7
- : chalk.dim(parts.slice(0, -1).join('#') + '#') +
8
- chalk.bold.white(parts[parts.length - 1]);
5
+ ? s.boldWhite(parts[0])
6
+ : s.dim(parts.slice(0, -1).join('#') + '#') +
7
+ s.boldWhite(parts[parts.length - 1]);
9
8
  }
10
- export function formatSectionPreview(section, latticeDir, opts) {
11
- const relPath = relative(process.cwd(), latticeDir + '/' + section.file + '.md');
9
+ export function formatSectionPreview(ctx, section, opts) {
10
+ const s = ctx.styler;
11
+ const relPath = relative(process.cwd(), join(ctx.projectRoot, section.filePath));
12
12
  const kind = section.id.includes('#') ? 'Section' : 'File';
13
- const reasonSuffix = opts?.reason ? ' ' + chalk.dim(`(${opts.reason})`) : '';
13
+ const reasonSuffix = opts?.reason ? ' ' + s.dim(`(${opts.reason})`) : '';
14
14
  const lines = [
15
- `${chalk.dim('*')} ${chalk.dim(kind + ':')} [[${formatSectionId(section.id)}]]${reasonSuffix}`,
16
- ` ${chalk.dim('Defined in')} ${chalk.cyan(relPath)}${chalk.dim(`:${section.startLine}-${section.endLine}`)}`,
15
+ `${s.dim('*')} ${s.dim(kind + ':')} [[${formatSectionId(section.id, s)}]]${reasonSuffix}`,
16
+ ` ${s.dim('Defined in')} ${s.cyan(relPath)}${s.dim(`:${section.startLine}-${section.endLine}`)}`,
17
17
  ];
18
- if (section.body) {
19
- const truncated = section.body.length > 200
20
- ? section.body.slice(0, 200) + '...'
21
- : section.body;
22
- lines.push('');
23
- lines.push(` ${chalk.dim('>')} ${truncated}`);
18
+ if (section.firstParagraph) {
19
+ lines.push('', ` ${s.dim('>')} ${section.firstParagraph}`);
24
20
  }
25
21
  return lines.join('\n');
26
22
  }
27
- export function formatResultList(header, matches, latticeDir) {
28
- const lines = ['', chalk.bold(header), ''];
23
+ export function formatResultList(ctx, header, matches) {
24
+ const lines = ['', ctx.styler.bold(header), ''];
29
25
  for (let i = 0; i < matches.length; i++) {
30
26
  if (i > 0)
31
27
  lines.push('');
32
- lines.push(formatSectionPreview(matches[i].section, latticeDir, {
28
+ lines.push(formatSectionPreview(ctx, matches[i].section, {
33
29
  reason: matches[i].reason,
34
30
  }));
35
31
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Bump this number whenever `lat init` setup changes in a way that
3
+ * requires users to re-run it (e.g. new hooks, AGENTS.md changes,
4
+ * MCP config changes).
5
+ */
6
+ export declare const INIT_VERSION = 1;
7
+ export declare function readInitVersion(latDir: string): number | null;
8
+ export declare function readFileHash(latDir: string, relPath: string): string | null;
9
+ export declare function contentHash(content: string): string;
10
+ export declare function writeInitMeta(latDir: string, fileHashes: Record<string, string>): void;
@@ -0,0 +1,49 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Bump this number whenever `lat init` setup changes in a way that
6
+ * requires users to re-run it (e.g. new hooks, AGENTS.md changes,
7
+ * MCP config changes).
8
+ */
9
+ export const INIT_VERSION = 1;
10
+ function cachePath(latDir) {
11
+ return join(latDir, '.cache', 'lat_init.json');
12
+ }
13
+ function readMeta(latDir) {
14
+ const p = cachePath(latDir);
15
+ if (!existsSync(p))
16
+ return null;
17
+ try {
18
+ return JSON.parse(readFileSync(p, 'utf-8'));
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function readInitVersion(latDir) {
25
+ const meta = readMeta(latDir);
26
+ if (!meta)
27
+ return null;
28
+ return typeof meta.init_version === 'number' ? meta.init_version : null;
29
+ }
30
+ export function readFileHash(latDir, relPath) {
31
+ const meta = readMeta(latDir);
32
+ return meta?.file_hashes?.[relPath] ?? null;
33
+ }
34
+ export function contentHash(content) {
35
+ return createHash('sha256').update(content).digest('hex');
36
+ }
37
+ export function writeInitMeta(latDir, fileHashes) {
38
+ const cacheDir = join(latDir, '.cache');
39
+ mkdirSync(cacheDir, { recursive: true });
40
+ // Merge with existing hashes so we don't lose entries from agents
41
+ // that weren't selected this run
42
+ const existing = readMeta(latDir);
43
+ const mergedHashes = { ...existing?.file_hashes, ...fileHashes };
44
+ const data = {
45
+ init_version: INIT_VERSION,
46
+ file_hashes: mergedHashes,
47
+ };
48
+ writeFileSync(cachePath(latDir), JSON.stringify(data, null, 2) + '\n');
49
+ }
@@ -3,10 +3,11 @@ export type Section = {
3
3
  heading: string;
4
4
  depth: number;
5
5
  file: string;
6
+ filePath: string;
6
7
  children: Section[];
7
8
  startLine: number;
8
9
  endLine: number;
9
- body: string;
10
+ firstParagraph: string;
10
11
  };
11
12
  export type Ref = {
12
13
  target: string;
@@ -20,13 +21,18 @@ export type LatFrontmatter = {
20
21
  export declare function parseFrontmatter(content: string): LatFrontmatter;
21
22
  export declare function stripFrontmatter(content: string): string;
22
23
  export declare function findLatticeDir(from?: string): string | null;
24
+ export declare function findProjectRoot(from?: string): string | null;
23
25
  export declare function listLatticeFiles(latticeDir: string): Promise<string[]>;
24
- export declare function parseSections(filePath: string, content: string, latticeDir?: string): Section[];
26
+ export declare function parseSections(filePath: string, content: string, projectRoot?: string): Section[];
25
27
  export declare function loadAllSections(latticeDir: string): Promise<Section[]>;
26
28
  export declare function flattenSections(sections: Section[]): Section[];
27
29
  /**
28
- * Build an index mapping bare file stems to their full vault-relative paths.
29
- * Used by resolveRef to allow short references when a stem is unambiguous.
30
+ * Build an index mapping path suffixes to their full vault-relative paths.
31
+ * Used by resolveRef to allow short references when a suffix is unambiguous.
32
+ *
33
+ * For a file like `lat.md/guides/setup`, indexes both `guides/setup` and `setup`.
34
+ * This ensures backward-compatible short refs after the vault root moved to the
35
+ * project root (so section IDs now include the `lat.md/` prefix).
30
36
  */
31
37
  export declare function buildFileIndex(sections: Section[]): Map<string, string[]>;
32
38
  export type ResolveResult = {
@@ -50,4 +56,4 @@ export type SectionMatch = {
50
56
  reason: string;
51
57
  };
52
58
  export declare function findSections(sections: Section[], query: string): SectionMatch[];
53
- export declare function extractRefs(filePath: string, content: string, latticeDir?: string): Ref[];
59
+ export declare function extractRefs(filePath: string, content: string, projectRoot?: string): Ref[];