Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ If `lat search` fails because no API key is configured, explain to the user that

- **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`).
- **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`.
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist.
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). Can also link to folders (`[[src/components]]`) or files with any extension (`[[schema.sql]]`) — these are validated to exist. Symbol references (`#`) only work for supported extensions. `lat check` validates these exist.
- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts

# Test specs
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ If `lat search` fails because no API key is configured, explain to the user that

- **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`).
- **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`.
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist.
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). Can also link to folders (`[[src/components]]`) or files with any extension (`[[schema.sql]]`) — these are validated to exist. Symbol references (`#`) only work for supported extensions. `lat check` validates these exist.
- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts

# Test specs
Expand Down
16 changes: 12 additions & 4 deletions lat.md/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,25 @@ Resolution is handled by [[src/lattice.ts#resolveRef]]. See [[parser#Short Ref R

### Source Code Links

Wiki links can reference symbols in TypeScript, JavaScript, Python, Rust, Go, and C source files:
Wiki links can point to any file or folder in the project, and — for supported languages — to individual symbols within a source file.

**Symbol targets** (use `#` to select a symbol inside a file):

- **`[[src/config.ts#getConfigDir]]`** — the `getConfigDir` function in `src/config.ts`
- **`[[src/server.ts#App#listen]]`** — the `listen` method on class `App` in `src/server.ts`
- **`[[src/server.ts#App#listen]]`** — the `listen` method on class `App`
- **`[[src/lib.rs#Greeter#greet]]`** — the `greet` method on struct `Greeter` in Rust
- **`[[src/app.go#Greeter#Greet]]`** — the `Greet` method on type `Greeter` in Go
- **`[[src/app.h#Greeter]]`** — the `Greeter` struct in a C header
- **`[[src/app.h#Greeter#prefix]]`** — the `prefix` field of struct `Greeter` in C
- **`[[src/config.ts]]`** — link to the file itself (no symbol)

Supported extensions: `.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.rs`, `.go`, `.c`, `.h`.
**Path targets** (no `#` — any file or folder in the project):

- **`[[src/config.ts]]`** — a source file, linked without picking a symbol
- **`[[src/schema.sql]]`** — a file with any extension
- **`[[docs/CHANGELOG]]`** — a file with no extension
- **`[[src/components]]`** — a folder

`lat check` verifies path targets exist on disk and symbol targets resolve to a real definition. Symbol targets (`#`) are only supported for these extensions: `.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.rs`, `.go`, `.c`, `.h`. Using `#` with any other extension (e.g. `[[schema.sql#foo]]`) is an error.

Python symbols: functions, classes, methods, module-level variables. Decorated definitions (`@decorator`) are unwrapped transparently — `[[file.py#my_func]]` resolves whether or not `my_func` has decorators, and `# @lat:` comments placed between decorators and the `def`/`class` line are scanned normally.

Expand Down
35 changes: 24 additions & 11 deletions src/cli/check.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { basename, dirname, extname, join, relative } from 'node:path';
import { scanCodeRefs } from '../code-refs.js';
import type { CmdContext, CmdResult, Styler } from '../context.js';
import { INIT_VERSION, readInitVersion } from '../init-version.js';
import {
listLatticeFiles,
loadAllSections,
buildFileIndex,
extractRefs,
flattenSections,
listLatticeFiles,
loadAllSections,
parseFrontmatter,
parseSections,
buildFileIndex,
resolveRef,
type Section,
} from '../lattice.js';
import { scanCodeRefs } from '../code-refs.js';
import { SOURCE_EXTENSIONS, clearSymbolCache } from '../source-parser.js';
import { walkEntries } from '../walk.js';
import type { CmdContext, CmdResult, Styler } from '../context.js';
import { INIT_VERSION, readInitVersion } from '../init-version.js';

export type CheckError = {
file: string;
Expand Down Expand Up @@ -92,14 +91,28 @@ async function tryResolveSourceRef(
projectRoot: string,
): Promise<string | null> {
if (!isSourcePath(target)) {
// Check if it looks like a file path with an unsupported extension
const hashIdx = target.indexOf('#');
const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
const ext = extname(filePart);
if (ext && hashIdx !== -1) {

const targetHasHash = hashIdx !== -1;

// Unsupported extension with # — can't validate symbols
if (ext && targetHasHash) {
const supported = [...SOURCE_EXTENSIONS].sort().join(', ');
return `broken link [[${target}]] — unsupported file extension "${ext}". Supported: ${supported}`;
return `broken link [[${target}]] — unsupported file extension "${ext}". Symbol references (#) only supported for: ${supported}`;
}

// File or folder without # — validate existence
if (!targetHasHash) {
const absPath = join(projectRoot, filePart);
if (existsSync(absPath)) {
return null;
}

return `broken link [[${target}]] — file or folder "${filePart}" not found`;
}

return `broken link [[${target}]] — no matching section found`;
}

Expand Down
2 changes: 1 addition & 1 deletion templates/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ If `lat search` fails because no API key is configured, explain to the user that

- **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`).
- **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`.
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist.
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). Can also link to folders (`[[src/components]]`) or files with any extension (`[[schema.sql]]`) — these are validated to exist. Symbol references (`#`) only work for supported extensions. `lat check` validates these exist.
- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts

# Test specs
Expand Down
2 changes: 1 addition & 1 deletion templates/cursor-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ If `lat_search` fails because `LAT_LLM_KEY` is not set, explain to the user that

- **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`).
- **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`.
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist.
- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). Can also link to folders (`[[src/components]]`) or files with any extension (`[[schema.sql]]`) — these are validated to exist. Symbol references (`#`) only work for supported extensions. `lat check` validates these exist.
- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts

# Test specs
Expand Down
4 changes: 3 additions & 1 deletion templates/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@ Reference functions, classes, constants, and methods in source files:
[[src/lib.rs#Greeter#greet]] — Rust impl method
[[src/app.go#Greeter#Greet]] — Go method
[[src/app.h#Greeter]] — C struct
[[src/components]] — folder (existence check)
[[schema.sql]] — any file extension (existence check)
```

`lat check` validates that all targets exist.
Symbol references (`#`) only work for supported extensions. File-only and folder links work with any path. `lat check` validates that all targets exist.

## Code refs

Expand Down
51 changes: 48 additions & 3 deletions tests/cases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,15 +658,15 @@ describe('error-bare-heading-ref', () => {
const { errors } = await checkMd(lat);
const bare = errors.find((e) => e.target === 'Installation');
expect(bare).toBeDefined();
expect(bare!.message).toContain('no matching section found');
expect(bare!.message).toContain('not found');
});

// @lat: [[ref-resolution#Local section syntax in md is error]]
it('check md rejects [[#Heading]] local section syntax', async () => {
const { errors } = await checkMd(lat);
const local = errors.find((e) => e.target === '#Configuration');
expect(local).toBeDefined();
expect(local!.message).toContain('no matching section found');
expect(local!.message).toContain('broken link');
});

// @lat: [[ref-resolution#Nonexistent file ref in md is error]]
Expand Down Expand Up @@ -951,13 +951,58 @@ describe('error-source-ref-unsupported-ext', () => {
expect(errors).toHaveLength(1);
expect(errors[0].target).toBe('src/app.blah#spam');
expect(errors[0].message).toContain('unsupported file extension ".blah"');
expect(errors[0].message).toContain('Supported:');
expect(errors[0].message).toContain('Symbol references (#) only supported for:');
expect(errors[0].message).toContain('.ts');
expect(errors[0].message).toContain('.rs');
expect(errors[0].message).toContain('.go');
});
});

describe('source-ref-folder-valid', () => {
it('check md accepts wiki link to existing folder', async () => {
const { errors } = await checkMd(latDir('source-ref-folder-valid'));
expect(errors).toHaveLength(0);
});
});

describe('error-source-ref-bad-folder', () => {
it('check md reports broken link for nonexistent folder and folder with symbol ref', async () => {
const { errors } = await checkMd(latDir('error-source-ref-bad-folder'));
expect(errors).toHaveLength(2);
const byTarget = new Map(errors.map((e) => [e.target, e]));
expect(byTarget.get('src/nonexistent')!.message).toContain('file or folder "src/nonexistent" not found');
expect(byTarget.get('src/components#something')!.message).toContain('no matching section found');
});
});

describe('source-ref-unsupported-ext-valid', () => {
it('check md accepts wiki link to existing file with unsupported extension', async () => {
const { errors } = await checkMd(latDir('source-ref-unsupported-ext-valid'));
expect(errors).toHaveLength(0);
});
});

describe('source-ref-unsupported-ext-root-valid', () => {
it('check md accepts wiki link to root-level file with unsupported extension', async () => {
const { errors } = await checkMd(latDir('source-ref-unsupported-ext-root-valid'));
expect(errors).toHaveLength(0);
});
});

describe('error-source-ref-unsupported-ext-missing', () => {
it('check md reports broken link for missing unsupported-ext files and folders', async () => {
const { errors } = await checkMd(latDir('error-source-ref-unsupported-ext-missing'));
expect(errors).toHaveLength(3);
const targets = errors.map((e) => e.target);
expect(targets).toContain('nope.sql');
expect(targets).toContain('src/missing.sql');
expect(targets).toContain('src/missing/');
for (const e of errors) {
expect(e.message).toContain('not found');
}
});
});

// --- source file refs ---

describe('source-file-refs', () => {
Expand Down
5 changes: 5 additions & 0 deletions tests/cases/error-source-ref-bad-folder/lat.md/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Docs

Links to a nonexistent folder: [[src/nonexistent]].

Folder with symbol ref: [[src/components#something]].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. I do not understand.

If I have just a folder src/components, what does #something refer to? Don't I need a file with #something in it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an error case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Thanks for the explanation.

Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Docs

Missing root file: [[nope.sql]].

Missing pathed file: [[src/missing.sql]].

Missing pathed folder: [[src/missing/]].
3 changes: 3 additions & 0 deletions tests/cases/source-ref-folder-valid/lat.md/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Docs

Links to a folder: [[src/components]] and [[src/pages/]].
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Docs

Links to a root-level SQL file: [[schema.sql]].
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE users (id INTEGER PRIMARY KEY);
3 changes: 3 additions & 0 deletions tests/cases/source-ref-unsupported-ext-valid/lat.md/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Docs

Links to a SQL file: [[src/schema.sql]].
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE users (id INTEGER PRIMARY KEY);