@plebeiantech/lat.md 0.12.3-fork.4 → 0.12.3-fork.6

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 (64) hide show
  1. package/dist/src/cli/check-coverage.d.ts +2 -0
  2. package/dist/src/cli/check-coverage.js +71 -0
  3. package/dist/src/cli/check-mode.d.ts +6 -0
  4. package/dist/src/cli/check-mode.js +44 -2
  5. package/dist/src/cli/check.js +5 -1
  6. package/dist/src/cli/fork-instructions.d.ts +19 -0
  7. package/dist/src/cli/fork-instructions.js +183 -0
  8. package/dist/src/cli/fork-scaffold.d.ts +16 -0
  9. package/dist/src/cli/fork-scaffold.js +129 -0
  10. package/dist/src/cli/index.js +29 -4
  11. package/dist/src/cli/init.js +4 -0
  12. package/dist/src/cli/search.js +6 -4
  13. package/dist/src/cli/ui-build.d.ts +4 -0
  14. package/dist/src/cli/ui-build.js +13 -0
  15. package/dist/src/cli/ui.js +18 -2
  16. package/dist/src/code-refs.js +25 -4
  17. package/dist/src/fork/upstream-drift-cli.d.ts +1 -0
  18. package/dist/src/fork/upstream-drift-cli.js +2 -0
  19. package/dist/src/fork/upstream-drift.d.ts +26 -0
  20. package/dist/src/fork/upstream-drift.js +185 -0
  21. package/dist/src/fork/upstream-guard-cli.d.ts +1 -0
  22. package/dist/src/fork/upstream-guard-cli.js +2 -0
  23. package/dist/src/fork/upstream-guard.d.ts +72 -0
  24. package/dist/src/fork/upstream-guard.js +445 -0
  25. package/dist/src/init-version.d.ts +1 -1
  26. package/dist/src/init-version.js +1 -1
  27. package/dist/src/lattice.d.ts +2 -1
  28. package/dist/src/lattice.js +1 -0
  29. package/dist/src/search/search.d.ts +1 -0
  30. package/dist/src/search/search.js +15 -9
  31. package/dist/src/view/client/assets/index-VZwPPiFE.js +334 -0
  32. package/dist/src/view/client/assets/index-W8ktvNj9.css +1 -0
  33. package/dist/src/view/client/assets/logo-CNF8zs47.svg +182 -0
  34. package/dist/src/view/client/index.html +2 -2
  35. package/dist/src/view/git-diff.js +17 -0
  36. package/dist/src/view/graph.d.ts +6 -0
  37. package/dist/src/view/graph.js +176 -0
  38. package/dist/src/view/markdown.d.ts +5 -1
  39. package/dist/src/view/markdown.js +112 -25
  40. package/dist/src/view/protocol.d.ts +43 -0
  41. package/dist/src/view/protocol.js +1 -1
  42. package/dist/src/view/references.d.ts +2 -0
  43. package/dist/src/view/references.js +30 -8
  44. package/dist/src/view/repository.d.ts +5 -2
  45. package/dist/src/view/repository.js +13 -12
  46. package/dist/src/view/search.js +1 -0
  47. package/dist/src/view/server.d.ts +3 -0
  48. package/dist/src/view/server.js +42 -9
  49. package/dist/src/view/source-target.d.ts +8 -0
  50. package/dist/src/view/source-target.js +22 -0
  51. package/dist/src/view/static-build.d.ts +16 -0
  52. package/dist/src/view/static-build.js +408 -0
  53. package/dist/src/view/static-protocol.d.ts +23 -0
  54. package/dist/src/view/static-protocol.js +10 -0
  55. package/dist/src/view/store.d.ts +9 -1
  56. package/dist/src/view/store.js +82 -15
  57. package/dist/src/view/table-of-contents.d.ts +9 -0
  58. package/dist/src/view/table-of-contents.js +103 -0
  59. package/package.json +3 -1
  60. package/templates/AGENTS.md +2 -13
  61. package/templates/fork/conventions.md +95 -0
  62. package/templates/skill/SKILL.md +3 -14
  63. package/dist/src/view/client/assets/index-B0n0u03y.css +0 -1
  64. package/dist/src/view/client/assets/index-xm2_px4q.js +0 -23
@@ -10,6 +10,7 @@ const CODE_LINK_CLASSES = [
10
10
  'wiki-link-code',
11
11
  'wiki-link-active',
12
12
  'code-link-language',
13
+ 'code-link-leading',
13
14
  'code-language-ts',
14
15
  'code-language-js',
15
16
  'code-language-py',
@@ -18,11 +19,17 @@ const CODE_LINK_CLASSES = [
18
19
  'code-language-c',
19
20
  ];
20
21
  const ERROR_CLASS = 'markdown-error';
22
+ const EXTERNAL_LINK_CLASS = 'external-link';
23
+ const EXTERNAL_LINK_ICON_CLASS = 'external-link-icon';
21
24
  const GIT_CLASSES = ['git-added', 'git-removed'];
22
25
  function classAttributes(tag) {
26
+ const attributes = defaultSchema.attributes?.[tag] ?? [];
27
+ const allowedClasses = attributes.flatMap((attribute) => Array.isArray(attribute) && attribute[0] === 'className'
28
+ ? attribute.slice(1)
29
+ : []);
23
30
  return [
24
- ...(defaultSchema.attributes?.[tag] ?? []),
25
- ['className', ERROR_CLASS, ...GIT_CLASSES],
31
+ ...attributes.filter((attribute) => !(Array.isArray(attribute) && attribute[0] === 'className')),
32
+ ['className', ...allowedClasses, ERROR_CLASS, ...GIT_CLASSES],
26
33
  ];
27
34
  }
28
35
  const sanitizeSchema = {
@@ -38,6 +45,7 @@ const sanitizeSchema = {
38
45
  'wiki-link-segmented',
39
46
  'wiki-link-code',
40
47
  'wiki-link-active',
48
+ EXTERNAL_LINK_CLASS,
41
49
  ERROR_CLASS,
42
50
  ...GIT_CLASSES,
43
51
  ],
@@ -55,20 +63,25 @@ const sanitizeSchema = {
55
63
  img: classAttributes('img'),
56
64
  ins: classAttributes('ins'),
57
65
  li: classAttributes('li'),
66
+ ol: classAttributes('ol'),
58
67
  p: classAttributes('p'),
59
68
  pre: classAttributes('pre'),
60
69
  span: [
61
70
  ...(defaultSchema.attributes?.span ?? []),
62
71
  'ariaHidden',
72
+ 'ariaLabel',
63
73
  [
64
74
  'className',
65
75
  'wiki-link-context',
66
76
  'wiki-link-leaf',
77
+ 'wiki-link-ref-count',
78
+ EXTERNAL_LINK_ICON_CLASS,
67
79
  ...CODE_LINK_CLASSES.slice(2),
68
80
  ERROR_CLASS,
69
81
  ...GIT_CLASSES,
70
82
  ],
71
83
  ],
84
+ ul: classAttributes('ul'),
72
85
  },
73
86
  };
74
87
  const htmlProcessor = unified()
@@ -155,6 +168,61 @@ function languageIcon(language) {
155
168
  children: [{ type: 'text', value: language.label }],
156
169
  };
157
170
  }
171
+ function codeLinkContent(language, children) {
172
+ const [first, ...rest] = children;
173
+ if (!first)
174
+ return [languageIcon(language)];
175
+ let leading = first;
176
+ let remainder = null;
177
+ if (first.type === 'text') {
178
+ const breakAt = first.value.search(/\s/);
179
+ if (breakAt > 0) {
180
+ leading = { ...first, value: first.value.slice(0, breakAt) };
181
+ remainder = { ...first, value: first.value.slice(breakAt) };
182
+ }
183
+ }
184
+ return [
185
+ {
186
+ type: 'emphasis',
187
+ data: {
188
+ hName: 'span',
189
+ hProperties: { className: ['code-link-leading'] },
190
+ },
191
+ children: [languageIcon(language), leading],
192
+ },
193
+ ...(remainder ? [remainder] : []),
194
+ ...rest,
195
+ ];
196
+ }
197
+ function externalLinkIcon() {
198
+ return {
199
+ type: 'emphasis',
200
+ data: {
201
+ hName: 'span',
202
+ hProperties: {
203
+ ariaHidden: 'true',
204
+ className: [EXTERNAL_LINK_ICON_CLASS],
205
+ },
206
+ },
207
+ children: [],
208
+ };
209
+ }
210
+ function isExternalSiteUrl(url) {
211
+ return /^(?:https?:)?\/\//i.test(url);
212
+ }
213
+ function referenceCountBadge(count) {
214
+ return {
215
+ type: 'emphasis',
216
+ data: {
217
+ hName: 'span',
218
+ hProperties: {
219
+ className: ['wiki-link-ref-count'],
220
+ ariaLabel: `${count} ${count === 1 ? 'reference' : 'references'}`,
221
+ },
222
+ },
223
+ children: [{ type: 'text', value: String(count) }],
224
+ };
225
+ }
158
226
  function targetMatches(node, target) {
159
227
  if (node.type === 'wikiLink') {
160
228
  return node.value.toLowerCase() === target.toLowerCase();
@@ -229,24 +297,39 @@ export async function renderMarkdown(markdown, filePath, resolveWikiLink, option
229
297
  tree.children = tree.children.filter((node) => node.type !== 'yaml');
230
298
  if (options.errors)
231
299
  markMarkdownErrors(tree, options.errors);
232
- if (options.rewriteMarkdownLink || options.activeMarkdownLink) {
233
- visit(tree, 'link', (node) => {
234
- const authoredUrl = node.url;
235
- if (options.activeMarkdownLink &&
236
- authoredUrl === options.activeMarkdownLink) {
237
- node.data = {
238
- ...node.data,
239
- hProperties: {
240
- ...(node.data?.hProperties ?? {}),
241
- className: ['wiki-link-active'],
242
- },
243
- };
244
- }
245
- if (options.rewriteMarkdownLink) {
246
- node.url = options.rewriteMarkdownLink(authoredUrl);
247
- }
248
- });
249
- }
300
+ visit(tree, 'link', (node) => {
301
+ const authoredUrl = node.url;
302
+ if (options.activeMarkdownLink &&
303
+ authoredUrl === options.activeMarkdownLink) {
304
+ node.data = {
305
+ ...node.data,
306
+ hProperties: {
307
+ ...(node.data?.hProperties ?? {}),
308
+ className: ['wiki-link-active'],
309
+ },
310
+ };
311
+ }
312
+ if (options.rewriteMarkdownLink) {
313
+ node.url = options.rewriteMarkdownLink(authoredUrl);
314
+ }
315
+ if (!isExternalSiteUrl(node.url))
316
+ return;
317
+ const properties = node.data?.hProperties ?? {};
318
+ const currentClasses = properties.className;
319
+ const classes = Array.isArray(currentClasses)
320
+ ? currentClasses.map(String)
321
+ : currentClasses
322
+ ? [String(currentClasses)]
323
+ : [];
324
+ node.data = {
325
+ ...node.data,
326
+ hProperties: {
327
+ ...properties,
328
+ className: [...classes, EXTERNAL_LINK_CLASS],
329
+ },
330
+ };
331
+ node.children.push(externalLinkIcon());
332
+ });
250
333
  const firstHeading = tree.children.find((node) => node.type === 'heading');
251
334
  const title = firstHeading
252
335
  ? nodeText(firstHeading)
@@ -266,9 +349,10 @@ export async function renderMarkdown(markdown, filePath, resolveWikiLink, option
266
349
  visit(tree, 'wikiLink', (node, index, parent) => {
267
350
  if (index === undefined || !parent || !('children' in parent))
268
351
  return;
269
- const href = resolvedLinks.get(node);
352
+ const resolution = resolvedLinks.get(node);
270
353
  const markedProperties = node.data?.hProperties;
271
- if (href) {
354
+ if (resolution) {
355
+ const { href, referenceCount } = resolution;
272
356
  const content = wikiLinkContent(node);
273
357
  const language = href.startsWith('/code/')
274
358
  ? codeLanguage(node.value)
@@ -296,9 +380,12 @@ export async function renderMarkdown(markdown, filePath, resolveWikiLink, option
296
380
  },
297
381
  }
298
382
  : undefined,
299
- children: language
300
- ? [languageIcon(language), ...content.children]
301
- : content.children,
383
+ children: [
384
+ ...(language
385
+ ? codeLinkContent(language, content.children)
386
+ : content.children),
387
+ ...(referenceCount > 1 ? [referenceCountBadge(referenceCount)] : []),
388
+ ],
302
389
  };
303
390
  return;
304
391
  }
@@ -7,7 +7,9 @@ export type ViewIndex = {
7
7
  entry: string;
8
8
  errorCounts: Record<string, number>;
9
9
  git: ViewGitState | null;
10
+ logoText: string;
10
11
  };
12
+ export declare const DEFAULT_VIEW_LOGO_TEXT = "lat.md";
11
13
  export type ViewDocumentError = {
12
14
  anchor: string;
13
15
  line: number;
@@ -22,6 +24,7 @@ export type ViewSearchResult = {
22
24
  breadcrumbs: string[];
23
25
  description: string;
24
26
  url: string;
27
+ score: number;
25
28
  };
26
29
  export type ViewSearchResponse = {
27
30
  query: string;
@@ -31,11 +34,51 @@ export type ViewProjectChange = {
31
34
  generation: number;
32
35
  markdownGeneration: number;
33
36
  };
37
+ export type ViewGraphNodeKind = 'document' | 'source' | 'code-reference';
38
+ export type ViewGraphNode = {
39
+ id: string;
40
+ kind: ViewGraphNodeKind;
41
+ label: string;
42
+ url: string;
43
+ breadcrumbs: string[];
44
+ inDegree: number;
45
+ outDegree: number;
46
+ documentPath?: string;
47
+ sectionId?: string;
48
+ sourcePath?: string;
49
+ symbol?: string;
50
+ line?: number;
51
+ snippet?: string;
52
+ gitStatus?: ViewGitFileStatus;
53
+ errorCount?: number;
54
+ };
55
+ export type ViewGraphEdgeKind = 'wiki' | 'markdown' | 'source' | 'code-mention';
56
+ export type ViewGraphEdge = {
57
+ id: string;
58
+ from: string;
59
+ to: string;
60
+ kind: ViewGraphEdgeKind;
61
+ weight: number;
62
+ };
63
+ export type ViewGraph = {
64
+ generation: number;
65
+ nodes: ViewGraphNode[];
66
+ edges: ViewGraphEdge[];
67
+ };
68
+ export type ViewDocumentTocItem = {
69
+ id: string;
70
+ title: string;
71
+ depth: number;
72
+ errorCount: number;
73
+ hasGitChanges: boolean;
74
+ };
34
75
  export type ViewDocument = {
35
76
  path: string;
36
77
  title: string;
37
78
  html: string;
38
79
  gitHtml: string | null;
80
+ graphNodeIds: Record<string, string>;
81
+ tableOfContents: ViewDocumentTocItem[];
39
82
  errors: ViewDocumentError[];
40
83
  backReferences: ViewSectionBackReferences[];
41
84
  frontmatter: {
@@ -1 +1 @@
1
- export {};
1
+ export const DEFAULT_VIEW_LOGO_TEXT = 'lat.md';
@@ -47,9 +47,11 @@ type IndexedBackReference = IndexedMarkdownReference | ViewCodeBackReference;
47
47
  export type ViewReferenceIndex = {
48
48
  incomingBySection: ReadonlyMap<string, readonly IndexedBackReference[]>;
49
49
  sourceByTarget: ReadonlyMap<string, readonly IndexedMarkdownReference[]>;
50
+ sourceReferenceCounts: ReadonlyMap<string, number>;
50
51
  };
51
52
  /** Parse one Markdown file once into every structure needed by the view store. */
52
53
  export declare function parseViewMarkdownFile(absolutePath: string, content: string, latDir: string, projectRoot: string): ViewParsedMarkdownFile;
54
+ export declare function linkedSection(url: string, sourcePath: string, sectionsByPath: ReadonlyMap<string, Section[]>): Section | null;
53
55
  /** Resolve cached outgoing occurrences into direct section and source indexes. */
54
56
  export declare function buildViewReferenceIndex(markdownFiles: Iterable<ViewParsedMarkdownFile>, codeFiles: Iterable<ViewCodeReferenceFile>, allSections: Section[]): ViewReferenceIndex;
55
57
  /** Render direct reverse-index entries for the sections visible in one file. */
@@ -4,6 +4,7 @@ import { buildFileIndex, buildSectionSlugIndex, extractRefs, extractLinks, flatt
4
4
  import { parse } from '../parser.js';
5
5
  import { toPosix } from '../walk.js';
6
6
  import { renderMarkdown } from './markdown.js';
7
+ import { viewSourceTarget } from './source-target.js';
7
8
  function inlineText(node) {
8
9
  if (node.type === 'wikiLink')
9
10
  return node.data.alias ?? node.value;
@@ -115,7 +116,7 @@ function sourceLineUrl(path, line) {
115
116
  const encoded = path.split('/').map(encodeURIComponent).join('/');
116
117
  return `/code/${encoded}?at=${line}`;
117
118
  }
118
- function linkedSection(url, sourcePath, sectionsByPath) {
119
+ export function linkedSection(url, sourcePath, sectionsByPath) {
119
120
  if (url.startsWith('//') || /^[a-z][a-z\d+.-]*:/i.test(url))
120
121
  return null;
121
122
  const encodedSourcePath = sourcePath
@@ -184,6 +185,15 @@ export function buildViewReferenceIndex(markdownFiles, codeFiles, allSections) {
184
185
  if (!references.has(key))
185
186
  references.set(key, reference);
186
187
  };
188
+ const addSourceReference = (target, key, reference) => {
189
+ let references = sourceByTarget.get(target);
190
+ if (!references) {
191
+ references = new Map();
192
+ sourceByTarget.set(target, references);
193
+ }
194
+ if (!references.has(key))
195
+ references.set(key, reference);
196
+ };
187
197
  for (const file of files) {
188
198
  const fileSections = flattenSections(file.sections).sort((a, b) => a.startLine - b.startLine);
189
199
  for (const ref of file.wikiRefs) {
@@ -198,16 +208,20 @@ export function buildViewReferenceIndex(markdownFiles, codeFiles, allSections) {
198
208
  paragraph: paragraphFor(file, section, ref.line),
199
209
  activeWikiLink: ref.target,
200
210
  };
201
- const sourceKey = ref.target.toLowerCase();
202
- const sourceReferences = sourceByTarget.get(sourceKey) ?? [];
203
- sourceReferences.push(reference);
204
- sourceByTarget.set(sourceKey, sourceReferences);
211
+ const locationKey = `markdown:${section.filePath}:${reference.paragraph.startLine}`;
212
+ const source = viewSourceTarget(ref.target);
213
+ if (source) {
214
+ addSourceReference(source.key, locationKey, reference);
215
+ if (source.symbol) {
216
+ addSourceReference(source.fileKey, locationKey, reference);
217
+ }
218
+ }
205
219
  const resolved = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
206
220
  if (resolved.ambiguous ||
207
221
  !sectionById.has(resolved.resolved.toLowerCase())) {
208
222
  continue;
209
223
  }
210
- addIncoming(resolved.resolved, `markdown:${section.filePath}:${reference.paragraph.startLine}`, reference);
224
+ addIncoming(resolved.resolved, locationKey, reference);
211
225
  }
212
226
  for (const link of file.markdownLinks) {
213
227
  if (link.kind !== 'link')
@@ -252,7 +266,14 @@ export function buildViewReferenceIndex(markdownFiles, codeFiles, allSections) {
252
266
  target,
253
267
  [...references.values()],
254
268
  ])),
255
- sourceByTarget,
269
+ sourceByTarget: new Map([...sourceByTarget].map(([target, references]) => [
270
+ target,
271
+ [...references.values()],
272
+ ])),
273
+ sourceReferenceCounts: new Map([...sourceByTarget].map(([target, references]) => [
274
+ target,
275
+ references.size,
276
+ ])),
256
277
  };
257
278
  }
258
279
  async function renderIndexedMarkdownReference(reference, latDir, projectRoot, resolveWikiLink) {
@@ -305,7 +326,8 @@ export async function renderSectionBackReferences(index, visibleSections, latDir
305
326
  }
306
327
  /** Render cached Markdown occurrences that point to one source target. */
307
328
  export async function renderSourceReferenceContext(index, target, origin, latDir, projectRoot, createWikiLinkResolver) {
308
- const indexed = index.sourceByTarget.get(target.toLowerCase()) ?? [];
329
+ const source = viewSourceTarget(target);
330
+ const indexed = source ? (index.sourceByTarget.get(source.key) ?? []) : [];
309
331
  const resolverByPath = new Map();
310
332
  const located = [];
311
333
  for (const reference of indexed) {
@@ -5,8 +5,11 @@ export declare class ViewDocumentNotFoundError extends Error {
5
5
  }
6
6
  export declare class ViewSourceNotFoundError extends Error {
7
7
  }
8
- export declare function createMarkdownWikiLinkResolver(latDir: string, requestedPath: string, loadedSections: Section[]): Promise<(target: string, context: {
8
+ export declare function createMarkdownWikiLinkResolver(latDir: string, requestedPath: string, loadedSections: Section[], referenceIndex?: ViewReferenceIndex): Promise<(target: string, context: {
9
9
  line: number;
10
- }) => Promise<string | null>>;
10
+ }) => Promise<{
11
+ href: string;
12
+ referenceCount: number;
13
+ } | null>>;
11
14
  /** Read and highlight a source file after constraining it to the project root. */
12
15
  export declare function getViewSource(latDir: string, projectRoot: string, requestedPath: string, requestedSymbol?: string, origin?: SourceReferenceOrigin, requestedLine?: number, allSections?: Section[], referenceIndex?: ViewReferenceIndex): Promise<ViewSourceDocument>;
@@ -5,6 +5,7 @@ import { resolveSourceSymbol, SOURCE_EXTENSIONS, } from '../source-parser.js';
5
5
  import { toPosix } from '../walk.js';
6
6
  import { highlightSource } from './highlight.js';
7
7
  import { renderSourceReferenceContext, } from './references.js';
8
+ import { viewSourceTarget } from './source-target.js';
8
9
  export class ViewDocumentNotFoundError extends Error {
9
10
  }
10
11
  export class ViewSourceNotFoundError extends Error {
@@ -28,13 +29,6 @@ function sourceUrl(path, symbol, origin) {
28
29
  const fragment = symbol ? `#${encodeURIComponent(symbol)}` : '';
29
30
  return `/code/${encodedPath}${search}${fragment}`;
30
31
  }
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
32
  function matchingSymbol(symbols, symbolPath) {
39
33
  const parts = symbolPath.split('#');
40
34
  if (parts.length === 1) {
@@ -45,7 +39,7 @@ function matchingSymbol(symbols, symbolPath) {
45
39
  }
46
40
  return undefined;
47
41
  }
48
- export async function createMarkdownWikiLinkResolver(latDir, requestedPath, loadedSections) {
42
+ export async function createMarkdownWikiLinkResolver(latDir, requestedPath, loadedSections, referenceIndex) {
49
43
  const projectRoot = dirname(latDir);
50
44
  const flat = flattenSections(loadedSections);
51
45
  const sectionIds = new Set(flat.map((section) => section.id.toLowerCase()));
@@ -67,9 +61,13 @@ export async function createMarkdownWikiLinkResolver(latDir, requestedPath, load
67
61
  const fragment = target.includes('#') && section.githubSlug
68
62
  ? `#${encodeURIComponent(section.githubSlug)}`
69
63
  : '';
70
- return `${documentUrl(file)}${fragment}`;
64
+ return {
65
+ href: `${documentUrl(file)}${fragment}`,
66
+ referenceCount: referenceIndex?.incomingBySection.get(section.id.toLowerCase())
67
+ ?.length ?? 0,
68
+ };
71
69
  }
72
- const source = sourceTarget(target);
70
+ const source = viewSourceTarget(target);
73
71
  if (!source)
74
72
  return null;
75
73
  try {
@@ -83,7 +81,10 @@ export async function createMarkdownWikiLinkResolver(latDir, requestedPath, load
83
81
  const origin = section
84
82
  ? { sectionId: section.id, line: context.line }
85
83
  : undefined;
86
- return sourceUrl(source.path, source.symbol, origin);
84
+ return {
85
+ href: sourceUrl(source.path, source.symbol, origin),
86
+ referenceCount: referenceIndex?.sourceReferenceCounts.get(source.key) ?? 0,
87
+ };
87
88
  }
88
89
  catch (error) {
89
90
  if (error instanceof ViewSourceNotFoundError)
@@ -157,7 +158,7 @@ async function readViewSource(projectRoot, requestedPath, requestedSymbol = '',
157
158
  export async function getViewSource(latDir, projectRoot, requestedPath, requestedSymbol = '', origin, requestedLine = 0, allSections = [], referenceIndex) {
158
159
  const source = await readViewSource(projectRoot, requestedPath, requestedSymbol, requestedLine);
159
160
  const references = referenceIndex
160
- ? await renderSourceReferenceContext(referenceIndex, `${source.path}${requestedSymbol ? `#${requestedSymbol}` : ''}`, origin, latDir, projectRoot, (path) => createMarkdownWikiLinkResolver(latDir, path, allSections))
161
+ ? await renderSourceReferenceContext(referenceIndex, `${source.path}${requestedSymbol ? `#${requestedSymbol}` : ''}`, origin, latDir, projectRoot, (path) => createMarkdownWikiLinkResolver(latDir, path, allSections, referenceIndex))
161
162
  : { context: null, otherReferences: [] };
162
163
  return {
163
164
  ...source,
@@ -28,6 +28,7 @@ function viewSearchResult(latDir, match) {
28
28
  breadcrumbs: [...fileBreadcrumbs, ...section.id.split('#').slice(1)],
29
29
  description: section.firstParagraph,
30
30
  url: documentUrl(path, section.githubSlug),
31
+ score: match.score ?? 0,
31
32
  };
32
33
  }
33
34
  /** Create the lazily indexed semantic search service used by `lat ui`. */
@@ -2,6 +2,7 @@ import { type Server } from 'node:http';
2
2
  import type { CmdContext } from '../context.js';
3
3
  import { type ViewSearch } from './search.js';
4
4
  import { type ViewStore } from './store.js';
5
+ export declare const DEFAULT_VIEW_PORT = 4242;
5
6
  export type ViewServer = {
6
7
  server: Server;
7
8
  store: ViewStore;
@@ -11,7 +12,9 @@ export type ViewServer = {
11
12
  export type ViewServerOptions = {
12
13
  clientDir?: string;
13
14
  git?: boolean;
15
+ gitPollMs?: number;
14
16
  host?: string;
17
+ logoText?: string;
15
18
  port?: number;
16
19
  search?: ViewSearch;
17
20
  watch?: boolean;
@@ -2,16 +2,18 @@ import { readFile } from 'node:fs/promises';
2
2
  import { createServer } from 'node:http';
3
3
  import { extname, join, relative, resolve, sep } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { DEFAULT_VIEW_LOGO_TEXT, } from './protocol.js';
5
6
  import { ViewDocumentNotFoundError, ViewSourceNotFoundError, } from './repository.js';
6
7
  import { createViewSearch } from './search.js';
7
8
  import { createViewStore } from './store.js';
8
9
  const DEFAULT_HOST = '127.0.0.1';
10
+ export const DEFAULT_VIEW_PORT = 4242;
9
11
  const defaultClientDir = fileURLToPath(new URL('./client/', import.meta.url));
10
12
  function documentUrl(path) {
11
13
  return `/docs/${path.split('/').map(encodeURIComponent).join('/')}`;
12
14
  }
13
15
  function setSecurityHeaders(res) {
14
- res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
16
+ res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
15
17
  res.setHeader('Referrer-Policy', 'no-referrer');
16
18
  res.setHeader('X-Content-Type-Options', 'nosniff');
17
19
  }
@@ -69,12 +71,29 @@ async function sendClientFile(res, path, headOnly, immutable = false) {
69
71
  send(res, 404, 'text/plain; charset=utf-8', 'Not found', headOnly);
70
72
  }
71
73
  }
74
+ function listen(server, host, port) {
75
+ return new Promise((resolveListen, reject) => {
76
+ const onError = (error) => {
77
+ server.off('listening', onListening);
78
+ reject(error);
79
+ };
80
+ const onListening = () => {
81
+ server.off('error', onError);
82
+ resolveListen();
83
+ };
84
+ server.once('error', onError);
85
+ server.once('listening', onListening);
86
+ server.listen(port, host);
87
+ });
88
+ }
72
89
  /** Start the read-only loopback server used by `lat ui`. */
73
90
  export async function startViewServer(ctx, options = {}) {
74
91
  const host = options.host ?? DEFAULT_HOST;
75
92
  const clientDir = options.clientDir ?? defaultClientDir;
93
+ const logoText = options.logoText ?? DEFAULT_VIEW_LOGO_TEXT;
76
94
  const store = await createViewStore(ctx.latDir, ctx.projectRoot, {
77
95
  git: options.git,
96
+ gitPollMs: options.gitPollMs,
78
97
  watch: options.watch,
79
98
  });
80
99
  const search = options.search ??
@@ -109,7 +128,11 @@ export async function startViewServer(ctx, options = {}) {
109
128
  return;
110
129
  }
111
130
  if (url.pathname === '/api/index') {
112
- sendJson(res, 200, store.getIndex(), headOnly);
131
+ sendJson(res, 200, { ...store.getIndex(), logoText }, headOnly);
132
+ return;
133
+ }
134
+ if (url.pathname === '/api/graph') {
135
+ sendJson(res, 200, store.getGraph(), headOnly);
113
136
  return;
114
137
  }
115
138
  if (url.pathname === '/api/events') {
@@ -180,6 +203,7 @@ export async function startViewServer(ctx, options = {}) {
180
203
  return;
181
204
  }
182
205
  if (url.pathname === '/search' ||
206
+ url.pathname === '/graph' ||
183
207
  url.pathname.startsWith('/docs/') ||
184
208
  url.pathname.startsWith('/code/')) {
185
209
  await sendClientFile(res, join(clientDir, 'index.html'), headOnly);
@@ -195,13 +219,22 @@ export async function startViewServer(ctx, options = {}) {
195
219
  });
196
220
  });
197
221
  try {
198
- await new Promise((resolveListen, reject) => {
199
- server.once('error', reject);
200
- server.listen(options.port ?? 0, host, () => {
201
- server.off('error', reject);
202
- resolveListen();
203
- });
204
- });
222
+ const requestedPort = options.port;
223
+ let port = requestedPort ?? DEFAULT_VIEW_PORT;
224
+ while (true) {
225
+ try {
226
+ await listen(server, host, port);
227
+ break;
228
+ }
229
+ catch (error) {
230
+ if (requestedPort !== undefined ||
231
+ error.code !== 'EADDRINUSE' ||
232
+ port === 65_535) {
233
+ throw error;
234
+ }
235
+ port++;
236
+ }
237
+ }
205
238
  }
206
239
  catch (error) {
207
240
  unsubscribeStore();
@@ -0,0 +1,8 @@
1
+ export type ViewSourceTarget = {
2
+ path: string;
3
+ symbol: string;
4
+ key: string;
5
+ fileKey: string;
6
+ };
7
+ /** Normalize a supported source wiki-link target for view indexes and routes. */
8
+ export declare function viewSourceTarget(target: string): ViewSourceTarget | null;
@@ -0,0 +1,22 @@
1
+ import { extname, posix } from 'node:path';
2
+ import { SOURCE_EXTENSIONS } from '../source-parser.js';
3
+ /** Normalize a supported source wiki-link target for view indexes and routes. */
4
+ export function viewSourceTarget(target) {
5
+ const hash = target.indexOf('#');
6
+ const authoredPath = hash === -1 ? target : target.slice(0, hash);
7
+ if (!authoredPath ||
8
+ authoredPath.includes('\\') ||
9
+ posix.isAbsolute(authoredPath) ||
10
+ !SOURCE_EXTENSIONS.has(extname(authoredPath))) {
11
+ return null;
12
+ }
13
+ const path = posix.normalize(authoredPath).replace(/^\.\//, '');
14
+ const symbol = hash === -1 ? '' : target.slice(hash + 1);
15
+ const fileKey = path.toLowerCase();
16
+ return {
17
+ path,
18
+ symbol,
19
+ key: `${fileKey}${symbol ? `#${symbol.toLowerCase()}` : ''}`,
20
+ fileKey,
21
+ };
22
+ }
@@ -0,0 +1,16 @@
1
+ import type { CmdContext } from '../context.js';
2
+ export type StaticViewBuildOptions = {
3
+ basePath?: string;
4
+ clientDir?: string;
5
+ logoText?: string;
6
+ };
7
+ export type StaticViewBuildResult = {
8
+ documents: number;
9
+ outputDir: string;
10
+ sources: number;
11
+ };
12
+ export declare function normalizeStaticViewBasePath(value: string): string;
13
+ /** Convert a live view route to its physical static-directory URL. */
14
+ export declare function staticViewUrl(value: string, basePath: string): string;
15
+ /** Build a serverless snapshot of the current read-only Lat UI. */
16
+ export declare function buildStaticView(ctx: CmdContext, requestedOutput: string, options?: StaticViewBuildOptions): Promise<StaticViewBuildResult>;