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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -8
  3. package/dist/src/cli/check.d.ts +18 -0
  4. package/dist/src/cli/check.js +122 -0
  5. package/dist/src/cli/context.d.ts +10 -0
  6. package/dist/src/cli/context.js +14 -0
  7. package/dist/src/cli/gen.d.ts +2 -0
  8. package/dist/src/cli/gen.js +14 -0
  9. package/dist/src/cli/index.js +126 -17
  10. package/dist/src/cli/init.d.ts +1 -0
  11. package/dist/src/cli/init.js +67 -0
  12. package/dist/src/cli/locate.d.ts +2 -1
  13. package/dist/src/cli/locate.js +8 -20
  14. package/dist/src/cli/prompt.d.ts +2 -0
  15. package/dist/src/cli/prompt.js +62 -0
  16. package/dist/src/cli/refs.d.ts +4 -1
  17. package/dist/src/cli/refs.js +36 -109
  18. package/dist/src/cli/search.d.ts +5 -0
  19. package/dist/src/cli/search.js +55 -0
  20. package/dist/src/cli/templates.d.ts +1 -0
  21. package/dist/src/cli/templates.js +15 -0
  22. package/dist/src/code-refs.d.ts +13 -0
  23. package/dist/src/code-refs.js +63 -0
  24. package/dist/src/format.d.ts +7 -1
  25. package/dist/src/format.js +26 -4
  26. package/dist/src/lattice.d.ts +6 -0
  27. package/dist/src/lattice.js +98 -11
  28. package/dist/src/search/db.d.ts +4 -0
  29. package/dist/src/search/db.js +31 -0
  30. package/dist/src/search/embeddings.d.ts +2 -0
  31. package/dist/src/search/embeddings.js +25 -0
  32. package/dist/src/search/index.d.ts +9 -0
  33. package/dist/src/search/index.js +66 -0
  34. package/dist/src/search/provider.d.ts +8 -0
  35. package/dist/src/search/provider.js +40 -0
  36. package/dist/src/search/search.d.ts +9 -0
  37. package/dist/src/search/search.js +17 -0
  38. package/package.json +28 -4
  39. package/templates/AGENTS.md +56 -0
  40. package/templates/README +1 -0
  41. package/templates/init/README.md +5 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yury Selivanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,16 +1,66 @@
1
- # Lattice
1
+ # lat.md
2
2
 
3
- Anchor source code to high-level concepts defined in markdown.
4
- See [lattice.md](lattice.md) for the full spec.
3
+ [![CI](https://github.com/1st1/lat.md/actions/workflows/ci.yml/badge.svg)](https://github.com/1st1/lat.md/actions/workflows/ci.yml)
4
+
5
+ A knowledge graph for your codebase, written in markdown.
6
+
7
+ ## The problem
8
+
9
+ `AGENTS.md` doesn't scale. A single flat file can describe a small project, but as a codebase grows, maintaining one monolithic document becomes impractical. Key design decisions get buried, business logic goes undocumented, and agents hallucinate context they should be able to look up.
10
+
11
+ ## The idea
12
+
13
+ Compress the knowledge about your program domain into a **graph** — a set of interconnected markdown files that live in a `lat.md/` directory at the root of your project. Sections link to each other with `[[wiki links]]`, source files link back with `// @lat:` comments, and `lat check` ensures nothing drifts out of sync.
14
+
15
+ The result is a structured knowledge base that:
16
+
17
+ - 📈 **Scales** — split knowledge across as many files and sections as you need
18
+ - 🔗 **Cross-references** — wiki links (`[[cli#search#Indexing]]`) connect concepts into a navigable graph
19
+ - ✅ **Stays in sync** — `lat check` validates that all links resolve and that required code references exist
20
+ - 🔍 **Is searchable** — exact, fuzzy, and semantic (vector) search across all sections
21
+ - 🤝 **Works for humans and machines** — readable in any editor (or Obsidian), queryable by agents via the `lat` CLI
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install -g lat.md
27
+ ```
28
+
29
+ Or use directly with `npx lat.md <command>`.
30
+
31
+ ## How it works
32
+
33
+ Run `lat init` to scaffold a `lat.md/` directory, then write markdown files describing your architecture, business logic, test specs — whatever matters. Link between sections using `[[file#Section#Subsection]]` syntax. Annotate source code with `// @lat: [[section-id]]` (or `# @lat: [[section-id]]` in Python) comments to tie implementation back to concepts.
34
+
35
+ ```
36
+ my-project/
37
+ ├── lat.md/
38
+ │ ├── architecture.md # system design, key decisions
39
+ │ ├── auth.md # authentication & authorization logic
40
+ │ └── tests.md # test specs (require-code-mention: true)
41
+ ├── src/
42
+ │ ├── auth.ts # // @lat: [[auth#OAuth Flow]]
43
+ │ └── server.ts # // @lat: [[architecture#Request Pipeline]]
44
+ └── ...
45
+ ```
46
+
47
+ ## CLI
48
+
49
+ ```bash
50
+ npx lat.md init # scaffold a lat.md/ directory
51
+ npx lat.md check # validate all wiki links and code refs
52
+ npx lat.md locate "OAuth Flow" # find sections by name (exact, fuzzy)
53
+ npx lat.md refs "auth#OAuth Flow" # find what references a section
54
+ npx lat.md search "how do we auth?" # semantic search via embeddings
55
+ npx lat.md prompt "fix [[OAuth Flow]]" # expand [[refs]] in a prompt for agents
56
+ ```
5
57
 
6
58
  ## Development
7
59
 
8
- Requires Node.js 22+.
60
+ Requires Node.js 22+ and pnpm.
9
61
 
10
62
  ```bash
11
63
  pnpm install
12
- pnpm test # run tests once
13
- pnpm test:watch # run tests in watch mode
14
- pnpm typecheck # tsc --noEmit
15
- pnpm format # prettier
64
+ pnpm build
65
+ pnpm test
16
66
  ```
@@ -0,0 +1,18 @@
1
+ import type { CliContext } from './context.js';
2
+ export type CheckError = {
3
+ file: string;
4
+ line: number;
5
+ target: string;
6
+ message: string;
7
+ };
8
+ /** File counts grouped by extension (e.g. { ".ts": 5, ".py": 2 }). */
9
+ export type FileStats = Record<string, number>;
10
+ export type CheckResult = {
11
+ errors: CheckError[];
12
+ files: FileStats;
13
+ };
14
+ export declare function checkMd(latticeDir: string): Promise<CheckResult>;
15
+ export declare function checkCodeRefs(latticeDir: string): Promise<CheckResult>;
16
+ export declare function checkMdCmd(ctx: CliContext): Promise<void>;
17
+ export declare function checkCodeRefsCmd(ctx: CliContext): Promise<void>;
18
+ export declare function checkAllCmd(ctx: CliContext): Promise<void>;
@@ -0,0 +1,122 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname, join, relative } from 'node:path';
3
+ import { listLatticeFiles, loadAllSections, extractRefs, flattenSections, parseFrontmatter, parseSections, } from '../lattice.js';
4
+ import { scanCodeRefs } from '../code-refs.js';
5
+ function countByExt(paths) {
6
+ const stats = {};
7
+ for (const p of paths) {
8
+ const ext = extname(p) || '(no ext)';
9
+ stats[ext] = (stats[ext] || 0) + 1;
10
+ }
11
+ return stats;
12
+ }
13
+ export async function checkMd(latticeDir) {
14
+ const files = await listLatticeFiles(latticeDir);
15
+ const allSections = await loadAllSections(latticeDir);
16
+ const flat = flattenSections(allSections);
17
+ const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
18
+ const errors = [];
19
+ for (const file of files) {
20
+ const content = await readFile(file, 'utf-8');
21
+ const refs = extractRefs(file, content);
22
+ const relPath = relative(process.cwd(), file);
23
+ for (const ref of refs) {
24
+ const target = ref.target.toLowerCase();
25
+ if (!sectionIds.has(target)) {
26
+ errors.push({
27
+ file: relPath,
28
+ line: ref.line,
29
+ target: ref.target,
30
+ message: `broken link [[${ref.target}]] — no matching section found`,
31
+ });
32
+ }
33
+ }
34
+ }
35
+ return { errors, files: countByExt(files) };
36
+ }
37
+ export async function checkCodeRefs(latticeDir) {
38
+ const projectRoot = join(latticeDir, '..');
39
+ const allSections = await loadAllSections(latticeDir);
40
+ const flat = flattenSections(allSections);
41
+ const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
42
+ const scan = await scanCodeRefs(projectRoot);
43
+ const errors = [];
44
+ const mentionedSections = new Set();
45
+ for (const ref of scan.refs) {
46
+ const target = ref.target.toLowerCase();
47
+ mentionedSections.add(target);
48
+ if (!sectionIds.has(target)) {
49
+ errors.push({
50
+ file: ref.file,
51
+ line: ref.line,
52
+ target: ref.target,
53
+ message: `@lat: [[${ref.target}]] — no matching section found`,
54
+ });
55
+ }
56
+ }
57
+ const files = await listLatticeFiles(latticeDir);
58
+ for (const file of files) {
59
+ const content = await readFile(file, 'utf-8');
60
+ const fm = parseFrontmatter(content);
61
+ if (!fm.requireCodeMention)
62
+ continue;
63
+ const sections = parseSections(file, content);
64
+ const fileSections = flattenSections(sections);
65
+ const leafSections = fileSections.filter((s) => s.children.length === 0);
66
+ const relPath = relative(process.cwd(), file);
67
+ for (const leaf of leafSections) {
68
+ if (!mentionedSections.has(leaf.id.toLowerCase())) {
69
+ errors.push({
70
+ file: relPath,
71
+ line: leaf.startLine,
72
+ target: leaf.id,
73
+ message: `section "${leaf.id}" requires a code mention but none found`,
74
+ });
75
+ }
76
+ }
77
+ }
78
+ return { errors, files: countByExt(scan.files) };
79
+ }
80
+ function formatErrors(ctx, errors) {
81
+ for (const err of errors) {
82
+ console.error(`${ctx.chalk.cyan(err.file + ':' + err.line)}: ${ctx.chalk.red(err.message)}`);
83
+ }
84
+ if (errors.length > 0) {
85
+ console.error(ctx.chalk.red(`\n${errors.length} error${errors.length === 1 ? '' : 's'} found`));
86
+ }
87
+ }
88
+ function formatStats(ctx, stats) {
89
+ const entries = Object.entries(stats).sort(([a], [b]) => a.localeCompare(b));
90
+ const parts = entries.map(([ext, n]) => `${n} ${ext}`);
91
+ console.log(ctx.chalk.dim(`Scanned ${parts.join(', ')}`));
92
+ }
93
+ export async function checkMdCmd(ctx) {
94
+ const { errors, files } = await checkMd(ctx.latDir);
95
+ formatStats(ctx, files);
96
+ formatErrors(ctx, errors);
97
+ if (errors.length > 0)
98
+ process.exit(1);
99
+ console.log(ctx.chalk.green('md: All links OK'));
100
+ }
101
+ export async function checkCodeRefsCmd(ctx) {
102
+ const { errors, files } = await checkCodeRefs(ctx.latDir);
103
+ formatStats(ctx, files);
104
+ formatErrors(ctx, errors);
105
+ if (errors.length > 0)
106
+ process.exit(1);
107
+ console.log(ctx.chalk.green('code-refs: All references OK'));
108
+ }
109
+ export async function checkAllCmd(ctx) {
110
+ const md = await checkMd(ctx.latDir);
111
+ const code = await checkCodeRefs(ctx.latDir);
112
+ const allErrors = [...md.errors, ...code.errors];
113
+ const allFiles = { ...md.files };
114
+ for (const [ext, n] of Object.entries(code.files)) {
115
+ allFiles[ext] = (allFiles[ext] || 0) + n;
116
+ }
117
+ formatStats(ctx, allFiles);
118
+ formatErrors(ctx, allErrors);
119
+ if (allErrors.length > 0)
120
+ process.exit(1);
121
+ console.log(ctx.chalk.green('All checks passed'));
122
+ }
@@ -0,0 +1,10 @@
1
+ import { type ChalkInstance } from 'chalk';
2
+ export type CliContext = {
3
+ latDir: string;
4
+ color: boolean;
5
+ chalk: ChalkInstance;
6
+ };
7
+ export declare function resolveContext(opts: {
8
+ dir?: string;
9
+ color?: boolean;
10
+ }): CliContext;
@@ -0,0 +1,14 @@
1
+ import chalk from 'chalk';
2
+ import { findLatticeDir } from '../lattice.js';
3
+ export function resolveContext(opts) {
4
+ const color = opts.color !== false;
5
+ if (!color) {
6
+ chalk.level = 0;
7
+ }
8
+ const latDir = findLatticeDir(opts.dir) ?? '';
9
+ if (!latDir) {
10
+ console.error(chalk.red('No lat.md directory found'));
11
+ process.exit(1);
12
+ }
13
+ return { latDir, color, chalk };
14
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readAgentsTemplate(): string;
2
+ export declare function genCmd(target: string): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { findTemplatesDir } from './templates.js';
4
+ export function readAgentsTemplate() {
5
+ return readFileSync(join(findTemplatesDir(), 'AGENTS.md'), 'utf-8');
6
+ }
7
+ export async function genCmd(target) {
8
+ const normalized = target.toLowerCase();
9
+ if (normalized !== 'agents.md' && normalized !== 'claude.md') {
10
+ console.error(`Unknown target: ${target}. Supported: agents.md, claude.md`);
11
+ process.exit(1);
12
+ }
13
+ process.stdout.write(readAgentsTemplate());
14
+ }
@@ -1,19 +1,128 @@
1
1
  #!/usr/bin/env node
2
- import { locate } from './locate.js';
3
- import { refs } from './refs.js';
4
- const args = process.argv.slice(2);
5
- const command = args[0];
6
- const commands = {
7
- locate,
8
- refs,
9
- };
10
- const handler = commands[command];
11
- if (!handler) {
12
- console.error(`Usage: lat <command>
13
-
14
- Commands:
15
- locate <query> Find sections by id
16
- refs <query> [--scope=md|code|md+code] Find references to a section`);
17
- process.exit(1);
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Command } from 'commander';
6
+ import { resolveContext } from './context.js';
7
+ import { locateCmd } from './locate.js';
8
+ import { refsCmd } from './refs.js';
9
+ function findPackageJson() {
10
+ let dir = dirname(fileURLToPath(import.meta.url));
11
+ while (true) {
12
+ const candidate = join(dir, 'package.json');
13
+ try {
14
+ return JSON.parse(readFileSync(candidate, 'utf-8')).version;
15
+ }
16
+ catch { }
17
+ const parent = dirname(dir);
18
+ if (parent === dir)
19
+ return '0.0.0';
20
+ dir = parent;
21
+ }
18
22
  }
19
- await handler(args.slice(1));
23
+ const version = findPackageJson();
24
+ const program = new Command();
25
+ program
26
+ .name('lat')
27
+ .description('Anchor source code to high-level concepts defined in markdown')
28
+ .version(version)
29
+ .option('--dir <path>', 'project root to look for lat.md in (default: cwd)')
30
+ .option('--no-color', 'disable color output');
31
+ program
32
+ .command('locate')
33
+ .description('Find sections by id')
34
+ .argument('<query>', 'section id to search for')
35
+ .action(async (query) => {
36
+ const ctx = resolveContext(program.opts());
37
+ await locateCmd(ctx, query);
38
+ });
39
+ program
40
+ .command('refs')
41
+ .description('Find references to a section')
42
+ .argument('<query>', 'section id to find references for')
43
+ .option('--scope <scope>', 'where to search: md, code, or md+code', 'md')
44
+ .action(async (query, opts) => {
45
+ const scope = opts.scope;
46
+ if (scope !== 'md' && scope !== 'code' && scope !== 'md+code') {
47
+ console.error(`Unknown scope: ${scope}. Use md, code, or md+code.`);
48
+ process.exit(1);
49
+ }
50
+ const ctx = resolveContext(program.opts());
51
+ await refsCmd(ctx, query, scope);
52
+ });
53
+ const check = program
54
+ .command('check')
55
+ .description('Validate links and code references')
56
+ .action(async () => {
57
+ const ctx = resolveContext(program.opts());
58
+ const { checkAllCmd } = await import('./check.js');
59
+ await checkAllCmd(ctx);
60
+ });
61
+ check
62
+ .command('md')
63
+ .description('Validate wiki links in lat.md markdown files')
64
+ .action(async () => {
65
+ const ctx = resolveContext(program.opts());
66
+ const { checkMdCmd } = await import('./check.js');
67
+ await checkMdCmd(ctx);
68
+ });
69
+ check
70
+ .command('code-refs')
71
+ .description('Validate @lat code references and coverage')
72
+ .action(async () => {
73
+ const ctx = resolveContext(program.opts());
74
+ const { checkCodeRefsCmd } = await import('./check.js');
75
+ await checkCodeRefsCmd(ctx);
76
+ });
77
+ program
78
+ .command('prompt')
79
+ .description('Expand [[refs]] in a prompt to lat.md section locations')
80
+ .argument('[text]', 'prompt text')
81
+ .option('--stdin', 'read prompt from stdin')
82
+ .action(async (text, opts) => {
83
+ if (opts.stdin) {
84
+ const chunks = [];
85
+ for await (const chunk of process.stdin) {
86
+ chunks.push(chunk);
87
+ }
88
+ text = Buffer.concat(chunks).toString('utf-8');
89
+ }
90
+ if (!text) {
91
+ console.error('Provide prompt text as an argument or use --stdin');
92
+ process.exit(1);
93
+ }
94
+ const ctx = resolveContext(program.opts());
95
+ const { promptCmd } = await import('./prompt.js');
96
+ await promptCmd(ctx, text);
97
+ });
98
+ program
99
+ .command('search')
100
+ .description('Semantic search across lat.md sections')
101
+ .argument('[query]', 'search query in plain English')
102
+ .option('--limit <n>', 'max results', '5')
103
+ .option('--reindex', 'force full re-indexing')
104
+ .action(async (query, opts) => {
105
+ const ctx = resolveContext(program.opts());
106
+ const { searchCmd } = await import('./search.js');
107
+ await searchCmd(ctx, query, {
108
+ limit: parseInt(opts.limit),
109
+ reindex: opts.reindex,
110
+ });
111
+ });
112
+ program
113
+ .command('gen')
114
+ .description('Generate a file to stdout (agents.md, claude.md)')
115
+ .argument('<target>', 'file to generate: agents.md or claude.md')
116
+ .action(async (target) => {
117
+ const { genCmd } = await import('./gen.js');
118
+ await genCmd(target);
119
+ });
120
+ program
121
+ .command('init')
122
+ .description('Initialize a lat.md directory')
123
+ .argument('[dir]', 'target directory (default: cwd)')
124
+ .action(async (dir) => {
125
+ const { initCmd } = await import('./init.js');
126
+ await initCmd(dir);
127
+ });
128
+ await program.parseAsync();
@@ -0,0 +1 @@
1
+ export declare function initCmd(targetDir?: string): Promise<void>;
@@ -0,0 +1,67 @@
1
+ import { existsSync, cpSync, mkdirSync, writeFileSync, symlinkSync, } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import chalk from 'chalk';
5
+ import { findTemplatesDir } from './templates.js';
6
+ import { readAgentsTemplate } from './gen.js';
7
+ async function confirm(rl, message) {
8
+ try {
9
+ const answer = await rl.question(`${message} ${chalk.dim('[Y/n]')} `);
10
+ return answer.trim().toLowerCase() !== 'n';
11
+ }
12
+ catch {
13
+ return true;
14
+ }
15
+ }
16
+ export async function initCmd(targetDir) {
17
+ const root = resolve(targetDir ?? process.cwd());
18
+ const latDir = join(root, 'lat.md');
19
+ const interactive = process.stdin.isTTY ?? false;
20
+ const rl = interactive
21
+ ? createInterface({ input: process.stdin, output: process.stdout })
22
+ : null;
23
+ const ask = async (message) => {
24
+ if (!rl)
25
+ return true;
26
+ return confirm(rl, message);
27
+ };
28
+ try {
29
+ // Step 1: lat.md/ directory
30
+ if (existsSync(latDir)) {
31
+ console.log(chalk.green('lat.md/') + ' already exists');
32
+ }
33
+ else {
34
+ if (!(await ask('Create lat.md/ directory?'))) {
35
+ console.log('Aborted.');
36
+ return;
37
+ }
38
+ const templateDir = join(findTemplatesDir(), 'init');
39
+ mkdirSync(latDir, { recursive: true });
40
+ cpSync(templateDir, latDir, { recursive: true });
41
+ console.log(chalk.green('Created lat.md/'));
42
+ }
43
+ // Step 2: AGENTS.md / CLAUDE.md
44
+ const agentsPath = join(root, 'AGENTS.md');
45
+ const claudePath = join(root, 'CLAUDE.md');
46
+ const hasAgents = existsSync(agentsPath);
47
+ const hasClaude = existsSync(claudePath);
48
+ if (!hasAgents && !hasClaude) {
49
+ if (await ask('Generate AGENTS.md and CLAUDE.md with lat.md instructions for coding agents?')) {
50
+ const template = readAgentsTemplate();
51
+ writeFileSync(agentsPath, template);
52
+ symlinkSync('AGENTS.md', claudePath);
53
+ console.log(chalk.green('Created AGENTS.md and CLAUDE.md → AGENTS.md'));
54
+ }
55
+ }
56
+ else {
57
+ const existing = [hasAgents && 'AGENTS.md', hasClaude && 'CLAUDE.md']
58
+ .filter(Boolean)
59
+ .join(' and ');
60
+ console.log(`\n${existing} already exists. Run ${chalk.cyan('lat gen agents.md')} to preview the template,` +
61
+ ` then incorporate its content or overwrite as needed.`);
62
+ }
63
+ }
64
+ finally {
65
+ rl?.close();
66
+ }
67
+ }
@@ -1 +1,2 @@
1
- export declare function locate(args: string[]): Promise<void>;
1
+ import type { CliContext } from './context.js';
2
+ export declare function locateCmd(ctx: CliContext, query: string): Promise<void>;
@@ -1,25 +1,13 @@
1
- import { findLatticeDir, loadAllSections, findSections } from '../lattice.js';
2
- import { formatSectionPreview } from '../format.js';
3
- export async function locate(args) {
4
- if (args.length < 1) {
5
- console.error('Usage: lat locate <query>');
6
- process.exit(1);
7
- }
8
- const query = args[0];
9
- const latticeDir = findLatticeDir();
10
- if (!latticeDir) {
11
- console.error('No .lattice directory found');
12
- process.exit(1);
13
- }
14
- const sections = await loadAllSections(latticeDir);
1
+ import { loadAllSections, findSections } from '../lattice.js';
2
+ import { formatResultList } from '../format.js';
3
+ export async function locateCmd(ctx, query) {
4
+ const sections = await loadAllSections(ctx.latDir);
15
5
  const matches = findSections(sections, query);
16
6
  if (matches.length === 0) {
17
- console.error(`No sections matching "${query}"`);
7
+ console.error(ctx.chalk.red(`No sections matching "${query}" (no exact, substring, or fuzzy matches)`));
18
8
  process.exit(1);
19
9
  }
20
- for (let i = 0; i < matches.length; i++) {
21
- if (i > 0)
22
- console.log('');
23
- console.log(formatSectionPreview(matches[i], latticeDir));
24
- }
10
+ console.log(formatResultList(`Sections matching "${query}":`, matches, ctx.latDir, {
11
+ numbered: true,
12
+ }));
25
13
  }
@@ -0,0 +1,2 @@
1
+ import type { CliContext } from './context.js';
2
+ export declare function promptCmd(ctx: CliContext, text: string): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import { relative } from 'node:path';
2
+ import { loadAllSections, findSections, flattenSections, } from '../lattice.js';
3
+ const WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g;
4
+ function formatContext(section, latDir) {
5
+ const relPath = relative(process.cwd(), latDir + '/' + section.file + '.md');
6
+ const loc = `${relPath}:${section.startLine}-${section.endLine}`;
7
+ let text = `[${section.id}](${loc})`;
8
+ if (section.body) {
9
+ text += `: ${section.body}`;
10
+ }
11
+ return text;
12
+ }
13
+ export async function promptCmd(ctx, text) {
14
+ const allSections = await loadAllSections(ctx.latDir);
15
+ const flat = flattenSections(allSections);
16
+ const refs = [...text.matchAll(WIKI_LINK_RE)];
17
+ if (refs.length === 0) {
18
+ process.stdout.write(text);
19
+ return;
20
+ }
21
+ const resolved = new Map();
22
+ for (const match of refs) {
23
+ const target = match[1];
24
+ if (resolved.has(target))
25
+ continue;
26
+ const q = target.toLowerCase();
27
+ const exact = flat.find((s) => s.id.toLowerCase() === q);
28
+ if (exact) {
29
+ resolved.set(target, exact);
30
+ continue;
31
+ }
32
+ const fuzzy = findSections(allSections, target);
33
+ if (fuzzy.length === 1) {
34
+ resolved.set(target, fuzzy[0]);
35
+ continue;
36
+ }
37
+ if (fuzzy.length > 1) {
38
+ console.error(ctx.chalk.red(`Ambiguous reference [[${target}]].`));
39
+ console.error(ctx.chalk.dim('\nCould match:\n'));
40
+ for (const m of fuzzy) {
41
+ console.error(' ' + m.id);
42
+ }
43
+ console.error(ctx.chalk.dim('\nAsk the user which section they meant.'));
44
+ process.exit(1);
45
+ }
46
+ console.error(ctx.chalk.red(`No section found for [[${target}]] (no exact, substring, or fuzzy matches).`));
47
+ console.error(ctx.chalk.dim('Ask the user to correct the reference.'));
48
+ process.exit(1);
49
+ }
50
+ // Replace [[refs]] inline
51
+ let output = text.replace(WIKI_LINK_RE, (_match, target) => {
52
+ const section = resolved.get(target);
53
+ return `[[${section.id}]]`;
54
+ });
55
+ // Append context block
56
+ output += '\n\n<lat-context>\n';
57
+ for (const section of resolved.values()) {
58
+ output += formatContext(section, ctx.latDir) + '\n';
59
+ }
60
+ output += '</lat-context>\n';
61
+ process.stdout.write(output);
62
+ }
@@ -1 +1,4 @@
1
- export declare function refs(args: string[]): Promise<void>;
1
+ import type { CliContext } from './context.js';
2
+ type Scope = 'md' | 'code' | 'md+code';
3
+ export declare function refsCmd(ctx: CliContext, query: string, scope: Scope): Promise<void>;
4
+ export {};