lat.md 0.9.0 → 0.10.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/README.md CHANGED
@@ -1,9 +1,13 @@
1
- # lat.md
1
+ <p align="center">
2
+ <img src="templates/logo-dark.svg" alt="lat.md" width="500">
3
+ </p>
2
4
 
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
- [![npm](https://img.shields.io/npm/v/lat.md)](https://www.npmjs.com/package/lat.md)
5
+ <p align="center">
6
+ <a href="https://github.com/1st1/lat.md/actions/workflows/ci.yml"><img src="https://github.com/1st1/lat.md/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
7
+ <a href="https://www.npmjs.com/package/lat.md"><img src="https://img.shields.io/npm/v/lat.md" alt="npm"></a>
8
+ </p>
5
9
 
6
- A knowledge graph for your codebase, written in markdown.
10
+ <p align="center">A knowledge graph for your codebase, written in markdown.</p>
7
11
 
8
12
  ## The problem
9
13
 
@@ -27,9 +31,7 @@ The result is a structured knowledge base that:
27
31
  npm install -g lat.md
28
32
  ```
29
33
 
30
- Or use directly with `npx lat.md@latest <command>`.
31
-
32
- After installing, run `lat init` in the repo you want to use lat in.
34
+ Then run `lat init` in the repo you want to use lat in.
33
35
 
34
36
  ## How it works
35
37
 
@@ -50,13 +52,14 @@ my-project/
50
52
  ## CLI
51
53
 
52
54
  ```bash
53
- npx lat.md init # scaffold a lat.md/ directory
54
- npx lat.md check # validate all wiki links and code refs
55
- npx lat.md locate "OAuth Flow" # find sections by name (exact, fuzzy)
56
- npx lat.md section "auth#OAuth Flow" # show a section with its links and refs
57
- npx lat.md refs "auth#OAuth Flow" # find what references a section
58
- npx lat.md search "how do we auth?" # semantic search via embeddings
59
- npx lat.md expand "fix [[OAuth Flow]]" # expand [[refs]] in a prompt for agents
55
+ lat init # scaffold a lat.md/ directory
56
+ lat check # validate all wiki links and code refs
57
+ lat locate "OAuth Flow" # find sections by name (exact, fuzzy)
58
+ lat section "auth#OAuth Flow" # show a section with its links and refs
59
+ lat refs "auth#OAuth Flow" # find what references a section
60
+ lat search "how do we auth?" # semantic search via embeddings
61
+ lat expand "fix [[OAuth Flow]]" # expand [[refs]] in a prompt for agents
62
+ lat mcp # start MCP server for editor integration
60
63
  ```
61
64
 
62
65
  ## Configuration
@@ -131,9 +131,10 @@ export async function checkCodeRefs(latticeDir) {
131
131
  for (const ref of scan.refs) {
132
132
  const { resolved, ambiguous, suggested } = resolveRef(ref.target, sectionIds, fileIndex);
133
133
  mentionedSections.add(resolved.toLowerCase());
134
+ const displayPath = relative(process.cwd(), join(projectRoot, ref.file));
134
135
  if (ambiguous) {
135
136
  errors.push({
136
- file: ref.file,
137
+ file: displayPath,
137
138
  line: ref.line,
138
139
  target: ref.target,
139
140
  message: ambiguousMessage(ref.target, ambiguous, suggested),
@@ -141,7 +142,7 @@ export async function checkCodeRefs(latticeDir) {
141
142
  }
142
143
  else if (!sectionIds.has(resolved.toLowerCase())) {
143
144
  errors.push({
144
- file: ref.file,
145
+ file: displayPath,
145
146
  line: ref.line,
146
147
  target: ref.target,
147
148
  message: `@lat: [[${ref.target}]] — no matching section found`,
@@ -371,17 +372,22 @@ function formatErrorCount(count, s) {
371
372
  }
372
373
  // --- Unified command functions ---
373
374
  export async function checkAllCommand(ctx) {
375
+ const startTime = Date.now();
374
376
  const md = await checkMd(ctx.latDir);
375
377
  const code = await checkCodeRefs(ctx.latDir);
376
378
  const indexErrors = await checkIndex(ctx.latDir);
377
379
  const sectionErrors = await checkSections(ctx.latDir);
380
+ const elapsed = Date.now() - startTime;
378
381
  const allErrors = [...md.errors, ...code.errors];
379
382
  const allFiles = { ...md.files };
380
383
  for (const [ext, n] of Object.entries(code.files)) {
381
384
  allFiles[ext] = (allFiles[ext] || 0) + n;
382
385
  }
383
386
  const s = ctx.styler;
384
- const lines = [formatFileStats(allFiles, s)];
387
+ const elapsedStr = elapsed < 1000 ? `${elapsed}ms` : `${(elapsed / 1000).toFixed(1)}s`;
388
+ const lines = [
389
+ formatFileStats(allFiles, s) + s.dim(` in ${elapsedStr}`),
390
+ ];
385
391
  // Init version warning first — user should fix setup before addressing errors
386
392
  const storedVersion = readInitVersion(ctx.latDir);
387
393
  if (storedVersion === null) {
@@ -424,6 +430,17 @@ export async function checkAllCommand(ctx) {
424
430
  s.cyan('lat init') +
425
431
  ' to configure.');
426
432
  }
433
+ // Suggest ripgrep if check was slow (>1s) and rg is not available
434
+ if (elapsed > 1000) {
435
+ const { hasRipgrep } = await import('../code-refs.js');
436
+ if (!(await hasRipgrep())) {
437
+ lines.push(s.yellow('Tip:') +
438
+ ' Install ' +
439
+ s.cyan('ripgrep') +
440
+ ' (rg) for faster code scanning.' +
441
+ ' See https://github.com/BurntSushi/ripgrep#installation');
442
+ }
443
+ }
427
444
  return { output: lines.join('\n') };
428
445
  }
429
446
  export async function checkMdCommand(ctx) {
@@ -61,7 +61,7 @@ program
61
61
  .command('refs')
62
62
  .description('Find references to a section')
63
63
  .argument('<query>', 'section id to find references for')
64
- .option('--scope <scope>', 'where to search: md, code, or md+code', 'md')
64
+ .option('--scope <scope>', 'where to search: md, code, or md+code', 'md+code')
65
65
  .action(async (query, opts) => {
66
66
  const scope = opts.scope;
67
67
  if (scope !== 'md' && scope !== 'code' && scope !== 'md+code') {
@@ -682,6 +682,17 @@ export async function initCmd(targetDir) {
682
682
  ' Run ' +
683
683
  chalk.cyan('lat check') +
684
684
  ' to validate your setup.');
685
+ // Suggest ripgrep if not available
686
+ const { hasRipgrep } = await import('../code-refs.js');
687
+ if (!(await hasRipgrep())) {
688
+ console.log('');
689
+ console.log(chalk.yellow('Tip:') +
690
+ ' Install ' +
691
+ chalk.cyan('ripgrep') +
692
+ ' (rg) for faster code scanning.' +
693
+ ' See ' +
694
+ chalk.underline('https://github.com/BurntSushi/ripgrep#installation'));
695
+ }
685
696
  }
686
697
  finally {
687
698
  rl?.close();
@@ -13,8 +13,10 @@ export type RefsError = {
13
13
  };
14
14
  export type RefsResult = RefsFound | RefsError;
15
15
  /**
16
- * Find all sections and code locations that reference a given section.
17
- * Accepts any valid section id (full-path, short-form, with or without brackets).
16
+ * Find all sections and code locations that reference a given section or
17
+ * source file. Accepts section ids (full-path, short-form) and source file
18
+ * paths (e.g. src/app.rs#foo). Source file queries match wiki links directly
19
+ * without section resolution.
18
20
  */
19
21
  export declare function findRefs(ctx: CmdContext, query: string, scope: Scope): Promise<RefsResult>;
20
22
  export declare function refsCommand(ctx: CmdContext, query: string, scope: Scope): Promise<CmdResult>;
@@ -1,13 +1,135 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { extname, join, relative } from 'node:path';
2
4
  import { listLatticeFiles, loadAllSections, findSections, extractRefs, flattenSections, buildFileIndex, resolveRef, } from '../lattice.js';
3
5
  import { formatResultList } from '../format.js';
4
6
  import { scanCodeRefs } from '../code-refs.js';
7
+ /** Extensions recognized as source code for ref queries. */
8
+ const SOURCE_EXTS = new Set([
9
+ '.ts',
10
+ '.tsx',
11
+ '.js',
12
+ '.jsx',
13
+ '.py',
14
+ '.rs',
15
+ '.go',
16
+ '.c',
17
+ '.h',
18
+ ]);
5
19
  /**
6
- * Find all sections and code locations that reference a given section.
7
- * Accepts any valid section id (full-path, short-form, with or without brackets).
20
+ * Check if a query looks like a source file path (has a recognized extension
21
+ * and the file exists on disk).
22
+ */
23
+ function isSourceQuery(query, projectRoot) {
24
+ const hashIdx = query.indexOf('#');
25
+ const filePart = hashIdx === -1 ? query : query.slice(0, hashIdx);
26
+ const symbolPart = hashIdx === -1 ? '' : query.slice(hashIdx + 1);
27
+ const ext = extname(filePart);
28
+ if (!SOURCE_EXTS.has(ext))
29
+ return null;
30
+ if (!existsSync(join(projectRoot, filePart)))
31
+ return null;
32
+ return { filePart, symbolPart };
33
+ }
34
+ /**
35
+ * Find references to a source file or symbol across lat.md and code files.
36
+ * For file-level queries (no #symbol), matches all wiki links targeting
37
+ * that file or any symbol in it.
38
+ */
39
+ async function findSourceRefs(latDir, projectRoot, query, scope) {
40
+ const hashIdx = query.indexOf('#');
41
+ const filePart = hashIdx === -1 ? query : query.slice(0, hashIdx);
42
+ const isFileLevel = hashIdx === -1;
43
+ const queryLower = query.toLowerCase();
44
+ const fileLower = filePart.toLowerCase();
45
+ // Build a synthetic Section for the target
46
+ const target = {
47
+ id: query,
48
+ heading: hashIdx === -1 ? filePart : query.slice(hashIdx + 1),
49
+ depth: 0,
50
+ file: filePart,
51
+ filePath: filePart,
52
+ children: [],
53
+ startLine: 0,
54
+ endLine: 0,
55
+ firstParagraph: '',
56
+ };
57
+ // Try to get real line info from the source parser
58
+ try {
59
+ const { resolveSourceSymbol } = await import('../source-parser.js');
60
+ if (hashIdx !== -1) {
61
+ const symbolPart = query.slice(hashIdx + 1);
62
+ const { found, symbols } = await resolveSourceSymbol(filePart, symbolPart, projectRoot);
63
+ if (found) {
64
+ const parts = symbolPart.split('#');
65
+ const sym = symbols.find((s) => parts.length === 1
66
+ ? s.name === parts[0] && !s.parent
67
+ : s.name === parts[1] && s.parent === parts[0]);
68
+ if (sym) {
69
+ target.startLine = sym.startLine;
70
+ target.endLine = sym.endLine;
71
+ target.firstParagraph = sym.signature;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ // source parser unavailable — proceed without line info
78
+ }
79
+ const allSections = await loadAllSections(latDir);
80
+ const flat = flattenSections(allSections);
81
+ const mdRefs = [];
82
+ const codeRefs = [];
83
+ if (scope === 'md' || scope === 'md+code') {
84
+ const files = await listLatticeFiles(latDir);
85
+ const matchingFromSections = new Set();
86
+ for (const file of files) {
87
+ const content = await readFile(file, 'utf-8');
88
+ const fileRefs = extractRefs(file, content, projectRoot);
89
+ for (const ref of fileRefs) {
90
+ const targetLower = ref.target.toLowerCase();
91
+ const matches = isFileLevel
92
+ ? targetLower === fileLower || targetLower.startsWith(fileLower + '#')
93
+ : targetLower === queryLower;
94
+ if (matches) {
95
+ matchingFromSections.add(ref.fromSection.toLowerCase());
96
+ }
97
+ }
98
+ }
99
+ if (matchingFromSections.size > 0) {
100
+ const referrers = flat.filter((s) => matchingFromSections.has(s.id.toLowerCase()));
101
+ for (const s of referrers) {
102
+ mdRefs.push({ section: s, reason: 'wiki link' });
103
+ }
104
+ }
105
+ }
106
+ if (scope === 'code' || scope === 'md+code') {
107
+ const { refs: scannedRefs } = await scanCodeRefs(projectRoot);
108
+ for (const ref of scannedRefs) {
109
+ const targetLower = ref.target.toLowerCase();
110
+ const matches = isFileLevel
111
+ ? targetLower === fileLower || targetLower.startsWith(fileLower + '#')
112
+ : targetLower === queryLower;
113
+ if (matches) {
114
+ const displayPath = relative(process.cwd(), join(projectRoot, ref.file));
115
+ codeRefs.push(`${displayPath}:${ref.line}`);
116
+ }
117
+ }
118
+ }
119
+ return { kind: 'found', target, mdRefs, codeRefs };
120
+ }
121
+ /**
122
+ * Find all sections and code locations that reference a given section or
123
+ * source file. Accepts section ids (full-path, short-form) and source file
124
+ * paths (e.g. src/app.rs#foo). Source file queries match wiki links directly
125
+ * without section resolution.
8
126
  */
9
127
  export async function findRefs(ctx, query, scope) {
10
128
  query = query.replace(/^\[\[|\]\]$/g, '');
129
+ // Source file queries bypass section resolution
130
+ if (isSourceQuery(query, ctx.projectRoot)) {
131
+ return findSourceRefs(ctx.latDir, ctx.projectRoot, query, scope);
132
+ }
11
133
  const allSections = await loadAllSections(ctx.latDir);
12
134
  const flat = flattenSections(allSections);
13
135
  const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
@@ -58,7 +180,8 @@ export async function findRefs(ctx, query, scope) {
58
180
  for (const ref of scannedRefs) {
59
181
  const { resolved: codeResolved } = resolveRef(ref.target, sectionIds, fileIndex);
60
182
  if (codeResolved.toLowerCase() === targetId) {
61
- codeRefs.push(`${ref.file}:${ref.line}`);
183
+ const displayPath = relative(process.cwd(), join(ctx.projectRoot, ref.file));
184
+ codeRefs.push(`${displayPath}:${ref.line}`);
62
185
  }
63
186
  }
64
187
  }
@@ -1,5 +1,16 @@
1
1
  import { type Section, type SectionMatch } from '../lattice.js';
2
2
  import type { CmdContext, CmdResult } from '../context.js';
3
+ export type CodeBackRef = {
4
+ file: string;
5
+ line: number;
6
+ snippet: string;
7
+ };
8
+ export type SourceRef = {
9
+ target: string;
10
+ file: string;
11
+ line: number;
12
+ snippet: string;
13
+ };
3
14
  export type SectionFound = {
4
15
  kind: 'found';
5
16
  section: Section;
@@ -8,7 +19,9 @@ export type SectionFound = {
8
19
  target: string;
9
20
  resolved: Section;
10
21
  }[];
22
+ outgoingSourceRefs: SourceRef[];
11
23
  incomingRefs: SectionMatch[];
24
+ codeRefs: CodeBackRef[];
12
25
  };
13
26
  export type SectionResult = SectionFound | {
14
27
  kind: 'no-match';
@@ -1,6 +1,8 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { join, relative } from 'node:path';
2
+ import { extname, join, relative } from 'node:path';
3
3
  import { loadAllSections, findSections, flattenSections, extractRefs, buildFileIndex, resolveRef, listLatticeFiles, } from '../lattice.js';
4
+ import { scanCodeRefs } from '../code-refs.js';
5
+ import { SOURCE_EXTENSIONS, resolveSourceSymbol } from '../source-parser.js';
4
6
  import { formatSectionId, formatNavHints } from '../format.js';
5
7
  /**
6
8
  * Look up a section by id, return its content, outgoing wiki link targets,
@@ -35,10 +37,53 @@ export async function getSection(ctx, query) {
35
37
  const sectionRefs = extractRefs(absPath, fileContent, ctx.projectRoot);
36
38
  const sectionId = section.id.toLowerCase();
37
39
  const outgoingRefs = [];
40
+ const outgoingSourceRefs = [];
38
41
  const seen = new Set();
39
42
  for (const ref of sectionRefs) {
40
43
  if (ref.fromSection.toLowerCase() !== sectionId)
41
44
  continue;
45
+ // Detect source code references by file extension
46
+ const hashIdx = ref.target.indexOf('#');
47
+ const filePart = hashIdx === -1 ? ref.target : ref.target.slice(0, hashIdx);
48
+ const ext = extname(filePart);
49
+ if (SOURCE_EXTENSIONS.has(ext)) {
50
+ const targetLower = ref.target.toLowerCase();
51
+ if (!seen.has(targetLower)) {
52
+ seen.add(targetLower);
53
+ const symbolPart = hashIdx === -1 ? '' : ref.target.slice(hashIdx + 1);
54
+ let line = 0;
55
+ let snippet = '';
56
+ if (symbolPart) {
57
+ const { found, symbols } = await resolveSourceSymbol(filePart, symbolPart, ctx.projectRoot);
58
+ if (found) {
59
+ const parts = symbolPart.split('#');
60
+ const sym = symbols.find((s) => parts.length === 1
61
+ ? s.name === parts[0] && !s.parent
62
+ : s.name === parts[1] && s.parent === parts[0]);
63
+ if (sym) {
64
+ line = sym.startLine;
65
+ try {
66
+ const src = await readFile(join(ctx.projectRoot, filePart), 'utf-8');
67
+ const srcLines = src.split('\n');
68
+ const start = sym.startLine - 1;
69
+ const end = Math.min(srcLines.length, start + 5);
70
+ snippet = srcLines.slice(start, end).join('\n');
71
+ }
72
+ catch {
73
+ // file unreadable
74
+ }
75
+ }
76
+ }
77
+ }
78
+ outgoingSourceRefs.push({
79
+ target: ref.target,
80
+ file: filePart,
81
+ line,
82
+ snippet,
83
+ });
84
+ }
85
+ continue;
86
+ }
42
87
  const { resolved } = resolveRef(ref.target, sectionIds, fileIndex);
43
88
  const resolvedLower = resolved.toLowerCase();
44
89
  if (seen.has(resolvedLower))
@@ -70,7 +115,36 @@ export async function getSection(ctx, query) {
70
115
  }
71
116
  }
72
117
  }
73
- return { kind: 'found', section, content, outgoingRefs, incomingRefs };
118
+ // Find code back-references: @lat: comments pointing to this section
119
+ const codeRefs = [];
120
+ const { refs: scannedRefs } = await scanCodeRefs(ctx.projectRoot);
121
+ for (const ref of scannedRefs) {
122
+ const { resolved: codeResolved } = resolveRef(ref.target, sectionIds, fileIndex);
123
+ if (codeResolved.toLowerCase() === sectionId) {
124
+ const absFile = join(ctx.projectRoot, ref.file);
125
+ let snippet = '';
126
+ try {
127
+ const src = await readFile(absFile, 'utf-8');
128
+ const srcLines = src.split('\n');
129
+ const start = Math.max(0, ref.line - 1 - 2);
130
+ const end = Math.min(srcLines.length, ref.line - 1 + 3);
131
+ snippet = srcLines.slice(start, end).join('\n');
132
+ }
133
+ catch {
134
+ // file unreadable — skip snippet
135
+ }
136
+ codeRefs.push({ file: ref.file, line: ref.line, snippet });
137
+ }
138
+ }
139
+ return {
140
+ kind: 'found',
141
+ section,
142
+ content,
143
+ outgoingRefs,
144
+ outgoingSourceRefs,
145
+ incomingRefs,
146
+ codeRefs,
147
+ };
74
148
  }
75
149
  function fullEndLine(section) {
76
150
  if (section.children.length === 0)
@@ -85,7 +159,7 @@ function truncate(s, max) {
85
159
  */
86
160
  export function formatSectionOutput(ctx, result) {
87
161
  const s = ctx.styler;
88
- const { section, content, outgoingRefs, incomingRefs } = result;
162
+ const { section, content, outgoingRefs, outgoingSourceRefs, incomingRefs, codeRefs, } = result;
89
163
  const relPath = relative(process.cwd(), join(ctx.projectRoot, section.filePath));
90
164
  const loc = `${s.cyan(relPath)}${s.dim(`:${section.startLine}-${section.endLine}`)}`;
91
165
  const quoted = content
@@ -97,7 +171,7 @@ export function formatSectionOutput(ctx, result) {
97
171
  '',
98
172
  quoted,
99
173
  ];
100
- if (outgoingRefs.length > 0) {
174
+ if (outgoingRefs.length > 0 || outgoingSourceRefs.length > 0) {
101
175
  parts.push('', '## This section references:', '');
102
176
  for (const ref of outgoingRefs) {
103
177
  const body = ref.resolved.firstParagraph
@@ -105,6 +179,18 @@ export function formatSectionOutput(ctx, result) {
105
179
  : '';
106
180
  parts.push(`${s.dim('*')} [[${formatSectionId(ref.resolved.id, s)}]]${body}`);
107
181
  }
182
+ for (const ref of outgoingSourceRefs) {
183
+ const loc = ref.line
184
+ ? `${s.dim(` (${ref.file}:${ref.line})`)}`
185
+ : `${s.dim(` (${ref.file})`)}`;
186
+ parts.push(`${s.dim('*')} [[${s.cyan(ref.target)}]]${loc}`);
187
+ if (ref.snippet) {
188
+ const snippetLines = ref.snippet.split('\n');
189
+ for (const line of snippetLines) {
190
+ parts.push(` ${s.dim('|')} ${line}`);
191
+ }
192
+ }
193
+ }
108
194
  }
109
195
  if (incomingRefs.length > 0) {
110
196
  parts.push('', '## Referenced by:', '');
@@ -115,6 +201,19 @@ export function formatSectionOutput(ctx, result) {
115
201
  parts.push(`${s.dim('*')} [[${formatSectionId(ref.section.id, s)}]]${body}`);
116
202
  }
117
203
  }
204
+ if (codeRefs.length > 0) {
205
+ parts.push('', '## Referenced by code:', '');
206
+ for (const ref of codeRefs) {
207
+ const codeRelPath = relative(process.cwd(), join(ctx.projectRoot, ref.file));
208
+ parts.push(`${s.dim('*')} ${s.cyan(codeRelPath)}${s.dim(`:${ref.line}`)}`);
209
+ if (ref.snippet) {
210
+ const snippetLines = ref.snippet.split('\n');
211
+ for (const line of snippetLines) {
212
+ parts.push(` ${s.dim('|')} ${line}`);
213
+ }
214
+ }
215
+ }
216
+ }
118
217
  parts.push(formatNavHints(ctx));
119
218
  return parts.join('\n');
120
219
  }
@@ -10,5 +10,8 @@ export type CodeRef = {
10
10
  export type ScanResult = {
11
11
  refs: CodeRef[];
12
12
  files: string[];
13
+ usedRg: boolean;
13
14
  };
15
+ /** Check whether ripgrep (`rg`) is available on PATH. */
16
+ export declare function hasRipgrep(): Promise<boolean>;
14
17
  export declare function scanCodeRefs(projectRoot: string): Promise<ScanResult>;
@@ -1,6 +1,11 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { execFile } from 'node:child_process';
2
3
  import { join, relative } from 'node:path';
3
4
  import { walkEntries } from './walk.js';
5
+ /** Glob patterns used to exclude directories/files from code-ref scanning.
6
+ * Shared between rg args and the TS fallback's walkFiles filter. */
7
+ const EXCLUDE_DIRS = ['lat.md', '.claude'];
8
+ const EXCLUDE_GLOBS = ['*.md'];
4
9
  /** Walk project files for code-ref scanning. Uses walkEntries for .gitignore
5
10
  * support, then additionally skips .md files, lat.md/, .claude/, and sub-projects. */
6
11
  export async function walkFiles(dir) {
@@ -31,8 +36,141 @@ export const LAT_REF_RE = re('gv') `
31
36
  ( [^\]]+ )
32
37
  \]\]
33
38
  `;
34
- export async function scanCodeRefs(projectRoot) {
35
- const files = await walkFiles(projectRoot);
39
+ /**
40
+ * Run an external command and return stdout, or null if the command is not found
41
+ * or fails.
42
+ */
43
+ function tryExec(cmd, args, cwd) {
44
+ return new Promise((resolve) => {
45
+ execFile(cmd, args, { cwd, maxBuffer: 50 * 1024 * 1024 }, (err, out) => {
46
+ if (err) {
47
+ // Exit code 1 with no stderr typically means "no matches" for grep/rg
48
+ const exitCode = err.code;
49
+ if (exitCode === 'ENOENT') {
50
+ resolve(null); // command not found
51
+ return;
52
+ }
53
+ // rg/grep exit 1 = no matches (not an error)
54
+ if ('status' in err &&
55
+ err.status === 1 &&
56
+ out === '') {
57
+ resolve('');
58
+ return;
59
+ }
60
+ resolve(null);
61
+ return;
62
+ }
63
+ resolve(out);
64
+ });
65
+ });
66
+ }
67
+ /**
68
+ * Detect sub-projects (directories containing their own lat.md/) using
69
+ * rg --files. Finds files inside nested lat.md/ dirs and extracts the parent
70
+ * directory paths. Returns paths relative to projectRoot.
71
+ */
72
+ async function findSubProjects(projectRoot) {
73
+ // List files inside any lat.md/ dir, then extract unique parent paths.
74
+ // The root lat.md/ is excluded by EXCLUDE_DIRS in the caller, so we only
75
+ // need to find nested ones here — search for files under */lat.md/.
76
+ const out = await tryExec('rg', ['--files', '--glob', '**/lat.md/**', '.'], projectRoot);
77
+ if (!out)
78
+ return [];
79
+ const subProjects = new Set();
80
+ for (const line of out.split('\n')) {
81
+ if (!line)
82
+ continue;
83
+ const clean = line.startsWith('./') ? line.slice(2) : line;
84
+ // "tests/cases/foo/lat.md/specs.md" → "tests/cases/foo"
85
+ // Skip root lat.md/ (no parent prefix — starts with "lat.md/")
86
+ const idx = clean.indexOf('/lat.md/');
87
+ if (idx !== -1)
88
+ subProjects.add(clean.slice(0, idx));
89
+ }
90
+ return [...subProjects];
91
+ }
92
+ /** Build rg glob exclusion args. */
93
+ function rgExcludeArgs(subProjects) {
94
+ const args = [];
95
+ for (const dir of EXCLUDE_DIRS)
96
+ args.push('--glob', `!${dir}/`);
97
+ for (const glob of EXCLUDE_GLOBS)
98
+ args.push('--glob', `!${glob}`);
99
+ for (const sp of subProjects)
100
+ args.push('--glob', `!${sp}/`);
101
+ return args;
102
+ }
103
+ /**
104
+ * Try scanning with ripgrep. Returns parsed refs and scanned file list, or null
105
+ * if rg is not available. rg respects .gitignore by default; we add glob
106
+ * exclusions for lat.md/, .claude/, *.md files, and sub-projects.
107
+ */
108
+ async function tryRipgrep(projectRoot) {
109
+ // Detect sub-projects first so we can exclude them from all rg calls
110
+ const subProjects = await findSubProjects(projectRoot);
111
+ const excludes = rgExcludeArgs(subProjects);
112
+ // Search for @lat refs
113
+ const searchArgs = [
114
+ '--no-heading',
115
+ '--line-number',
116
+ '--with-filename',
117
+ ...excludes,
118
+ '@lat:.*\\[\\[',
119
+ '.',
120
+ ];
121
+ const out = await tryExec('rg', searchArgs, projectRoot);
122
+ if (out === null)
123
+ return null;
124
+ const { refs } = parseGrepOutput(out, projectRoot);
125
+ // List all scanned files (for stats) — rg --files is fast
126
+ const filesOut = await tryExec('rg', ['--files', ...excludes, '.'], projectRoot);
127
+ const files = (filesOut || '')
128
+ .split('\n')
129
+ .filter(Boolean)
130
+ .map((f) => {
131
+ const clean = f.startsWith('./') ? f.slice(2) : f;
132
+ return join(projectRoot, clean);
133
+ });
134
+ return { refs, files };
135
+ }
136
+ /**
137
+ * Parse rg output lines (file:line:content) into CodeRef entries.
138
+ */
139
+ function parseGrepOutput(output, projectRoot) {
140
+ const refs = [];
141
+ if (!output.trim())
142
+ return { refs };
143
+ for (const line of output.split('\n')) {
144
+ if (!line)
145
+ continue;
146
+ // Format: ./path/to/file:linenum:content
147
+ const firstColon = line.indexOf(':');
148
+ if (firstColon === -1)
149
+ continue;
150
+ const secondColon = line.indexOf(':', firstColon + 1);
151
+ if (secondColon === -1)
152
+ continue;
153
+ let filePath = line.slice(0, firstColon);
154
+ const lineNum = parseInt(line.slice(firstColon + 1, secondColon), 10);
155
+ const content = line.slice(secondColon + 1);
156
+ if (isNaN(lineNum))
157
+ continue;
158
+ // Strip leading ./ from path
159
+ if (filePath.startsWith('./'))
160
+ filePath = filePath.slice(2);
161
+ // Extract targets using the same regex as the TS fallback
162
+ LAT_REF_RE.lastIndex = 0;
163
+ let match;
164
+ while ((match = LAT_REF_RE.exec(content)) !== null) {
165
+ refs.push({ target: match[1], file: filePath, line: lineNum });
166
+ }
167
+ }
168
+ return { refs };
169
+ }
170
+ /**
171
+ * TypeScript fallback: read every file and scan for @lat refs.
172
+ */
173
+ async function scanWithTs(files, projectRoot) {
36
174
  const refs = [];
37
175
  for (const file of files) {
38
176
  let content;
@@ -50,11 +188,30 @@ export async function scanCodeRefs(projectRoot) {
50
188
  while ((match = LAT_REF_RE.exec(lines[i])) !== null) {
51
189
  refs.push({
52
190
  target: match[1],
53
- file: relative(process.cwd(), file),
191
+ file: relative(projectRoot, file),
54
192
  line: i + 1,
55
193
  });
56
194
  }
57
195
  }
58
196
  }
59
- return { refs, files };
197
+ return refs;
198
+ }
199
+ /** Check whether ripgrep (`rg`) is available on PATH. */
200
+ export async function hasRipgrep() {
201
+ const result = await tryExec('rg', ['--version'], '.');
202
+ return result !== null;
203
+ }
204
+ export async function scanCodeRefs(projectRoot) {
205
+ // Fast path: use rg for both searching and file listing
206
+ // _LAT_DISABLE_RG is a test-only escape hatch to force the TS fallback
207
+ if (process.env._LAT_DISABLE_RG !== '1') {
208
+ const rgResult = await tryRipgrep(projectRoot);
209
+ if (rgResult !== null) {
210
+ return { refs: rgResult.refs, files: rgResult.files, usedRg: true };
211
+ }
212
+ }
213
+ // Fallback: walk files ourselves and scan with TS
214
+ const files = await walkFiles(projectRoot);
215
+ const refs = await scanWithTs(files, projectRoot);
216
+ return { refs, files, usedRg: false };
60
217
  }
@@ -50,7 +50,7 @@ export async function startMcpServer() {
50
50
  scope: z
51
51
  .enum(['md', 'code', 'md+code'])
52
52
  .optional()
53
- .default('md')
53
+ .default('md+code')
54
54
  .describe('Where to search: md, code, or md+code'),
55
55
  }, async ({ query, scope }) => toMcp(await refsCommand(ctx, query, scope)));
56
56
  const transport = new StdioServerTransport();
@@ -173,33 +173,43 @@ function extractPySymbols(tree) {
173
173
  const node = root.child(i);
174
174
  const startLine = node.startPosition.row + 1;
175
175
  const endLine = node.endPosition.row + 1;
176
- if (node.type === 'function_definition') {
177
- const name = extractName(node);
176
+ // Unwrap decorated_definition to get the inner function/class
177
+ const inner = node.type === 'decorated_definition'
178
+ ? node.childForFieldName('definition')
179
+ : node;
180
+ if (!inner)
181
+ continue;
182
+ if (inner.type === 'function_definition') {
183
+ const name = extractName(inner);
178
184
  if (name) {
179
185
  symbols.push({
180
186
  name,
181
187
  kind: 'function',
182
188
  startLine,
183
189
  endLine,
184
- signature: firstLine(node.text),
190
+ signature: firstLine(inner.text),
185
191
  });
186
192
  }
187
193
  }
188
- else if (node.type === 'class_definition') {
189
- const name = extractName(node);
194
+ else if (inner.type === 'class_definition') {
195
+ const name = extractName(inner);
190
196
  if (name) {
191
197
  symbols.push({
192
198
  name,
193
199
  kind: 'class',
194
200
  startLine,
195
201
  endLine,
196
- signature: firstLine(node.text),
202
+ signature: firstLine(inner.text),
197
203
  });
198
204
  // Extract methods
199
- const body = node.childForFieldName('body');
205
+ const body = inner.childForFieldName('body');
200
206
  if (body) {
201
207
  for (let j = 0; j < body.namedChildCount; j++) {
202
- const member = body.namedChild(j);
208
+ let member = body.namedChild(j);
209
+ // Unwrap decorated methods
210
+ if (member.type === 'decorated_definition') {
211
+ member = member.childForFieldName('definition') ?? member;
212
+ }
203
213
  if (member.type === 'function_definition') {
204
214
  const methodName = extractName(member);
205
215
  if (methodName) {
@@ -217,11 +227,11 @@ function extractPySymbols(tree) {
217
227
  }
218
228
  }
219
229
  }
220
- else if (node.type === 'expression_statement' &&
221
- node.namedChildCount === 1 &&
222
- node.namedChild(0).type === 'assignment') {
230
+ else if (inner.type === 'expression_statement' &&
231
+ inner.namedChildCount === 1 &&
232
+ inner.namedChild(0).type === 'assignment') {
223
233
  // Top-level assignment: FOO = ...
224
- const assign = node.namedChild(0);
234
+ const assign = inner.namedChild(0);
225
235
  const left = assign.childForFieldName('left');
226
236
  if (left && left.type === 'identifier') {
227
237
  symbols.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lat.md",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "A knowledge graph for your codebase, written in markdown",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.30.2",
@@ -0,0 +1,182 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 73224 16688" fill="#444">
2
+ <defs>
3
+ <path id="g2591" d="M934 -512V-251H1094V-512ZM298 -512V-251H457V-512ZM616 -147V114H775V-147ZM-20 -147V114H138V-147ZM934 219V480H1094V219ZM298 219V480H457V219ZM616 584V845H775V584ZM-20 584V845H138V584ZM934 950V1211H1094V950ZM298 950V1211H457V950ZM616 1315V1576H775V1315ZM-20 1315V1576H138V1315Z"/>
4
+ <path id="g2588" d="M-20 -512V1576H1253V-512Z"/>
5
+ </defs>
6
+ <use href="#g2591" transform="translate(0,0) scale(1,-1) translate(0,-1901)"/>
7
+ <use href="#g2588" transform="translate(1356,0) scale(1,-1) translate(0,-1901)"/>
8
+ <use href="#g2588" transform="translate(2712,0) scale(1,-1) translate(0,-1901)"/>
9
+ <use href="#g2591" transform="translate(21696,0) scale(1,-1) translate(0,-1901)"/>
10
+ <use href="#g2588" transform="translate(23052,0) scale(1,-1) translate(0,-1901)"/>
11
+ <use href="#g2588" transform="translate(24408,0) scale(1,-1) translate(0,-1901)"/>
12
+ <use href="#g2591" transform="translate(69156,0) scale(1,-1) translate(0,-1901)"/>
13
+ <use href="#g2588" transform="translate(70512,0) scale(1,-1) translate(0,-1901)"/>
14
+ <use href="#g2588" transform="translate(71868,0) scale(1,-1) translate(0,-1901)"/>
15
+ <use href="#g2591" transform="translate(0,2384) scale(1,-1) translate(0,-1901)"/>
16
+ <use href="#g2588" transform="translate(1356,2384) scale(1,-1) translate(0,-1901)"/>
17
+ <use href="#g2588" transform="translate(2712,2384) scale(1,-1) translate(0,-1901)"/>
18
+ <use href="#g2591" transform="translate(21696,2384) scale(1,-1) translate(0,-1901)"/>
19
+ <use href="#g2588" transform="translate(23052,2384) scale(1,-1) translate(0,-1901)"/>
20
+ <use href="#g2588" transform="translate(24408,2384) scale(1,-1) translate(0,-1901)"/>
21
+ <use href="#g2591" transform="translate(69156,2384) scale(1,-1) translate(0,-1901)"/>
22
+ <use href="#g2588" transform="translate(70512,2384) scale(1,-1) translate(0,-1901)"/>
23
+ <use href="#g2588" transform="translate(71868,2384) scale(1,-1) translate(0,-1901)"/>
24
+ <use href="#g2591" transform="translate(0,4768) scale(1,-1) translate(0,-1901)"/>
25
+ <use href="#g2588" transform="translate(1356,4768) scale(1,-1) translate(0,-1901)"/>
26
+ <use href="#g2588" transform="translate(2712,4768) scale(1,-1) translate(0,-1901)"/>
27
+ <use href="#g2591" transform="translate(6780,4768) scale(1,-1) translate(0,-1901)"/>
28
+ <use href="#g2588" transform="translate(8136,4768) scale(1,-1) translate(0,-1901)"/>
29
+ <use href="#g2588" transform="translate(9492,4768) scale(1,-1) translate(0,-1901)"/>
30
+ <use href="#g2588" transform="translate(10848,4768) scale(1,-1) translate(0,-1901)"/>
31
+ <use href="#g2588" transform="translate(12204,4768) scale(1,-1) translate(0,-1901)"/>
32
+ <use href="#g2588" transform="translate(13560,4768) scale(1,-1) translate(0,-1901)"/>
33
+ <use href="#g2588" transform="translate(14916,4768) scale(1,-1) translate(0,-1901)"/>
34
+ <use href="#g2591" transform="translate(18984,4768) scale(1,-1) translate(0,-1901)"/>
35
+ <use href="#g2588" transform="translate(20340,4768) scale(1,-1) translate(0,-1901)"/>
36
+ <use href="#g2588" transform="translate(21696,4768) scale(1,-1) translate(0,-1901)"/>
37
+ <use href="#g2588" transform="translate(23052,4768) scale(1,-1) translate(0,-1901)"/>
38
+ <use href="#g2588" transform="translate(24408,4768) scale(1,-1) translate(0,-1901)"/>
39
+ <use href="#g2588" transform="translate(25764,4768) scale(1,-1) translate(0,-1901)"/>
40
+ <use href="#g2588" transform="translate(27120,4768) scale(1,-1) translate(0,-1901)"/>
41
+ <use href="#g2591" transform="translate(37968,4768) scale(1,-1) translate(0,-1901)"/>
42
+ <use href="#g2588" transform="translate(39324,4768) scale(1,-1) translate(0,-1901)"/>
43
+ <use href="#g2588" transform="translate(40680,4768) scale(1,-1) translate(0,-1901)"/>
44
+ <use href="#g2588" transform="translate(42036,4768) scale(1,-1) translate(0,-1901)"/>
45
+ <use href="#g2588" transform="translate(43392,4768) scale(1,-1) translate(0,-1901)"/>
46
+ <use href="#g2588" transform="translate(44748,4768) scale(1,-1) translate(0,-1901)"/>
47
+ <use href="#g2588" transform="translate(46104,4768) scale(1,-1) translate(0,-1901)"/>
48
+ <use href="#g2588" transform="translate(47460,4768) scale(1,-1) translate(0,-1901)"/>
49
+ <use href="#g2588" transform="translate(48816,4768) scale(1,-1) translate(0,-1901)"/>
50
+ <use href="#g2588" transform="translate(50172,4768) scale(1,-1) translate(0,-1901)"/>
51
+ <use href="#g2588" transform="translate(51528,4768) scale(1,-1) translate(0,-1901)"/>
52
+ <use href="#g2588" transform="translate(52884,4768) scale(1,-1) translate(0,-1901)"/>
53
+ <use href="#g2588" transform="translate(54240,4768) scale(1,-1) translate(0,-1901)"/>
54
+ <use href="#g2588" transform="translate(55596,4768) scale(1,-1) translate(0,-1901)"/>
55
+ <use href="#g2591" transform="translate(61020,4768) scale(1,-1) translate(0,-1901)"/>
56
+ <use href="#g2588" transform="translate(62376,4768) scale(1,-1) translate(0,-1901)"/>
57
+ <use href="#g2588" transform="translate(63732,4768) scale(1,-1) translate(0,-1901)"/>
58
+ <use href="#g2588" transform="translate(65088,4768) scale(1,-1) translate(0,-1901)"/>
59
+ <use href="#g2588" transform="translate(66444,4768) scale(1,-1) translate(0,-1901)"/>
60
+ <use href="#g2588" transform="translate(67800,4768) scale(1,-1) translate(0,-1901)"/>
61
+ <use href="#g2588" transform="translate(69156,4768) scale(1,-1) translate(0,-1901)"/>
62
+ <use href="#g2588" transform="translate(70512,4768) scale(1,-1) translate(0,-1901)"/>
63
+ <use href="#g2588" transform="translate(71868,4768) scale(1,-1) translate(0,-1901)"/>
64
+ <use href="#g2591" transform="translate(0,7152) scale(1,-1) translate(0,-1901)"/>
65
+ <use href="#g2588" transform="translate(1356,7152) scale(1,-1) translate(0,-1901)"/>
66
+ <use href="#g2588" transform="translate(2712,7152) scale(1,-1) translate(0,-1901)"/>
67
+ <use href="#g2591" transform="translate(13560,7152) scale(1,-1) translate(0,-1901)"/>
68
+ <use href="#g2588" transform="translate(14916,7152) scale(1,-1) translate(0,-1901)"/>
69
+ <use href="#g2588" transform="translate(16272,7152) scale(1,-1) translate(0,-1901)"/>
70
+ <use href="#g2591" transform="translate(21696,7152) scale(1,-1) translate(0,-1901)"/>
71
+ <use href="#g2588" transform="translate(23052,7152) scale(1,-1) translate(0,-1901)"/>
72
+ <use href="#g2588" transform="translate(24408,7152) scale(1,-1) translate(0,-1901)"/>
73
+ <use href="#g2591" transform="translate(37968,7152) scale(1,-1) translate(0,-1901)"/>
74
+ <use href="#g2588" transform="translate(39324,7152) scale(1,-1) translate(0,-1901)"/>
75
+ <use href="#g2588" transform="translate(40680,7152) scale(1,-1) translate(0,-1901)"/>
76
+ <use href="#g2591" transform="translate(46104,7152) scale(1,-1) translate(0,-1901)"/>
77
+ <use href="#g2588" transform="translate(47460,7152) scale(1,-1) translate(0,-1901)"/>
78
+ <use href="#g2588" transform="translate(48816,7152) scale(1,-1) translate(0,-1901)"/>
79
+ <use href="#g2591" transform="translate(54240,7152) scale(1,-1) translate(0,-1901)"/>
80
+ <use href="#g2588" transform="translate(55596,7152) scale(1,-1) translate(0,-1901)"/>
81
+ <use href="#g2588" transform="translate(56952,7152) scale(1,-1) translate(0,-1901)"/>
82
+ <use href="#g2591" transform="translate(59664,7152) scale(1,-1) translate(0,-1901)"/>
83
+ <use href="#g2588" transform="translate(61020,7152) scale(1,-1) translate(0,-1901)"/>
84
+ <use href="#g2588" transform="translate(62376,7152) scale(1,-1) translate(0,-1901)"/>
85
+ <use href="#g2591" transform="translate(69156,7152) scale(1,-1) translate(0,-1901)"/>
86
+ <use href="#g2588" transform="translate(70512,7152) scale(1,-1) translate(0,-1901)"/>
87
+ <use href="#g2588" transform="translate(71868,7152) scale(1,-1) translate(0,-1901)"/>
88
+ <use href="#g2591" transform="translate(0,9536) scale(1,-1) translate(0,-1901)"/>
89
+ <use href="#g2588" transform="translate(1356,9536) scale(1,-1) translate(0,-1901)"/>
90
+ <use href="#g2588" transform="translate(2712,9536) scale(1,-1) translate(0,-1901)"/>
91
+ <use href="#g2591" transform="translate(6780,9536) scale(1,-1) translate(0,-1901)"/>
92
+ <use href="#g2588" transform="translate(8136,9536) scale(1,-1) translate(0,-1901)"/>
93
+ <use href="#g2588" transform="translate(9492,9536) scale(1,-1) translate(0,-1901)"/>
94
+ <use href="#g2588" transform="translate(10848,9536) scale(1,-1) translate(0,-1901)"/>
95
+ <use href="#g2588" transform="translate(12204,9536) scale(1,-1) translate(0,-1901)"/>
96
+ <use href="#g2588" transform="translate(13560,9536) scale(1,-1) translate(0,-1901)"/>
97
+ <use href="#g2588" transform="translate(14916,9536) scale(1,-1) translate(0,-1901)"/>
98
+ <use href="#g2588" transform="translate(16272,9536) scale(1,-1) translate(0,-1901)"/>
99
+ <use href="#g2591" transform="translate(21696,9536) scale(1,-1) translate(0,-1901)"/>
100
+ <use href="#g2588" transform="translate(23052,9536) scale(1,-1) translate(0,-1901)"/>
101
+ <use href="#g2588" transform="translate(24408,9536) scale(1,-1) translate(0,-1901)"/>
102
+ <use href="#g2591" transform="translate(37968,9536) scale(1,-1) translate(0,-1901)"/>
103
+ <use href="#g2588" transform="translate(39324,9536) scale(1,-1) translate(0,-1901)"/>
104
+ <use href="#g2588" transform="translate(40680,9536) scale(1,-1) translate(0,-1901)"/>
105
+ <use href="#g2591" transform="translate(46104,9536) scale(1,-1) translate(0,-1901)"/>
106
+ <use href="#g2588" transform="translate(47460,9536) scale(1,-1) translate(0,-1901)"/>
107
+ <use href="#g2588" transform="translate(48816,9536) scale(1,-1) translate(0,-1901)"/>
108
+ <use href="#g2591" transform="translate(54240,9536) scale(1,-1) translate(0,-1901)"/>
109
+ <use href="#g2588" transform="translate(55596,9536) scale(1,-1) translate(0,-1901)"/>
110
+ <use href="#g2588" transform="translate(56952,9536) scale(1,-1) translate(0,-1901)"/>
111
+ <use href="#g2591" transform="translate(59664,9536) scale(1,-1) translate(0,-1901)"/>
112
+ <use href="#g2588" transform="translate(61020,9536) scale(1,-1) translate(0,-1901)"/>
113
+ <use href="#g2588" transform="translate(62376,9536) scale(1,-1) translate(0,-1901)"/>
114
+ <use href="#g2591" transform="translate(69156,9536) scale(1,-1) translate(0,-1901)"/>
115
+ <use href="#g2588" transform="translate(70512,9536) scale(1,-1) translate(0,-1901)"/>
116
+ <use href="#g2588" transform="translate(71868,9536) scale(1,-1) translate(0,-1901)"/>
117
+ <use href="#g2591" transform="translate(0,11920) scale(1,-1) translate(0,-1901)"/>
118
+ <use href="#g2588" transform="translate(1356,11920) scale(1,-1) translate(0,-1901)"/>
119
+ <use href="#g2588" transform="translate(2712,11920) scale(1,-1) translate(0,-1901)"/>
120
+ <use href="#g2591" transform="translate(5424,11920) scale(1,-1) translate(0,-1901)"/>
121
+ <use href="#g2588" transform="translate(6780,11920) scale(1,-1) translate(0,-1901)"/>
122
+ <use href="#g2588" transform="translate(8136,11920) scale(1,-1) translate(0,-1901)"/>
123
+ <use href="#g2591" transform="translate(13560,11920) scale(1,-1) translate(0,-1901)"/>
124
+ <use href="#g2588" transform="translate(14916,11920) scale(1,-1) translate(0,-1901)"/>
125
+ <use href="#g2588" transform="translate(16272,11920) scale(1,-1) translate(0,-1901)"/>
126
+ <use href="#g2591" transform="translate(21696,11920) scale(1,-1) translate(0,-1901)"/>
127
+ <use href="#g2588" transform="translate(23052,11920) scale(1,-1) translate(0,-1901)"/>
128
+ <use href="#g2588" transform="translate(24408,11920) scale(1,-1) translate(0,-1901)"/>
129
+ <use href="#g2591" transform="translate(37968,11920) scale(1,-1) translate(0,-1901)"/>
130
+ <use href="#g2588" transform="translate(39324,11920) scale(1,-1) translate(0,-1901)"/>
131
+ <use href="#g2588" transform="translate(40680,11920) scale(1,-1) translate(0,-1901)"/>
132
+ <use href="#g2591" transform="translate(46104,11920) scale(1,-1) translate(0,-1901)"/>
133
+ <use href="#g2588" transform="translate(47460,11920) scale(1,-1) translate(0,-1901)"/>
134
+ <use href="#g2588" transform="translate(48816,11920) scale(1,-1) translate(0,-1901)"/>
135
+ <use href="#g2591" transform="translate(54240,11920) scale(1,-1) translate(0,-1901)"/>
136
+ <use href="#g2588" transform="translate(55596,11920) scale(1,-1) translate(0,-1901)"/>
137
+ <use href="#g2588" transform="translate(56952,11920) scale(1,-1) translate(0,-1901)"/>
138
+ <use href="#g2591" transform="translate(59664,11920) scale(1,-1) translate(0,-1901)"/>
139
+ <use href="#g2588" transform="translate(61020,11920) scale(1,-1) translate(0,-1901)"/>
140
+ <use href="#g2588" transform="translate(62376,11920) scale(1,-1) translate(0,-1901)"/>
141
+ <use href="#g2591" transform="translate(67800,11920) scale(1,-1) translate(0,-1901)"/>
142
+ <use href="#g2588" transform="translate(69156,11920) scale(1,-1) translate(0,-1901)"/>
143
+ <use href="#g2588" transform="translate(70512,11920) scale(1,-1) translate(0,-1901)"/>
144
+ <use href="#g2588" transform="translate(71868,11920) scale(1,-1) translate(0,-1901)"/>
145
+ <use href="#g2591" transform="translate(0,14304) scale(1,-1) translate(0,-1901)"/>
146
+ <use href="#g2588" transform="translate(1356,14304) scale(1,-1) translate(0,-1901)"/>
147
+ <use href="#g2588" transform="translate(2712,14304) scale(1,-1) translate(0,-1901)"/>
148
+ <use href="#g2591" transform="translate(6780,14304) scale(1,-1) translate(0,-1901)"/>
149
+ <use href="#g2588" transform="translate(8136,14304) scale(1,-1) translate(0,-1901)"/>
150
+ <use href="#g2588" transform="translate(9492,14304) scale(1,-1) translate(0,-1901)"/>
151
+ <use href="#g2588" transform="translate(10848,14304) scale(1,-1) translate(0,-1901)"/>
152
+ <use href="#g2588" transform="translate(12204,14304) scale(1,-1) translate(0,-1901)"/>
153
+ <use href="#g2588" transform="translate(13560,14304) scale(1,-1) translate(0,-1901)"/>
154
+ <use href="#g2591" transform="translate(14916,14304) scale(1,-1) translate(0,-1901)"/>
155
+ <use href="#g2588" transform="translate(16272,14304) scale(1,-1) translate(0,-1901)"/>
156
+ <use href="#g2588" transform="translate(17628,14304) scale(1,-1) translate(0,-1901)"/>
157
+ <use href="#g2591" transform="translate(23052,14304) scale(1,-1) translate(0,-1901)"/>
158
+ <use href="#g2588" transform="translate(24408,14304) scale(1,-1) translate(0,-1901)"/>
159
+ <use href="#g2588" transform="translate(25764,14304) scale(1,-1) translate(0,-1901)"/>
160
+ <use href="#g2588" transform="translate(27120,14304) scale(1,-1) translate(0,-1901)"/>
161
+ <use href="#g2591" transform="translate(31188,14304) scale(1,-1) translate(0,-1901)"/>
162
+ <use href="#g2588" transform="translate(32544,14304) scale(1,-1) translate(0,-1901)"/>
163
+ <use href="#g2588" transform="translate(33900,14304) scale(1,-1) translate(0,-1901)"/>
164
+ <use href="#g2591" transform="translate(37968,14304) scale(1,-1) translate(0,-1901)"/>
165
+ <use href="#g2588" transform="translate(39324,14304) scale(1,-1) translate(0,-1901)"/>
166
+ <use href="#g2588" transform="translate(40680,14304) scale(1,-1) translate(0,-1901)"/>
167
+ <use href="#g2591" transform="translate(46104,14304) scale(1,-1) translate(0,-1901)"/>
168
+ <use href="#g2588" transform="translate(47460,14304) scale(1,-1) translate(0,-1901)"/>
169
+ <use href="#g2588" transform="translate(48816,14304) scale(1,-1) translate(0,-1901)"/>
170
+ <use href="#g2591" transform="translate(54240,14304) scale(1,-1) translate(0,-1901)"/>
171
+ <use href="#g2588" transform="translate(55596,14304) scale(1,-1) translate(0,-1901)"/>
172
+ <use href="#g2588" transform="translate(56952,14304) scale(1,-1) translate(0,-1901)"/>
173
+ <use href="#g2591" transform="translate(61020,14304) scale(1,-1) translate(0,-1901)"/>
174
+ <use href="#g2588" transform="translate(62376,14304) scale(1,-1) translate(0,-1901)"/>
175
+ <use href="#g2588" transform="translate(63732,14304) scale(1,-1) translate(0,-1901)"/>
176
+ <use href="#g2588" transform="translate(65088,14304) scale(1,-1) translate(0,-1901)"/>
177
+ <use href="#g2588" transform="translate(66444,14304) scale(1,-1) translate(0,-1901)"/>
178
+ <use href="#g2588" transform="translate(67800,14304) scale(1,-1) translate(0,-1901)"/>
179
+ <use href="#g2591" transform="translate(69156,14304) scale(1,-1) translate(0,-1901)"/>
180
+ <use href="#g2588" transform="translate(70512,14304) scale(1,-1) translate(0,-1901)"/>
181
+ <use href="#g2588" transform="translate(71868,14304) scale(1,-1) translate(0,-1901)"/>
182
+ </svg>
@@ -0,0 +1,182 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 73224 16688" fill="#ddd">
2
+ <defs>
3
+ <path id="g2588" d="M-20 -512V1576H1253V-512Z"/>
4
+ <path id="g2591" d="M934 -512V-251H1094V-512ZM298 -512V-251H457V-512ZM616 -147V114H775V-147ZM-20 -147V114H138V-147ZM934 219V480H1094V219ZM298 219V480H457V219ZM616 584V845H775V584ZM-20 584V845H138V584ZM934 950V1211H1094V950ZM298 950V1211H457V950ZM616 1315V1576H775V1315ZM-20 1315V1576H138V1315Z"/>
5
+ </defs>
6
+ <use href="#g2591" transform="translate(0,0) scale(1,-1) translate(0,-1901)"/>
7
+ <use href="#g2588" transform="translate(1356,0) scale(1,-1) translate(0,-1901)"/>
8
+ <use href="#g2588" transform="translate(2712,0) scale(1,-1) translate(0,-1901)"/>
9
+ <use href="#g2591" transform="translate(21696,0) scale(1,-1) translate(0,-1901)"/>
10
+ <use href="#g2588" transform="translate(23052,0) scale(1,-1) translate(0,-1901)"/>
11
+ <use href="#g2588" transform="translate(24408,0) scale(1,-1) translate(0,-1901)"/>
12
+ <use href="#g2591" transform="translate(69156,0) scale(1,-1) translate(0,-1901)"/>
13
+ <use href="#g2588" transform="translate(70512,0) scale(1,-1) translate(0,-1901)"/>
14
+ <use href="#g2588" transform="translate(71868,0) scale(1,-1) translate(0,-1901)"/>
15
+ <use href="#g2591" transform="translate(0,2384) scale(1,-1) translate(0,-1901)"/>
16
+ <use href="#g2588" transform="translate(1356,2384) scale(1,-1) translate(0,-1901)"/>
17
+ <use href="#g2588" transform="translate(2712,2384) scale(1,-1) translate(0,-1901)"/>
18
+ <use href="#g2591" transform="translate(21696,2384) scale(1,-1) translate(0,-1901)"/>
19
+ <use href="#g2588" transform="translate(23052,2384) scale(1,-1) translate(0,-1901)"/>
20
+ <use href="#g2588" transform="translate(24408,2384) scale(1,-1) translate(0,-1901)"/>
21
+ <use href="#g2591" transform="translate(69156,2384) scale(1,-1) translate(0,-1901)"/>
22
+ <use href="#g2588" transform="translate(70512,2384) scale(1,-1) translate(0,-1901)"/>
23
+ <use href="#g2588" transform="translate(71868,2384) scale(1,-1) translate(0,-1901)"/>
24
+ <use href="#g2591" transform="translate(0,4768) scale(1,-1) translate(0,-1901)"/>
25
+ <use href="#g2588" transform="translate(1356,4768) scale(1,-1) translate(0,-1901)"/>
26
+ <use href="#g2588" transform="translate(2712,4768) scale(1,-1) translate(0,-1901)"/>
27
+ <use href="#g2591" transform="translate(6780,4768) scale(1,-1) translate(0,-1901)"/>
28
+ <use href="#g2588" transform="translate(8136,4768) scale(1,-1) translate(0,-1901)"/>
29
+ <use href="#g2588" transform="translate(9492,4768) scale(1,-1) translate(0,-1901)"/>
30
+ <use href="#g2588" transform="translate(10848,4768) scale(1,-1) translate(0,-1901)"/>
31
+ <use href="#g2588" transform="translate(12204,4768) scale(1,-1) translate(0,-1901)"/>
32
+ <use href="#g2588" transform="translate(13560,4768) scale(1,-1) translate(0,-1901)"/>
33
+ <use href="#g2588" transform="translate(14916,4768) scale(1,-1) translate(0,-1901)"/>
34
+ <use href="#g2591" transform="translate(18984,4768) scale(1,-1) translate(0,-1901)"/>
35
+ <use href="#g2588" transform="translate(20340,4768) scale(1,-1) translate(0,-1901)"/>
36
+ <use href="#g2588" transform="translate(21696,4768) scale(1,-1) translate(0,-1901)"/>
37
+ <use href="#g2588" transform="translate(23052,4768) scale(1,-1) translate(0,-1901)"/>
38
+ <use href="#g2588" transform="translate(24408,4768) scale(1,-1) translate(0,-1901)"/>
39
+ <use href="#g2588" transform="translate(25764,4768) scale(1,-1) translate(0,-1901)"/>
40
+ <use href="#g2588" transform="translate(27120,4768) scale(1,-1) translate(0,-1901)"/>
41
+ <use href="#g2591" transform="translate(37968,4768) scale(1,-1) translate(0,-1901)"/>
42
+ <use href="#g2588" transform="translate(39324,4768) scale(1,-1) translate(0,-1901)"/>
43
+ <use href="#g2588" transform="translate(40680,4768) scale(1,-1) translate(0,-1901)"/>
44
+ <use href="#g2588" transform="translate(42036,4768) scale(1,-1) translate(0,-1901)"/>
45
+ <use href="#g2588" transform="translate(43392,4768) scale(1,-1) translate(0,-1901)"/>
46
+ <use href="#g2588" transform="translate(44748,4768) scale(1,-1) translate(0,-1901)"/>
47
+ <use href="#g2588" transform="translate(46104,4768) scale(1,-1) translate(0,-1901)"/>
48
+ <use href="#g2588" transform="translate(47460,4768) scale(1,-1) translate(0,-1901)"/>
49
+ <use href="#g2588" transform="translate(48816,4768) scale(1,-1) translate(0,-1901)"/>
50
+ <use href="#g2588" transform="translate(50172,4768) scale(1,-1) translate(0,-1901)"/>
51
+ <use href="#g2588" transform="translate(51528,4768) scale(1,-1) translate(0,-1901)"/>
52
+ <use href="#g2588" transform="translate(52884,4768) scale(1,-1) translate(0,-1901)"/>
53
+ <use href="#g2588" transform="translate(54240,4768) scale(1,-1) translate(0,-1901)"/>
54
+ <use href="#g2588" transform="translate(55596,4768) scale(1,-1) translate(0,-1901)"/>
55
+ <use href="#g2591" transform="translate(61020,4768) scale(1,-1) translate(0,-1901)"/>
56
+ <use href="#g2588" transform="translate(62376,4768) scale(1,-1) translate(0,-1901)"/>
57
+ <use href="#g2588" transform="translate(63732,4768) scale(1,-1) translate(0,-1901)"/>
58
+ <use href="#g2588" transform="translate(65088,4768) scale(1,-1) translate(0,-1901)"/>
59
+ <use href="#g2588" transform="translate(66444,4768) scale(1,-1) translate(0,-1901)"/>
60
+ <use href="#g2588" transform="translate(67800,4768) scale(1,-1) translate(0,-1901)"/>
61
+ <use href="#g2588" transform="translate(69156,4768) scale(1,-1) translate(0,-1901)"/>
62
+ <use href="#g2588" transform="translate(70512,4768) scale(1,-1) translate(0,-1901)"/>
63
+ <use href="#g2588" transform="translate(71868,4768) scale(1,-1) translate(0,-1901)"/>
64
+ <use href="#g2591" transform="translate(0,7152) scale(1,-1) translate(0,-1901)"/>
65
+ <use href="#g2588" transform="translate(1356,7152) scale(1,-1) translate(0,-1901)"/>
66
+ <use href="#g2588" transform="translate(2712,7152) scale(1,-1) translate(0,-1901)"/>
67
+ <use href="#g2591" transform="translate(13560,7152) scale(1,-1) translate(0,-1901)"/>
68
+ <use href="#g2588" transform="translate(14916,7152) scale(1,-1) translate(0,-1901)"/>
69
+ <use href="#g2588" transform="translate(16272,7152) scale(1,-1) translate(0,-1901)"/>
70
+ <use href="#g2591" transform="translate(21696,7152) scale(1,-1) translate(0,-1901)"/>
71
+ <use href="#g2588" transform="translate(23052,7152) scale(1,-1) translate(0,-1901)"/>
72
+ <use href="#g2588" transform="translate(24408,7152) scale(1,-1) translate(0,-1901)"/>
73
+ <use href="#g2591" transform="translate(37968,7152) scale(1,-1) translate(0,-1901)"/>
74
+ <use href="#g2588" transform="translate(39324,7152) scale(1,-1) translate(0,-1901)"/>
75
+ <use href="#g2588" transform="translate(40680,7152) scale(1,-1) translate(0,-1901)"/>
76
+ <use href="#g2591" transform="translate(46104,7152) scale(1,-1) translate(0,-1901)"/>
77
+ <use href="#g2588" transform="translate(47460,7152) scale(1,-1) translate(0,-1901)"/>
78
+ <use href="#g2588" transform="translate(48816,7152) scale(1,-1) translate(0,-1901)"/>
79
+ <use href="#g2591" transform="translate(54240,7152) scale(1,-1) translate(0,-1901)"/>
80
+ <use href="#g2588" transform="translate(55596,7152) scale(1,-1) translate(0,-1901)"/>
81
+ <use href="#g2588" transform="translate(56952,7152) scale(1,-1) translate(0,-1901)"/>
82
+ <use href="#g2591" transform="translate(59664,7152) scale(1,-1) translate(0,-1901)"/>
83
+ <use href="#g2588" transform="translate(61020,7152) scale(1,-1) translate(0,-1901)"/>
84
+ <use href="#g2588" transform="translate(62376,7152) scale(1,-1) translate(0,-1901)"/>
85
+ <use href="#g2591" transform="translate(69156,7152) scale(1,-1) translate(0,-1901)"/>
86
+ <use href="#g2588" transform="translate(70512,7152) scale(1,-1) translate(0,-1901)"/>
87
+ <use href="#g2588" transform="translate(71868,7152) scale(1,-1) translate(0,-1901)"/>
88
+ <use href="#g2591" transform="translate(0,9536) scale(1,-1) translate(0,-1901)"/>
89
+ <use href="#g2588" transform="translate(1356,9536) scale(1,-1) translate(0,-1901)"/>
90
+ <use href="#g2588" transform="translate(2712,9536) scale(1,-1) translate(0,-1901)"/>
91
+ <use href="#g2591" transform="translate(6780,9536) scale(1,-1) translate(0,-1901)"/>
92
+ <use href="#g2588" transform="translate(8136,9536) scale(1,-1) translate(0,-1901)"/>
93
+ <use href="#g2588" transform="translate(9492,9536) scale(1,-1) translate(0,-1901)"/>
94
+ <use href="#g2588" transform="translate(10848,9536) scale(1,-1) translate(0,-1901)"/>
95
+ <use href="#g2588" transform="translate(12204,9536) scale(1,-1) translate(0,-1901)"/>
96
+ <use href="#g2588" transform="translate(13560,9536) scale(1,-1) translate(0,-1901)"/>
97
+ <use href="#g2588" transform="translate(14916,9536) scale(1,-1) translate(0,-1901)"/>
98
+ <use href="#g2588" transform="translate(16272,9536) scale(1,-1) translate(0,-1901)"/>
99
+ <use href="#g2591" transform="translate(21696,9536) scale(1,-1) translate(0,-1901)"/>
100
+ <use href="#g2588" transform="translate(23052,9536) scale(1,-1) translate(0,-1901)"/>
101
+ <use href="#g2588" transform="translate(24408,9536) scale(1,-1) translate(0,-1901)"/>
102
+ <use href="#g2591" transform="translate(37968,9536) scale(1,-1) translate(0,-1901)"/>
103
+ <use href="#g2588" transform="translate(39324,9536) scale(1,-1) translate(0,-1901)"/>
104
+ <use href="#g2588" transform="translate(40680,9536) scale(1,-1) translate(0,-1901)"/>
105
+ <use href="#g2591" transform="translate(46104,9536) scale(1,-1) translate(0,-1901)"/>
106
+ <use href="#g2588" transform="translate(47460,9536) scale(1,-1) translate(0,-1901)"/>
107
+ <use href="#g2588" transform="translate(48816,9536) scale(1,-1) translate(0,-1901)"/>
108
+ <use href="#g2591" transform="translate(54240,9536) scale(1,-1) translate(0,-1901)"/>
109
+ <use href="#g2588" transform="translate(55596,9536) scale(1,-1) translate(0,-1901)"/>
110
+ <use href="#g2588" transform="translate(56952,9536) scale(1,-1) translate(0,-1901)"/>
111
+ <use href="#g2591" transform="translate(59664,9536) scale(1,-1) translate(0,-1901)"/>
112
+ <use href="#g2588" transform="translate(61020,9536) scale(1,-1) translate(0,-1901)"/>
113
+ <use href="#g2588" transform="translate(62376,9536) scale(1,-1) translate(0,-1901)"/>
114
+ <use href="#g2591" transform="translate(69156,9536) scale(1,-1) translate(0,-1901)"/>
115
+ <use href="#g2588" transform="translate(70512,9536) scale(1,-1) translate(0,-1901)"/>
116
+ <use href="#g2588" transform="translate(71868,9536) scale(1,-1) translate(0,-1901)"/>
117
+ <use href="#g2591" transform="translate(0,11920) scale(1,-1) translate(0,-1901)"/>
118
+ <use href="#g2588" transform="translate(1356,11920) scale(1,-1) translate(0,-1901)"/>
119
+ <use href="#g2588" transform="translate(2712,11920) scale(1,-1) translate(0,-1901)"/>
120
+ <use href="#g2591" transform="translate(5424,11920) scale(1,-1) translate(0,-1901)"/>
121
+ <use href="#g2588" transform="translate(6780,11920) scale(1,-1) translate(0,-1901)"/>
122
+ <use href="#g2588" transform="translate(8136,11920) scale(1,-1) translate(0,-1901)"/>
123
+ <use href="#g2591" transform="translate(13560,11920) scale(1,-1) translate(0,-1901)"/>
124
+ <use href="#g2588" transform="translate(14916,11920) scale(1,-1) translate(0,-1901)"/>
125
+ <use href="#g2588" transform="translate(16272,11920) scale(1,-1) translate(0,-1901)"/>
126
+ <use href="#g2591" transform="translate(21696,11920) scale(1,-1) translate(0,-1901)"/>
127
+ <use href="#g2588" transform="translate(23052,11920) scale(1,-1) translate(0,-1901)"/>
128
+ <use href="#g2588" transform="translate(24408,11920) scale(1,-1) translate(0,-1901)"/>
129
+ <use href="#g2591" transform="translate(37968,11920) scale(1,-1) translate(0,-1901)"/>
130
+ <use href="#g2588" transform="translate(39324,11920) scale(1,-1) translate(0,-1901)"/>
131
+ <use href="#g2588" transform="translate(40680,11920) scale(1,-1) translate(0,-1901)"/>
132
+ <use href="#g2591" transform="translate(46104,11920) scale(1,-1) translate(0,-1901)"/>
133
+ <use href="#g2588" transform="translate(47460,11920) scale(1,-1) translate(0,-1901)"/>
134
+ <use href="#g2588" transform="translate(48816,11920) scale(1,-1) translate(0,-1901)"/>
135
+ <use href="#g2591" transform="translate(54240,11920) scale(1,-1) translate(0,-1901)"/>
136
+ <use href="#g2588" transform="translate(55596,11920) scale(1,-1) translate(0,-1901)"/>
137
+ <use href="#g2588" transform="translate(56952,11920) scale(1,-1) translate(0,-1901)"/>
138
+ <use href="#g2591" transform="translate(59664,11920) scale(1,-1) translate(0,-1901)"/>
139
+ <use href="#g2588" transform="translate(61020,11920) scale(1,-1) translate(0,-1901)"/>
140
+ <use href="#g2588" transform="translate(62376,11920) scale(1,-1) translate(0,-1901)"/>
141
+ <use href="#g2591" transform="translate(67800,11920) scale(1,-1) translate(0,-1901)"/>
142
+ <use href="#g2588" transform="translate(69156,11920) scale(1,-1) translate(0,-1901)"/>
143
+ <use href="#g2588" transform="translate(70512,11920) scale(1,-1) translate(0,-1901)"/>
144
+ <use href="#g2588" transform="translate(71868,11920) scale(1,-1) translate(0,-1901)"/>
145
+ <use href="#g2591" transform="translate(0,14304) scale(1,-1) translate(0,-1901)"/>
146
+ <use href="#g2588" transform="translate(1356,14304) scale(1,-1) translate(0,-1901)"/>
147
+ <use href="#g2588" transform="translate(2712,14304) scale(1,-1) translate(0,-1901)"/>
148
+ <use href="#g2591" transform="translate(6780,14304) scale(1,-1) translate(0,-1901)"/>
149
+ <use href="#g2588" transform="translate(8136,14304) scale(1,-1) translate(0,-1901)"/>
150
+ <use href="#g2588" transform="translate(9492,14304) scale(1,-1) translate(0,-1901)"/>
151
+ <use href="#g2588" transform="translate(10848,14304) scale(1,-1) translate(0,-1901)"/>
152
+ <use href="#g2588" transform="translate(12204,14304) scale(1,-1) translate(0,-1901)"/>
153
+ <use href="#g2588" transform="translate(13560,14304) scale(1,-1) translate(0,-1901)"/>
154
+ <use href="#g2591" transform="translate(14916,14304) scale(1,-1) translate(0,-1901)"/>
155
+ <use href="#g2588" transform="translate(16272,14304) scale(1,-1) translate(0,-1901)"/>
156
+ <use href="#g2588" transform="translate(17628,14304) scale(1,-1) translate(0,-1901)"/>
157
+ <use href="#g2591" transform="translate(23052,14304) scale(1,-1) translate(0,-1901)"/>
158
+ <use href="#g2588" transform="translate(24408,14304) scale(1,-1) translate(0,-1901)"/>
159
+ <use href="#g2588" transform="translate(25764,14304) scale(1,-1) translate(0,-1901)"/>
160
+ <use href="#g2588" transform="translate(27120,14304) scale(1,-1) translate(0,-1901)"/>
161
+ <use href="#g2591" transform="translate(31188,14304) scale(1,-1) translate(0,-1901)"/>
162
+ <use href="#g2588" transform="translate(32544,14304) scale(1,-1) translate(0,-1901)"/>
163
+ <use href="#g2588" transform="translate(33900,14304) scale(1,-1) translate(0,-1901)"/>
164
+ <use href="#g2591" transform="translate(37968,14304) scale(1,-1) translate(0,-1901)"/>
165
+ <use href="#g2588" transform="translate(39324,14304) scale(1,-1) translate(0,-1901)"/>
166
+ <use href="#g2588" transform="translate(40680,14304) scale(1,-1) translate(0,-1901)"/>
167
+ <use href="#g2591" transform="translate(46104,14304) scale(1,-1) translate(0,-1901)"/>
168
+ <use href="#g2588" transform="translate(47460,14304) scale(1,-1) translate(0,-1901)"/>
169
+ <use href="#g2588" transform="translate(48816,14304) scale(1,-1) translate(0,-1901)"/>
170
+ <use href="#g2591" transform="translate(54240,14304) scale(1,-1) translate(0,-1901)"/>
171
+ <use href="#g2588" transform="translate(55596,14304) scale(1,-1) translate(0,-1901)"/>
172
+ <use href="#g2588" transform="translate(56952,14304) scale(1,-1) translate(0,-1901)"/>
173
+ <use href="#g2591" transform="translate(61020,14304) scale(1,-1) translate(0,-1901)"/>
174
+ <use href="#g2588" transform="translate(62376,14304) scale(1,-1) translate(0,-1901)"/>
175
+ <use href="#g2588" transform="translate(63732,14304) scale(1,-1) translate(0,-1901)"/>
176
+ <use href="#g2588" transform="translate(65088,14304) scale(1,-1) translate(0,-1901)"/>
177
+ <use href="#g2588" transform="translate(66444,14304) scale(1,-1) translate(0,-1901)"/>
178
+ <use href="#g2588" transform="translate(67800,14304) scale(1,-1) translate(0,-1901)"/>
179
+ <use href="#g2591" transform="translate(69156,14304) scale(1,-1) translate(0,-1901)"/>
180
+ <use href="#g2588" transform="translate(70512,14304) scale(1,-1) translate(0,-1901)"/>
181
+ <use href="#g2588" transform="translate(71868,14304) scale(1,-1) translate(0,-1901)"/>
182
+ </svg>
@@ -1,8 +1,8 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
- import { keyHint } from "@mariozechner/pi-coding-agent";
3
+ import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
4
4
  import type { Theme } from "@mariozechner/pi-tui";
5
- import { Box, Text } from "@mariozechner/pi-tui";
5
+ import { Box, Markdown, Text } from "@mariozechner/pi-tui";
6
6
 
7
7
  const PREVIEW_LINES = 4;
8
8
 
@@ -14,10 +14,11 @@ function collapsibleResult(
14
14
  const text = result.content?.[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "";
15
15
  if (!text) return new Text(theme.fg("dim", "(empty)"), 0, 0);
16
16
  if (options.isPartial) return new Text(theme.fg("dim", "…"), 0, 0);
17
- if (options.expanded) return new Text(text, 0, 0);
17
+ const mdTheme = getMarkdownTheme();
18
+ if (options.expanded) return new Markdown(text, 0, 0, mdTheme);
18
19
 
19
20
  const lines = text.split("\n");
20
- if (lines.length <= PREVIEW_LINES) return new Text(text, 0, 0);
21
+ if (lines.length <= PREVIEW_LINES) return new Markdown(text, 0, 0, mdTheme);
21
22
 
22
23
  const preview = lines.slice(0, PREVIEW_LINES).join("\n");
23
24
  const remaining = lines.length - PREVIEW_LINES;
@@ -223,7 +224,8 @@ export default function (pi: ExtensionAPI) {
223
224
  pi.registerMessageRenderer("lat-reminder", (message, { expanded }, theme) => {
224
225
  const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
225
226
  if (expanded) {
226
- box.addChild(new Text(theme.fg("accent", "lat.md") + " " + message.content, 0, 0));
227
+ box.addChild(new Text(theme.fg("accent", "lat.md"), 0, 0));
228
+ box.addChild(new Markdown(message.content, 0, 0, getMarkdownTheme()));
227
229
  } else {
228
230
  const hint = keyHint("expandTools", "to expand");
229
231
  box.addChild(new Text(
@@ -238,7 +240,8 @@ export default function (pi: ExtensionAPI) {
238
240
  pi.registerMessageRenderer("lat-check", (message, { expanded }, theme) => {
239
241
  const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
240
242
  if (expanded) {
241
- box.addChild(new Text(theme.fg("warning", "lat check") + " " + message.content, 0, 0));
243
+ box.addChild(new Text(theme.fg("warning", "lat check"), 0, 0));
244
+ box.addChild(new Markdown(message.content, 0, 0, getMarkdownTheme()));
242
245
  } else {
243
246
  const hint = keyHint("expandTools", "to expand");
244
247
  const firstLine = message.content.split("\n")[0];
@@ -1,16 +0,0 @@
1
- #!/bin/bash
2
- # lat.md prompt hook for Claude Code
3
- # Injected by `lat init` — reminds the agent to consult lat.md before working.
4
- #
5
- # This hook fires on every user prompt submission. It adds a short instruction
6
- # telling the agent to run `lat search` and `lat expand` so that lat.md context
7
- # is consulted before any code changes.
8
-
9
- cat <<'HOOK_JSON'
10
- {
11
- "hookSpecificOutput": {
12
- "hookEventName": "UserPromptSubmit",
13
- "additionalContext": "Before starting work on this task:\n1. Run `lat search` with a query relevant to the task and read the results to understand the design intent.\n2. If the prompt contains [[refs]], run `lat expand` on the full prompt text to resolve them.\n3. After completing work, run `lat check` to validate all links and code refs.\nDo not skip these steps."
14
- }
15
- }
16
- HOOK_JSON