@plebeiantech/lat.md 0.12.3-fork.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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/src/cli/check-frontmatter.d.ts +12 -0
  4. package/dist/src/cli/check-frontmatter.js +44 -0
  5. package/dist/src/cli/check-mode.d.ts +6 -0
  6. package/dist/src/cli/check-mode.js +303 -0
  7. package/dist/src/cli/check-status.d.ts +74 -0
  8. package/dist/src/cli/check-status.js +217 -0
  9. package/dist/src/cli/check.d.ts +45 -0
  10. package/dist/src/cli/check.js +777 -0
  11. package/dist/src/cli/checklist-menu.d.ts +12 -0
  12. package/dist/src/cli/checklist-menu.js +111 -0
  13. package/dist/src/cli/comment-guard.d.ts +16 -0
  14. package/dist/src/cli/comment-guard.js +111 -0
  15. package/dist/src/cli/comment-reminder.d.ts +62 -0
  16. package/dist/src/cli/comment-reminder.js +210 -0
  17. package/dist/src/cli/context.d.ts +11 -0
  18. package/dist/src/cli/context.js +58 -0
  19. package/dist/src/cli/expand.d.ts +8 -0
  20. package/dist/src/cli/expand.js +94 -0
  21. package/dist/src/cli/gen-index.d.ts +45 -0
  22. package/dist/src/cli/gen-index.js +235 -0
  23. package/dist/src/cli/gen.d.ts +6 -0
  24. package/dist/src/cli/gen.js +42 -0
  25. package/dist/src/cli/graph.d.ts +7 -0
  26. package/dist/src/cli/graph.js +47 -0
  27. package/dist/src/cli/hook.d.ts +1 -0
  28. package/dist/src/cli/hook.js +432 -0
  29. package/dist/src/cli/index.d.ts +2 -0
  30. package/dist/src/cli/index.js +307 -0
  31. package/dist/src/cli/init.d.ts +9 -0
  32. package/dist/src/cli/init.js +1031 -0
  33. package/dist/src/cli/link-scheme.d.ts +54 -0
  34. package/dist/src/cli/link-scheme.js +101 -0
  35. package/dist/src/cli/locate.d.ts +2 -0
  36. package/dist/src/cli/locate.js +17 -0
  37. package/dist/src/cli/refs.d.ts +22 -0
  38. package/dist/src/cli/refs.js +229 -0
  39. package/dist/src/cli/reindex.d.ts +13 -0
  40. package/dist/src/cli/reindex.js +136 -0
  41. package/dist/src/cli/search.d.ts +36 -0
  42. package/dist/src/cli/search.js +217 -0
  43. package/dist/src/cli/section.d.ts +43 -0
  44. package/dist/src/cli/section.js +252 -0
  45. package/dist/src/cli/select-menu.d.ts +13 -0
  46. package/dist/src/cli/select-menu.js +100 -0
  47. package/dist/src/cli/templates.d.ts +1 -0
  48. package/dist/src/cli/templates.js +15 -0
  49. package/dist/src/cli/ui.d.ts +11 -0
  50. package/dist/src/cli/ui.js +34 -0
  51. package/dist/src/code-refs.d.ts +17 -0
  52. package/dist/src/code-refs.js +277 -0
  53. package/dist/src/config.d.ts +29 -0
  54. package/dist/src/config.js +83 -0
  55. package/dist/src/context.d.ts +23 -0
  56. package/dist/src/context.js +11 -0
  57. package/dist/src/extensions/wiki-link/from-markdown.d.ts +5 -0
  58. package/dist/src/extensions/wiki-link/from-markdown.js +34 -0
  59. package/dist/src/extensions/wiki-link/index.d.ts +4 -0
  60. package/dist/src/extensions/wiki-link/index.js +3 -0
  61. package/dist/src/extensions/wiki-link/syntax.d.ts +14 -0
  62. package/dist/src/extensions/wiki-link/syntax.js +121 -0
  63. package/dist/src/extensions/wiki-link/to-markdown.d.ts +5 -0
  64. package/dist/src/extensions/wiki-link/to-markdown.js +28 -0
  65. package/dist/src/extensions/wiki-link/types.d.ts +34 -0
  66. package/dist/src/extensions/wiki-link/types.js +1 -0
  67. package/dist/src/format.d.ts +8 -0
  68. package/dist/src/format.js +43 -0
  69. package/dist/src/graph-export.d.ts +60 -0
  70. package/dist/src/graph-export.js +309 -0
  71. package/dist/src/init-version.d.ts +10 -0
  72. package/dist/src/init-version.js +49 -0
  73. package/dist/src/knowledge/bd.d.ts +2 -0
  74. package/dist/src/knowledge/bd.js +107 -0
  75. package/dist/src/knowledge/claude-memory.d.ts +13 -0
  76. package/dist/src/knowledge/claude-memory.js +204 -0
  77. package/dist/src/knowledge/cq.d.ts +2 -0
  78. package/dist/src/knowledge/cq.js +74 -0
  79. package/dist/src/knowledge/index.d.ts +49 -0
  80. package/dist/src/knowledge/index.js +271 -0
  81. package/dist/src/knowledge/ranking.d.ts +17 -0
  82. package/dist/src/knowledge/ranking.js +62 -0
  83. package/dist/src/knowledge/session.d.ts +46 -0
  84. package/dist/src/knowledge/session.js +203 -0
  85. package/dist/src/knowledge/types.d.ts +47 -0
  86. package/dist/src/knowledge/types.js +10 -0
  87. package/dist/src/lattice.d.ts +108 -0
  88. package/dist/src/lattice.js +816 -0
  89. package/dist/src/mcp/server.d.ts +1 -0
  90. package/dist/src/mcp/server.js +58 -0
  91. package/dist/src/parser.d.ts +3 -0
  92. package/dist/src/parser.js +18 -0
  93. package/dist/src/search/db.d.ts +16 -0
  94. package/dist/src/search/db.js +54 -0
  95. package/dist/src/search/embedder.d.ts +25 -0
  96. package/dist/src/search/embedder.js +58 -0
  97. package/dist/src/search/index.d.ts +9 -0
  98. package/dist/src/search/index.js +66 -0
  99. package/dist/src/search/search.d.ts +9 -0
  100. package/dist/src/search/search.js +16 -0
  101. package/dist/src/source-parser.d.ts +27 -0
  102. package/dist/src/source-parser.js +916 -0
  103. package/dist/src/untrusted.d.ts +24 -0
  104. package/dist/src/untrusted.js +97 -0
  105. package/dist/src/version.d.ts +7 -0
  106. package/dist/src/version.js +39 -0
  107. package/dist/src/view/client/assets/index-B0n0u03y.css +1 -0
  108. package/dist/src/view/client/assets/index-xm2_px4q.js +23 -0
  109. package/dist/src/view/client/index.html +14 -0
  110. package/dist/src/view/diagnostics.d.ts +5 -0
  111. package/dist/src/view/diagnostics.js +171 -0
  112. package/dist/src/view/git-diff.d.ts +3 -0
  113. package/dist/src/view/git-diff.js +255 -0
  114. package/dist/src/view/git.d.ts +22 -0
  115. package/dist/src/view/git.js +193 -0
  116. package/dist/src/view/highlight.d.ts +2 -0
  117. package/dist/src/view/highlight.js +3767 -0
  118. package/dist/src/view/markdown.d.ts +22 -0
  119. package/dist/src/view/markdown.js +322 -0
  120. package/dist/src/view/protocol.d.ts +89 -0
  121. package/dist/src/view/protocol.js +1 -0
  122. package/dist/src/view/references.d.ts +62 -0
  123. package/dist/src/view/references.js +345 -0
  124. package/dist/src/view/repository.d.ts +12 -0
  125. package/dist/src/view/repository.js +167 -0
  126. package/dist/src/view/search.d.ts +14 -0
  127. package/dist/src/view/search.js +64 -0
  128. package/dist/src/view/server.d.ts +20 -0
  129. package/dist/src/view/server.js +233 -0
  130. package/dist/src/view/store.d.ts +57 -0
  131. package/dist/src/view/store.js +404 -0
  132. package/dist/src/walk.d.ts +18 -0
  133. package/dist/src/walk.js +27 -0
  134. package/package.json +85 -0
  135. package/templates/AGENTS.md +118 -0
  136. package/templates/README +1 -0
  137. package/templates/cursor-rules.md +77 -0
  138. package/templates/init/lat.md +1 -0
  139. package/templates/logo-dark.svg +182 -0
  140. package/templates/logo-light.svg +182 -0
  141. package/templates/logo.txt +7 -0
  142. package/templates/opencode-plugin.ts +180 -0
  143. package/templates/pi-extension.ts +364 -0
  144. package/templates/skill/SKILL.md +189 -0
@@ -0,0 +1,345 @@
1
+ import { relative, resolve } from 'node:path';
2
+ import { visit } from 'unist-util-visit';
3
+ import { buildFileIndex, buildSectionSlugIndex, extractRefs, extractLinks, flattenSections, parseSections, resolveRef, } from '../lattice.js';
4
+ import { parse } from '../parser.js';
5
+ import { toPosix } from '../walk.js';
6
+ import { renderMarkdown } from './markdown.js';
7
+ function inlineText(node) {
8
+ if (node.type === 'wikiLink')
9
+ return node.data.alias ?? node.value;
10
+ if ('value' in node && typeof node.value === 'string')
11
+ return node.value;
12
+ if (node.type === 'image')
13
+ return node.alt ?? '';
14
+ if (!('children' in node) || !Array.isArray(node.children))
15
+ return '';
16
+ return node.children
17
+ .map((child) => inlineText(child))
18
+ .join('');
19
+ }
20
+ function paragraphs(content, tree) {
21
+ const byLine = new Map();
22
+ visit(tree, 'paragraph', (node) => {
23
+ const start = node.position?.start.line;
24
+ const end = node.position?.end.line;
25
+ if (!start || !end)
26
+ return;
27
+ const text = inlineText(node).replace(/\s+/g, ' ').trim();
28
+ const startOffset = node.position?.start.offset;
29
+ const endOffset = node.position?.end.offset;
30
+ const paragraph = {
31
+ markdown: startOffset === undefined || endOffset === undefined
32
+ ? text
33
+ : content.slice(startOffset, endOffset),
34
+ startLine: start,
35
+ text,
36
+ };
37
+ for (let line = start; line <= end; line++)
38
+ byLine.set(line, paragraph);
39
+ });
40
+ return byLine;
41
+ }
42
+ function markdownLinks(tree) {
43
+ const definitions = new Map();
44
+ visit(tree, 'definition', (node) => {
45
+ definitions.set(node.identifier.toLowerCase(), node.url);
46
+ });
47
+ const links = [];
48
+ visit(tree, (node) => {
49
+ if (node.type !== 'link' &&
50
+ node.type !== 'image' &&
51
+ node.type !== 'linkReference' &&
52
+ node.type !== 'imageReference') {
53
+ return;
54
+ }
55
+ const line = node.position?.start.line;
56
+ if (!line)
57
+ return;
58
+ const url = node.type === 'link' || node.type === 'image'
59
+ ? node.url
60
+ : definitions.get(node.identifier.toLowerCase());
61
+ if (url) {
62
+ links.push({
63
+ kind: node.type === 'image' || node.type === 'imageReference'
64
+ ? 'image'
65
+ : 'link',
66
+ line,
67
+ url,
68
+ });
69
+ }
70
+ });
71
+ return links;
72
+ }
73
+ /** Parse one Markdown file once into every structure needed by the view store. */
74
+ export function parseViewMarkdownFile(absolutePath, content, latDir, projectRoot) {
75
+ const tree = parse(content);
76
+ return {
77
+ absolutePath,
78
+ content,
79
+ path: toPosix(relative(latDir, absolutePath)),
80
+ projectPath: toPosix(relative(projectRoot, absolutePath)),
81
+ tree,
82
+ sections: parseSections(absolutePath, content, projectRoot, tree),
83
+ wikiRefs: extractRefs(absolutePath, content, projectRoot, tree),
84
+ paragraphs: paragraphs(content, tree),
85
+ markdownLinks: markdownLinks(tree),
86
+ validationLinks: extractLinks(content, tree),
87
+ };
88
+ }
89
+ function contextMarkdownLink(requestedPath, url) {
90
+ if (url.startsWith('/') ||
91
+ url.startsWith('//') ||
92
+ /^[a-z][a-z\d+.-]*:/i.test(url)) {
93
+ return url;
94
+ }
95
+ const encodedPath = requestedPath
96
+ .split('/')
97
+ .map(encodeURIComponent)
98
+ .join('/');
99
+ const resolved = new URL(url, `http://lat.local/docs/${encodedPath}`);
100
+ return `${resolved.pathname}${resolved.search}${resolved.hash}`;
101
+ }
102
+ function documentUrl(latDir, projectRoot, section) {
103
+ const path = toPosix(relative(latDir, resolve(projectRoot, section.filePath)));
104
+ const encoded = path.split('/').map(encodeURIComponent).join('/');
105
+ const fragment = section.githubSlug
106
+ ? `#${encodeURIComponent(section.githubSlug)}`
107
+ : '';
108
+ return `/docs/${encoded}${fragment}`;
109
+ }
110
+ function breadcrumbs(latDir, projectRoot, section) {
111
+ const path = toPosix(relative(latDir, resolve(projectRoot, section.filePath))).replace(/\.md$/i, '');
112
+ return [...path.split('/'), ...section.id.split('#').slice(1)];
113
+ }
114
+ function sourceLineUrl(path, line) {
115
+ const encoded = path.split('/').map(encodeURIComponent).join('/');
116
+ return `/code/${encoded}?at=${line}`;
117
+ }
118
+ function linkedSection(url, sourcePath, sectionsByPath) {
119
+ if (url.startsWith('//') || /^[a-z][a-z\d+.-]*:/i.test(url))
120
+ return null;
121
+ const encodedSourcePath = sourcePath
122
+ .split('/')
123
+ .map(encodeURIComponent)
124
+ .join('/');
125
+ let destination;
126
+ try {
127
+ destination = new URL(url, `http://lat.local/docs/${encodedSourcePath}`);
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ if (destination.origin !== 'http://lat.local' ||
133
+ !destination.pathname.startsWith('/docs/')) {
134
+ return null;
135
+ }
136
+ let path;
137
+ let fragment;
138
+ try {
139
+ path = destination.pathname
140
+ .slice('/docs/'.length)
141
+ .split('/')
142
+ .map(decodeURIComponent)
143
+ .join('/');
144
+ fragment = decodeURIComponent(destination.hash.slice(1));
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ const sections = sectionsByPath.get(path.toLowerCase());
150
+ if (!sections)
151
+ return null;
152
+ if (!fragment)
153
+ return sections[0] ?? null;
154
+ return (sections.find((section) => section.githubSlug?.toLowerCase() === fragment.toLowerCase()) ?? null);
155
+ }
156
+ function paragraphFor(file, section, line) {
157
+ return (file.paragraphs.get(line) ?? {
158
+ markdown: section.firstParagraph,
159
+ startLine: line,
160
+ text: section.firstParagraph,
161
+ });
162
+ }
163
+ /** Resolve cached outgoing occurrences into direct section and source indexes. */
164
+ export function buildViewReferenceIndex(markdownFiles, codeFiles, allSections) {
165
+ const files = [...markdownFiles].sort((a, b) => a.path.localeCompare(b.path));
166
+ const sections = flattenSections(allSections);
167
+ const sectionIds = new Set(sections.map((section) => section.id.toLowerCase()));
168
+ const fileIndex = buildFileIndex(allSections);
169
+ const slugIndex = buildSectionSlugIndex(allSections);
170
+ const sectionById = new Map(sections.map((section) => [section.id.toLowerCase(), section]));
171
+ const sectionsByPath = new Map();
172
+ for (const file of files) {
173
+ sectionsByPath.set(file.path.toLowerCase(), flattenSections(file.sections));
174
+ }
175
+ const incoming = new Map();
176
+ const sourceByTarget = new Map();
177
+ const addIncoming = (targetId, key, reference) => {
178
+ const target = targetId.toLowerCase();
179
+ let references = incoming.get(target);
180
+ if (!references) {
181
+ references = new Map();
182
+ incoming.set(target, references);
183
+ }
184
+ if (!references.has(key))
185
+ references.set(key, reference);
186
+ };
187
+ for (const file of files) {
188
+ const fileSections = flattenSections(file.sections).sort((a, b) => a.startLine - b.startLine);
189
+ for (const ref of file.wikiRefs) {
190
+ const section = sectionById.get(ref.fromSection.toLowerCase());
191
+ if (!section)
192
+ continue;
193
+ const reference = {
194
+ kind: 'markdown',
195
+ section,
196
+ sourcePath: file.path,
197
+ line: ref.line,
198
+ paragraph: paragraphFor(file, section, ref.line),
199
+ activeWikiLink: ref.target,
200
+ };
201
+ const sourceKey = ref.target.toLowerCase();
202
+ const sourceReferences = sourceByTarget.get(sourceKey) ?? [];
203
+ sourceReferences.push(reference);
204
+ sourceByTarget.set(sourceKey, sourceReferences);
205
+ const resolved = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
206
+ if (resolved.ambiguous ||
207
+ !sectionById.has(resolved.resolved.toLowerCase())) {
208
+ continue;
209
+ }
210
+ addIncoming(resolved.resolved, `markdown:${section.filePath}:${reference.paragraph.startLine}`, reference);
211
+ }
212
+ for (const link of file.markdownLinks) {
213
+ if (link.kind !== 'link')
214
+ continue;
215
+ const targetSection = linkedSection(link.url, file.path, sectionsByPath);
216
+ if (!targetSection)
217
+ continue;
218
+ const section = fileSections
219
+ .filter((candidate) => candidate.startLine <= link.line)
220
+ .at(-1);
221
+ if (!section)
222
+ continue;
223
+ const reference = {
224
+ kind: 'markdown',
225
+ section,
226
+ sourcePath: file.path,
227
+ line: link.line,
228
+ paragraph: paragraphFor(file, section, link.line),
229
+ activeMarkdownLink: link.url,
230
+ };
231
+ addIncoming(targetSection.id, `markdown:${section.filePath}:${reference.paragraph.startLine}`, reference);
232
+ }
233
+ }
234
+ for (const file of [...codeFiles].sort((a, b) => a.path.localeCompare(b.path))) {
235
+ for (const ref of [...file.refs].sort((a, b) => a.line - b.line)) {
236
+ const resolved = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
237
+ if (resolved.ambiguous ||
238
+ !sectionById.has(resolved.resolved.toLowerCase())) {
239
+ continue;
240
+ }
241
+ addIncoming(resolved.resolved, `code:${ref.file}:${ref.line}`, {
242
+ kind: 'code',
243
+ path: ref.file,
244
+ line: ref.line,
245
+ snippet: file.lines[ref.line - 1]?.trim() ?? '',
246
+ url: sourceLineUrl(ref.file, ref.line),
247
+ });
248
+ }
249
+ }
250
+ return {
251
+ incomingBySection: new Map([...incoming].map(([target, references]) => [
252
+ target,
253
+ [...references.values()],
254
+ ])),
255
+ sourceByTarget,
256
+ };
257
+ }
258
+ async function renderIndexedMarkdownReference(reference, latDir, projectRoot, resolveWikiLink) {
259
+ const paragraphHtml = (await renderMarkdown(reference.paragraph.markdown, reference.sourcePath, resolveWikiLink, {
260
+ activeWikiLink: reference.activeWikiLink,
261
+ activeMarkdownLink: reference.activeMarkdownLink,
262
+ lineOffset: reference.paragraph.startLine - 1,
263
+ rewriteMarkdownLink: (url) => contextMarkdownLink(reference.sourcePath, url),
264
+ })).html;
265
+ return {
266
+ kind: 'markdown',
267
+ sectionId: reference.section.id,
268
+ breadcrumbs: breadcrumbs(latDir, projectRoot, reference.section),
269
+ paragraph: reference.paragraph.text,
270
+ paragraphHtml,
271
+ url: documentUrl(latDir, projectRoot, reference.section),
272
+ };
273
+ }
274
+ /** Render direct reverse-index entries for the sections visible in one file. */
275
+ export async function renderSectionBackReferences(index, visibleSections, latDir, projectRoot, createWikiLinkResolver) {
276
+ const resolverByPath = new Map();
277
+ const resolverFor = (path) => {
278
+ let resolver = resolverByPath.get(path);
279
+ if (!resolver) {
280
+ resolver = createWikiLinkResolver
281
+ ? createWikiLinkResolver(path)
282
+ : Promise.resolve(async () => null);
283
+ resolverByPath.set(path, resolver);
284
+ }
285
+ return resolver;
286
+ };
287
+ const result = [];
288
+ for (const section of flattenSections(visibleSections)) {
289
+ const indexed = index.incomingBySection.get(section.id.toLowerCase()) ?? [];
290
+ if (indexed.length === 0)
291
+ continue;
292
+ const references = [];
293
+ for (const reference of indexed) {
294
+ references.push(reference.kind === 'code'
295
+ ? reference
296
+ : await renderIndexedMarkdownReference(reference, latDir, projectRoot, await resolverFor(reference.sourcePath)));
297
+ }
298
+ result.push({
299
+ sectionId: section.id,
300
+ headingId: section.githubSlug ?? '',
301
+ references,
302
+ });
303
+ }
304
+ return result;
305
+ }
306
+ /** Render cached Markdown occurrences that point to one source target. */
307
+ export async function renderSourceReferenceContext(index, target, origin, latDir, projectRoot, createWikiLinkResolver) {
308
+ const indexed = index.sourceByTarget.get(target.toLowerCase()) ?? [];
309
+ const resolverByPath = new Map();
310
+ const located = [];
311
+ for (const reference of indexed) {
312
+ let resolver = resolverByPath.get(reference.sourcePath);
313
+ if (!resolver) {
314
+ resolver = createWikiLinkResolver
315
+ ? createWikiLinkResolver(reference.sourcePath)
316
+ : Promise.resolve(async () => null);
317
+ resolverByPath.set(reference.sourcePath, resolver);
318
+ }
319
+ const rendered = await renderIndexedMarkdownReference(reference, latDir, projectRoot, await resolver);
320
+ located.push({
321
+ line: reference.line,
322
+ reference: {
323
+ sectionId: rendered.sectionId,
324
+ breadcrumbs: rendered.breadcrumbs,
325
+ paragraph: rendered.paragraph,
326
+ paragraphHtml: rendered.paragraphHtml,
327
+ url: rendered.url,
328
+ },
329
+ });
330
+ }
331
+ const context = origin
332
+ ? (located.find((candidate) => candidate.line === origin.line &&
333
+ candidate.reference.sectionId.toLowerCase() ===
334
+ origin.sectionId.toLowerCase())?.reference ?? null)
335
+ : null;
336
+ const otherSections = new Map();
337
+ for (const candidate of located) {
338
+ const key = candidate.reference.sectionId.toLowerCase();
339
+ if (context && key === context.sectionId.toLowerCase())
340
+ continue;
341
+ if (!otherSections.has(key))
342
+ otherSections.set(key, candidate.reference);
343
+ }
344
+ return { context, otherReferences: [...otherSections.values()] };
345
+ }
@@ -0,0 +1,12 @@
1
+ import { type Section } from '../lattice.js';
2
+ import type { ViewSourceDocument } from './protocol.js';
3
+ import { type SourceReferenceOrigin, type ViewReferenceIndex } from './references.js';
4
+ export declare class ViewDocumentNotFoundError extends Error {
5
+ }
6
+ export declare class ViewSourceNotFoundError extends Error {
7
+ }
8
+ export declare function createMarkdownWikiLinkResolver(latDir: string, requestedPath: string, loadedSections: Section[]): Promise<(target: string, context: {
9
+ line: number;
10
+ }) => Promise<string | null>>;
11
+ /** Read and highlight a source file after constraining it to the project root. */
12
+ export declare function getViewSource(latDir: string, projectRoot: string, requestedPath: string, requestedSymbol?: string, origin?: SourceReferenceOrigin, requestedLine?: number, allSections?: Section[], referenceIndex?: ViewReferenceIndex): Promise<ViewSourceDocument>;
@@ -0,0 +1,167 @@
1
+ import { readFile, realpath } from 'node:fs/promises';
2
+ import { dirname, extname, isAbsolute, relative, resolve, sep, } from 'node:path';
3
+ import { buildFileIndex, buildSectionSlugIndex, flattenSections, resolveRef, } from '../lattice.js';
4
+ import { resolveSourceSymbol, SOURCE_EXTENSIONS, } from '../source-parser.js';
5
+ import { toPosix } from '../walk.js';
6
+ import { highlightSource } from './highlight.js';
7
+ import { renderSourceReferenceContext, } from './references.js';
8
+ export class ViewDocumentNotFoundError extends Error {
9
+ }
10
+ export class ViewSourceNotFoundError extends Error {
11
+ }
12
+ function isInside(root, candidate) {
13
+ const rel = relative(root, candidate);
14
+ return (rel === '' ||
15
+ (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)));
16
+ }
17
+ function documentUrl(path) {
18
+ return `/docs/${path.split('/').map(encodeURIComponent).join('/')}`;
19
+ }
20
+ function sourceUrl(path, symbol, origin) {
21
+ const encodedPath = path.split('/').map(encodeURIComponent).join('/');
22
+ const query = new URLSearchParams();
23
+ if (origin) {
24
+ query.set('from', origin.sectionId);
25
+ query.set('line', String(origin.line));
26
+ }
27
+ const search = query.size > 0 ? `?${query}` : '';
28
+ const fragment = symbol ? `#${encodeURIComponent(symbol)}` : '';
29
+ return `/code/${encodedPath}${search}${fragment}`;
30
+ }
31
+ function sourceTarget(target) {
32
+ const hash = target.indexOf('#');
33
+ const path = hash === -1 ? target : target.slice(0, hash);
34
+ if (!SOURCE_EXTENSIONS.has(extname(path)))
35
+ return null;
36
+ return { path, symbol: hash === -1 ? '' : target.slice(hash + 1) };
37
+ }
38
+ function matchingSymbol(symbols, symbolPath) {
39
+ const parts = symbolPath.split('#');
40
+ if (parts.length === 1) {
41
+ return symbols.find((symbol) => symbol.name === parts[0] && !symbol.parent);
42
+ }
43
+ if (parts.length === 2) {
44
+ return symbols.find((symbol) => symbol.parent === parts[0] && symbol.name === parts[1]);
45
+ }
46
+ return undefined;
47
+ }
48
+ export async function createMarkdownWikiLinkResolver(latDir, requestedPath, loadedSections) {
49
+ const projectRoot = dirname(latDir);
50
+ const flat = flattenSections(loadedSections);
51
+ const sectionIds = new Set(flat.map((section) => section.id.toLowerCase()));
52
+ const fileIndex = buildFileIndex(loadedSections);
53
+ const slugIndex = buildSectionSlugIndex(loadedSections);
54
+ const byId = new Map(flat.map((section) => [section.id.toLowerCase(), section]));
55
+ const currentFile = toPosix(relative(projectRoot, resolve(latDir, requestedPath)));
56
+ const currentSections = flat
57
+ .filter((section) => section.filePath === currentFile)
58
+ .sort((a, b) => a.startLine - b.startLine);
59
+ return async (target, context) => {
60
+ const result = resolveRef(target, sectionIds, fileIndex, slugIndex);
61
+ if (result.ambiguous)
62
+ return null;
63
+ const section = byId.get(result.resolved.toLowerCase());
64
+ if (section) {
65
+ const absoluteFile = resolve(projectRoot, section.filePath);
66
+ const file = toPosix(relative(latDir, absoluteFile));
67
+ const fragment = target.includes('#') && section.githubSlug
68
+ ? `#${encodeURIComponent(section.githubSlug)}`
69
+ : '';
70
+ return `${documentUrl(file)}${fragment}`;
71
+ }
72
+ const source = sourceTarget(target);
73
+ if (!source)
74
+ return null;
75
+ try {
76
+ await readViewSource(projectRoot, source.path, source.symbol);
77
+ let section;
78
+ for (const candidate of currentSections) {
79
+ if (candidate.startLine > context.line)
80
+ break;
81
+ section = candidate;
82
+ }
83
+ const origin = section
84
+ ? { sectionId: section.id, line: context.line }
85
+ : undefined;
86
+ return sourceUrl(source.path, source.symbol, origin);
87
+ }
88
+ catch (error) {
89
+ if (error instanceof ViewSourceNotFoundError)
90
+ return null;
91
+ throw error;
92
+ }
93
+ };
94
+ }
95
+ async function readViewSource(projectRoot, requestedPath, requestedSymbol = '', requestedLine = 0) {
96
+ if (!requestedPath ||
97
+ requestedPath.includes('\\') ||
98
+ isAbsolute(requestedPath) ||
99
+ !SOURCE_EXTENSIONS.has(extname(requestedPath))) {
100
+ throw new ViewSourceNotFoundError('Source document not found');
101
+ }
102
+ const candidate = resolve(projectRoot, requestedPath);
103
+ let realRoot;
104
+ let realFile;
105
+ try {
106
+ [realRoot, realFile] = await Promise.all([
107
+ realpath(projectRoot),
108
+ realpath(candidate),
109
+ ]);
110
+ }
111
+ catch {
112
+ throw new ViewSourceNotFoundError('Source document not found');
113
+ }
114
+ if (!isInside(realRoot, realFile)) {
115
+ throw new ViewSourceNotFoundError('Source document not found');
116
+ }
117
+ const content = await readFile(realFile, 'utf-8');
118
+ if (!requestedSymbol) {
119
+ if (!requestedLine)
120
+ return { path: requestedPath, content, focus: null };
121
+ const line = content.split('\n')[requestedLine - 1];
122
+ if (line === undefined) {
123
+ throw new ViewSourceNotFoundError('Source line not found');
124
+ }
125
+ return {
126
+ path: requestedPath,
127
+ content,
128
+ focus: {
129
+ symbol: `line ${requestedLine}`,
130
+ kind: 'reference',
131
+ signature: line.trim() || `Line ${requestedLine}`,
132
+ startLine: requestedLine,
133
+ endLine: requestedLine,
134
+ },
135
+ };
136
+ }
137
+ const resolved = await resolveSourceSymbol(requestedPath, requestedSymbol, projectRoot);
138
+ const symbol = resolved.found
139
+ ? matchingSymbol(resolved.symbols, requestedSymbol)
140
+ : undefined;
141
+ if (!symbol) {
142
+ throw new ViewSourceNotFoundError('Source symbol not found');
143
+ }
144
+ return {
145
+ path: requestedPath,
146
+ content,
147
+ focus: {
148
+ symbol: requestedSymbol,
149
+ kind: symbol.kind,
150
+ signature: symbol.signature,
151
+ startLine: symbol.startLine,
152
+ endLine: symbol.endLine,
153
+ },
154
+ };
155
+ }
156
+ /** Read and highlight a source file after constraining it to the project root. */
157
+ export async function getViewSource(latDir, projectRoot, requestedPath, requestedSymbol = '', origin, requestedLine = 0, allSections = [], referenceIndex) {
158
+ const source = await readViewSource(projectRoot, requestedPath, requestedSymbol, requestedLine);
159
+ const references = referenceIndex
160
+ ? await renderSourceReferenceContext(referenceIndex, `${source.path}${requestedSymbol ? `#${requestedSymbol}` : ''}`, origin, latDir, projectRoot, (path) => createMarkdownWikiLinkResolver(latDir, path, allSections))
161
+ : { context: null, otherReferences: [] };
162
+ return {
163
+ ...source,
164
+ highlightedHtmlLines: highlightSource(source.path, source.content),
165
+ ...references,
166
+ };
167
+ }
@@ -0,0 +1,14 @@
1
+ import type { SectionMatch } from '../lattice.js';
2
+ import type { ViewSearchResponse } from './protocol.js';
3
+ export type ViewSearch = (query: string) => Promise<ViewSearchResponse>;
4
+ export type ViewSearchDependencies = {
5
+ runIndex: (latDir: string) => Promise<void>;
6
+ runSearch: (latDir: string, query: string, limit: number, options: {
7
+ buildIndex: false;
8
+ }) => Promise<{
9
+ query: string;
10
+ matches: SectionMatch[];
11
+ }>;
12
+ };
13
+ /** Create the lazily indexed semantic search service used by `lat ui`. */
14
+ export declare function createViewSearch(latDir: string, dependencies?: ViewSearchDependencies, getGeneration?: () => number): ViewSearch;
@@ -0,0 +1,64 @@
1
+ import { dirname, relative, resolve } from 'node:path';
2
+ import { toPosix } from '../walk.js';
3
+ const VIEW_SEARCH_LIMIT = 10;
4
+ const defaultDependencies = {
5
+ async runIndex(latDir) {
6
+ const { runIndex } = await import('../cli/search.js');
7
+ await runIndex(latDir);
8
+ },
9
+ async runSearch(latDir, query, limit, options) {
10
+ const { runSearch } = await import('../cli/search.js');
11
+ return runSearch(latDir, query, limit, undefined, options);
12
+ },
13
+ };
14
+ function documentUrl(path, headingId) {
15
+ const encodedPath = path.split('/').map(encodeURIComponent).join('/');
16
+ const fragment = headingId ? `#${encodeURIComponent(headingId)}` : '';
17
+ return `/docs/${encodedPath}${fragment}`;
18
+ }
19
+ function viewSearchResult(latDir, match) {
20
+ const section = match.section;
21
+ const projectRoot = dirname(latDir);
22
+ const path = toPosix(relative(latDir, resolve(projectRoot, section.filePath)));
23
+ const fileBreadcrumbs = path.replace(/\.md$/i, '').split('/');
24
+ return {
25
+ sectionId: section.id,
26
+ title: section.heading,
27
+ path,
28
+ breadcrumbs: [...fileBreadcrumbs, ...section.id.split('#').slice(1)],
29
+ description: section.firstParagraph,
30
+ url: documentUrl(path, section.githubSlug),
31
+ };
32
+ }
33
+ /** Create the lazily indexed semantic search service used by `lat ui`. */
34
+ export function createViewSearch(latDir, dependencies = defaultDependencies, getGeneration = () => 0) {
35
+ let indexReady = null;
36
+ let indexedGeneration = -1;
37
+ const prepareIndex = async () => {
38
+ while (indexedGeneration < getGeneration() || indexedGeneration < 0) {
39
+ if (!indexReady) {
40
+ const generation = getGeneration();
41
+ indexReady = dependencies
42
+ .runIndex(latDir)
43
+ .then(() => {
44
+ indexedGeneration = Math.max(indexedGeneration, generation);
45
+ })
46
+ .finally(() => {
47
+ indexReady = null;
48
+ });
49
+ }
50
+ await indexReady;
51
+ }
52
+ };
53
+ return async (rawQuery) => {
54
+ const query = rawQuery.trim();
55
+ if (!query)
56
+ return { query: '', results: [] };
57
+ await prepareIndex();
58
+ const search = await dependencies.runSearch(latDir, query, VIEW_SEARCH_LIMIT, { buildIndex: false });
59
+ return {
60
+ query: search.query,
61
+ results: search.matches.map((match) => viewSearchResult(latDir, match)),
62
+ };
63
+ };
64
+ }
@@ -0,0 +1,20 @@
1
+ import { type Server } from 'node:http';
2
+ import type { CmdContext } from '../context.js';
3
+ import { type ViewSearch } from './search.js';
4
+ import { type ViewStore } from './store.js';
5
+ export type ViewServer = {
6
+ server: Server;
7
+ store: ViewStore;
8
+ url: string;
9
+ close: () => Promise<void>;
10
+ };
11
+ export type ViewServerOptions = {
12
+ clientDir?: string;
13
+ git?: boolean;
14
+ host?: string;
15
+ port?: number;
16
+ search?: ViewSearch;
17
+ watch?: boolean;
18
+ };
19
+ /** Start the read-only loopback server used by `lat ui`. */
20
+ export declare function startViewServer(ctx: CmdContext, options?: ViewServerOptions): Promise<ViewServer>;