@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,233 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createServer } from 'node:http';
3
+ import { extname, join, relative, resolve, sep } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { ViewDocumentNotFoundError, ViewSourceNotFoundError, } from './repository.js';
6
+ import { createViewSearch } from './search.js';
7
+ import { createViewStore } from './store.js';
8
+ const DEFAULT_HOST = '127.0.0.1';
9
+ const defaultClientDir = fileURLToPath(new URL('./client/', import.meta.url));
10
+ function documentUrl(path) {
11
+ return `/docs/${path.split('/').map(encodeURIComponent).join('/')}`;
12
+ }
13
+ 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'");
15
+ res.setHeader('Referrer-Policy', 'no-referrer');
16
+ res.setHeader('X-Content-Type-Options', 'nosniff');
17
+ }
18
+ function send(res, status, contentType, body, headOnly = false) {
19
+ res.statusCode = status;
20
+ res.setHeader('Content-Type', contentType);
21
+ res.setHeader('Content-Length', Buffer.byteLength(body));
22
+ res.end(headOnly ? undefined : body);
23
+ }
24
+ function sendJson(res, status, value, headOnly) {
25
+ res.setHeader('Cache-Control', 'no-store');
26
+ send(res, status, 'application/json; charset=utf-8', JSON.stringify(value), headOnly);
27
+ }
28
+ function contentType(path) {
29
+ switch (extname(path)) {
30
+ case '.css':
31
+ return 'text/css; charset=utf-8';
32
+ case '.html':
33
+ return 'text/html; charset=utf-8';
34
+ case '.js':
35
+ return 'text/javascript; charset=utf-8';
36
+ case '.json':
37
+ return 'application/json; charset=utf-8';
38
+ case '.svg':
39
+ return 'image/svg+xml';
40
+ case '.woff2':
41
+ return 'font/woff2';
42
+ default:
43
+ return 'application/octet-stream';
44
+ }
45
+ }
46
+ function clientPath(clientDir, requestPath) {
47
+ let decoded;
48
+ try {
49
+ decoded = decodeURIComponent(requestPath);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ const candidate = resolve(clientDir, `.${decoded}`);
55
+ const rel = relative(clientDir, candidate);
56
+ if (rel === '..' || rel.startsWith(`..${sep}`))
57
+ return null;
58
+ return candidate;
59
+ }
60
+ async function sendClientFile(res, path, headOnly, immutable = false) {
61
+ try {
62
+ const body = await readFile(path);
63
+ res.setHeader('Cache-Control', immutable ? 'public, max-age=31536000, immutable' : 'no-cache');
64
+ send(res, 200, contentType(path), body, headOnly);
65
+ }
66
+ catch (error) {
67
+ if (error.code !== 'ENOENT')
68
+ throw error;
69
+ send(res, 404, 'text/plain; charset=utf-8', 'Not found', headOnly);
70
+ }
71
+ }
72
+ /** Start the read-only loopback server used by `lat ui`. */
73
+ export async function startViewServer(ctx, options = {}) {
74
+ const host = options.host ?? DEFAULT_HOST;
75
+ const clientDir = options.clientDir ?? defaultClientDir;
76
+ const store = await createViewStore(ctx.latDir, ctx.projectRoot, {
77
+ git: options.git,
78
+ watch: options.watch,
79
+ });
80
+ const search = options.search ??
81
+ createViewSearch(ctx.latDir, undefined, () => store.markdownGeneration);
82
+ const eventClients = new Set();
83
+ const broadcastChange = (change) => {
84
+ const message = `event: change\ndata: ${JSON.stringify(change)}\n\n`;
85
+ for (const client of eventClients)
86
+ client.write(message);
87
+ };
88
+ const unsubscribeStore = store.subscribe(broadcastChange);
89
+ const server = createServer((req, res) => {
90
+ void (async () => {
91
+ setSecurityHeaders(res);
92
+ const method = req.method ?? 'GET';
93
+ const headOnly = method === 'HEAD';
94
+ if (method !== 'GET' && !headOnly) {
95
+ res.setHeader('Allow', 'GET, HEAD');
96
+ send(res, 405, 'text/plain; charset=utf-8', 'Method not allowed');
97
+ return;
98
+ }
99
+ const url = new URL(req.url ?? '/', `http://${host}`);
100
+ if (url.pathname === '/') {
101
+ const entry = store.getIndex().entry;
102
+ if (!entry) {
103
+ send(res, 404, 'text/plain; charset=utf-8', 'No Markdown files found');
104
+ return;
105
+ }
106
+ res.statusCode = 302;
107
+ res.setHeader('Location', documentUrl(entry));
108
+ res.end();
109
+ return;
110
+ }
111
+ if (url.pathname === '/api/index') {
112
+ sendJson(res, 200, store.getIndex(), headOnly);
113
+ return;
114
+ }
115
+ if (url.pathname === '/api/events') {
116
+ res.statusCode = 200;
117
+ res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
118
+ res.setHeader('Cache-Control', 'no-cache');
119
+ res.setHeader('Connection', 'keep-alive');
120
+ res.flushHeaders();
121
+ if (headOnly) {
122
+ res.end();
123
+ return;
124
+ }
125
+ eventClients.add(res);
126
+ res.write(`event: ready\ndata: ${JSON.stringify({ generation: store.snapshot.generation, markdownGeneration: store.markdownGeneration })}\n\n`);
127
+ req.once('close', () => eventClients.delete(res));
128
+ return;
129
+ }
130
+ if (url.pathname === '/api/document') {
131
+ const path = url.searchParams.get('path') ?? '';
132
+ try {
133
+ sendJson(res, 200, await store.getDocument(path), headOnly);
134
+ }
135
+ catch (error) {
136
+ if (!(error instanceof ViewDocumentNotFoundError))
137
+ throw error;
138
+ sendJson(res, 404, { error: error.message }, headOnly);
139
+ }
140
+ return;
141
+ }
142
+ if (url.pathname === '/api/search') {
143
+ const query = (url.searchParams.get('query') ?? '').trim();
144
+ if (query.length > 500) {
145
+ sendJson(res, 400, { error: 'Search query is too long' }, headOnly);
146
+ return;
147
+ }
148
+ sendJson(res, 200, await search(query), headOnly);
149
+ return;
150
+ }
151
+ if (url.pathname === '/api/source') {
152
+ const path = url.searchParams.get('path') ?? '';
153
+ const symbol = url.searchParams.get('symbol') ?? '';
154
+ const from = url.searchParams.get('from') ?? '';
155
+ const parsedLine = Number(url.searchParams.get('line'));
156
+ const parsedFocusLine = Number(url.searchParams.get('at'));
157
+ const focusLine = Number.isInteger(parsedFocusLine) && parsedFocusLine > 0
158
+ ? parsedFocusLine
159
+ : 0;
160
+ const origin = from && Number.isInteger(parsedLine) && parsedLine > 0
161
+ ? { sectionId: from, line: parsedLine }
162
+ : undefined;
163
+ try {
164
+ sendJson(res, 200, await store.getSource(path, symbol, origin, focusLine), headOnly);
165
+ }
166
+ catch (error) {
167
+ if (!(error instanceof ViewSourceNotFoundError))
168
+ throw error;
169
+ sendJson(res, 404, { error: error.message }, headOnly);
170
+ }
171
+ return;
172
+ }
173
+ if (url.pathname.startsWith('/assets/')) {
174
+ const path = clientPath(clientDir, url.pathname);
175
+ if (!path) {
176
+ send(res, 404, 'text/plain; charset=utf-8', 'Not found', headOnly);
177
+ return;
178
+ }
179
+ await sendClientFile(res, path, headOnly, true);
180
+ return;
181
+ }
182
+ if (url.pathname === '/search' ||
183
+ url.pathname.startsWith('/docs/') ||
184
+ url.pathname.startsWith('/code/')) {
185
+ await sendClientFile(res, join(clientDir, 'index.html'), headOnly);
186
+ return;
187
+ }
188
+ send(res, 404, 'text/plain; charset=utf-8', 'Not found', headOnly);
189
+ })().catch((error) => {
190
+ if (res.headersSent) {
191
+ res.destroy(error);
192
+ return;
193
+ }
194
+ sendJson(res, 500, { error: error.message }, false);
195
+ });
196
+ });
197
+ 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
+ });
205
+ }
206
+ catch (error) {
207
+ unsubscribeStore();
208
+ await store.close();
209
+ throw error;
210
+ }
211
+ const address = server.address();
212
+ if (!address || typeof address === 'string') {
213
+ await new Promise((resolveClose) => server.close(() => resolveClose()));
214
+ unsubscribeStore();
215
+ await store.close();
216
+ throw new Error('Could not determine lat ui server address');
217
+ }
218
+ return {
219
+ server,
220
+ store,
221
+ url: `http://${host}:${address.port}/`,
222
+ close: async () => {
223
+ unsubscribeStore();
224
+ for (const client of eventClients)
225
+ client.end();
226
+ eventClients.clear();
227
+ await store.close();
228
+ await new Promise((resolveClose, reject) => {
229
+ server.close((error) => (error ? reject(error) : resolveClose()));
230
+ });
231
+ },
232
+ };
233
+ }
@@ -0,0 +1,57 @@
1
+ import { type Section } from '../lattice.js';
2
+ import { type ViewGitRepository, type ViewGitSnapshot } from './git.js';
3
+ import type { ViewDocument, ViewDocumentError, ViewIndex, ViewProjectChange, ViewSourceDocument } from './protocol.js';
4
+ import { type ViewCodeReferenceFile, type ViewParsedMarkdownFile, type ViewReferenceIndex } from './references.js';
5
+ export type ViewProjectSnapshot = {
6
+ generation: number;
7
+ markdownGeneration: number;
8
+ files: ReadonlyMap<string, ViewParsedMarkdownFile>;
9
+ allSections: Section[];
10
+ references: ViewReferenceIndex;
11
+ diagnostics: ReadonlyMap<string, readonly ViewDocumentError[]>;
12
+ git: ViewGitSnapshot;
13
+ index: ViewIndex;
14
+ };
15
+ export type ViewStoreOptions = {
16
+ debounceMs?: number;
17
+ git?: boolean;
18
+ watch?: boolean;
19
+ };
20
+ type ViewStoreListener = (change: ViewProjectChange) => void;
21
+ /** Own the immutable, incrementally refreshed project snapshot for `lat ui`. */
22
+ export declare class ViewStore {
23
+ readonly latDir: string;
24
+ readonly projectRoot: string;
25
+ private readonly realLatDir;
26
+ private readonly gitRepository;
27
+ private readonly options;
28
+ private snapshotValue;
29
+ private codeFiles;
30
+ private codeScope;
31
+ private ignoredCodePaths;
32
+ private listeners;
33
+ private watcher;
34
+ private pendingPaths;
35
+ private debounceTimer;
36
+ private refreshTail;
37
+ private closed;
38
+ constructor(latDir: string, projectRoot: string, realLatDir: string, snapshot: ViewProjectSnapshot, codeFiles: Map<string, ViewCodeReferenceFile>, codeScope: Set<string>, gitRepository: ViewGitRepository | null, options: ViewStoreOptions);
39
+ get snapshot(): ViewProjectSnapshot;
40
+ get markdownGeneration(): number;
41
+ startWatching(): void;
42
+ subscribe(listener: ViewStoreListener): () => void;
43
+ getIndex(): ViewIndex;
44
+ getDocument(requestedPath: string): Promise<ViewDocument>;
45
+ getSource(requestedPath: string, requestedSymbol?: string, origin?: {
46
+ sectionId: string;
47
+ line: number;
48
+ }, requestedLine?: number): Promise<ViewSourceDocument>;
49
+ refresh(paths: string[]): Promise<void>;
50
+ close(): Promise<void>;
51
+ private scheduleRefresh;
52
+ private readMarkdownFile;
53
+ private applyRefresh;
54
+ }
55
+ /** Build the initial project snapshot and optionally begin incremental updates. */
56
+ export declare function createViewStore(latDir: string, projectRoot: string, options?: ViewStoreOptions): Promise<ViewStore>;
57
+ export {};
@@ -0,0 +1,404 @@
1
+ import { watch as watchFiles } from 'node:fs';
2
+ import { readFile, realpath } from 'node:fs/promises';
3
+ import { basename, extname, isAbsolute, relative, resolve, sep, } from 'node:path';
4
+ import { LAT_REF_RE, scanCodeRefs } from '../code-refs.js';
5
+ import { listLatticeFiles, parseFrontmatter, } from '../lattice.js';
6
+ import { SOURCE_EXTENSIONS } from '../source-parser.js';
7
+ import { toPosix } from '../walk.js';
8
+ import { renderMarkdown } from './markdown.js';
9
+ import { buildViewDiagnostics } from './diagnostics.js';
10
+ import { buildGitDiffTree } from './git-diff.js';
11
+ import { emptyViewGitSnapshot, findViewGitRepository, readViewGitSnapshot, sameViewGitSnapshot, } from './git.js';
12
+ import { createMarkdownWikiLinkResolver, getViewSource, ViewDocumentNotFoundError, } from './repository.js';
13
+ import { buildViewReferenceIndex, parseViewMarkdownFile, renderSectionBackReferences, } from './references.js';
14
+ const DEFAULT_DEBOUNCE_MS = 75;
15
+ function isInside(root, candidate) {
16
+ const rel = relative(root, candidate);
17
+ return (rel === '' ||
18
+ (!isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)));
19
+ }
20
+ function projectPath(projectRoot, path) {
21
+ const normalized = isAbsolute(path) ? relative(projectRoot, path) : path;
22
+ return toPosix(normalized).replace(/^\.\//, '');
23
+ }
24
+ function sourcePath(path) {
25
+ return SOURCE_EXTENSIONS.has(extname(path).toLowerCase());
26
+ }
27
+ function obviouslyIgnoredCodePath(path, latPath) {
28
+ const parts = path.split('/');
29
+ return (path === latPath ||
30
+ path.startsWith(`${latPath}/`) ||
31
+ parts.includes('.git') ||
32
+ parts.includes('.claude') ||
33
+ parts.includes('node_modules'));
34
+ }
35
+ function codeRefsFromContent(path, content) {
36
+ const refs = [];
37
+ const lines = content.split('\n');
38
+ for (let index = 0; index < lines.length; index++) {
39
+ LAT_REF_RE.lastIndex = 0;
40
+ let match;
41
+ while ((match = LAT_REF_RE.exec(lines[index])) !== null) {
42
+ refs.push({ target: match[1], file: path, line: index + 1 });
43
+ }
44
+ }
45
+ return refs;
46
+ }
47
+ async function loadCodeReferenceFiles(projectRoot, refs) {
48
+ const refsByFile = new Map();
49
+ for (const ref of refs) {
50
+ const path = toPosix(ref.file);
51
+ const fileRefs = refsByFile.get(path) ?? [];
52
+ fileRefs.push({ ...ref, file: path });
53
+ refsByFile.set(path, fileRefs);
54
+ }
55
+ const files = new Map();
56
+ await Promise.all([...refsByFile].map(async ([path, fileRefs]) => {
57
+ try {
58
+ const content = await readFile(resolve(projectRoot, path), 'utf8');
59
+ files.set(path, { path, lines: content.split('\n'), refs: fileRefs });
60
+ }
61
+ catch {
62
+ // A file may disappear between the project scan and this read.
63
+ }
64
+ }));
65
+ return files;
66
+ }
67
+ async function scanCodeState(projectRoot) {
68
+ const scan = await scanCodeRefs(projectRoot);
69
+ const scope = new Set(scan.files.map((path) => projectPath(projectRoot, path)));
70
+ return {
71
+ files: await loadCodeReferenceFiles(projectRoot, scan.refs),
72
+ scope,
73
+ };
74
+ }
75
+ function viewIndex(latDir, paths, diagnostics, git) {
76
+ const files = [...paths].sort();
77
+ const directoryName = basename(latDir);
78
+ const indexName = directoryName.endsWith('.md')
79
+ ? directoryName
80
+ : `${directoryName}.md`;
81
+ return {
82
+ files,
83
+ entry: files.includes(indexName) ? indexName : (files[0] ?? ''),
84
+ errorCounts: Object.fromEntries([...diagnostics]
85
+ .filter(([, errors]) => errors.length > 0)
86
+ .map(([path, errors]) => [path, errors.length])),
87
+ git: git.available
88
+ ? {
89
+ files: Object.fromEntries([...git.files].map(([path, file]) => [path, file.status])),
90
+ }
91
+ : null,
92
+ };
93
+ }
94
+ async function buildSnapshot(latDir, projectRoot, markdownFiles, codeFiles, git, generation, markdownGeneration) {
95
+ const files = new Map([...markdownFiles].sort(([left], [right]) => left.localeCompare(right)));
96
+ const allSections = [...files.values()].flatMap((file) => file.sections);
97
+ const diagnostics = await buildViewDiagnostics(files.values(), codeFiles.values(), allSections, projectRoot);
98
+ return {
99
+ generation,
100
+ markdownGeneration,
101
+ files,
102
+ allSections,
103
+ references: buildViewReferenceIndex(files.values(), codeFiles.values(), allSections),
104
+ diagnostics,
105
+ git,
106
+ index: viewIndex(latDir, [...files.keys()], diagnostics, git),
107
+ };
108
+ }
109
+ /** Own the immutable, incrementally refreshed project snapshot for `lat ui`. */
110
+ export class ViewStore {
111
+ latDir;
112
+ projectRoot;
113
+ realLatDir;
114
+ gitRepository;
115
+ options;
116
+ snapshotValue;
117
+ codeFiles;
118
+ codeScope;
119
+ ignoredCodePaths = new Set();
120
+ listeners = new Set();
121
+ watcher = null;
122
+ pendingPaths = new Set();
123
+ debounceTimer = null;
124
+ refreshTail = Promise.resolve();
125
+ closed = false;
126
+ constructor(latDir, projectRoot, realLatDir, snapshot, codeFiles, codeScope, gitRepository, options) {
127
+ this.latDir = latDir;
128
+ this.projectRoot = projectRoot;
129
+ this.realLatDir = realLatDir;
130
+ this.gitRepository = gitRepository;
131
+ this.options = options;
132
+ this.snapshotValue = snapshot;
133
+ this.codeFiles = codeFiles;
134
+ this.codeScope = codeScope;
135
+ }
136
+ get snapshot() {
137
+ return this.snapshotValue;
138
+ }
139
+ get markdownGeneration() {
140
+ return this.snapshotValue.markdownGeneration;
141
+ }
142
+ startWatching() {
143
+ if (this.options.watch === false || this.watcher)
144
+ return;
145
+ try {
146
+ this.watcher = watchFiles(this.projectRoot, { recursive: true }, (_event, filename) => this.scheduleRefresh(filename?.toString() ?? ''));
147
+ this.watcher.on('error', (error) => {
148
+ process.stderr.write(`lat ui watcher: ${error.message}\n`);
149
+ });
150
+ }
151
+ catch (error) {
152
+ process.stderr.write(`lat ui watcher: ${error.message}\n`);
153
+ }
154
+ }
155
+ subscribe(listener) {
156
+ this.listeners.add(listener);
157
+ return () => this.listeners.delete(listener);
158
+ }
159
+ getIndex() {
160
+ return this.snapshotValue.index;
161
+ }
162
+ async getDocument(requestedPath) {
163
+ if (!requestedPath ||
164
+ requestedPath.includes('\\') ||
165
+ isAbsolute(requestedPath) ||
166
+ !requestedPath.toLowerCase().endsWith('.md')) {
167
+ throw new ViewDocumentNotFoundError('Markdown document not found');
168
+ }
169
+ const snapshot = this.snapshotValue;
170
+ const file = snapshot.files.get(requestedPath);
171
+ if (!file) {
172
+ throw new ViewDocumentNotFoundError('Markdown document not found');
173
+ }
174
+ const resolver = await createMarkdownWikiLinkResolver(this.latDir, requestedPath, snapshot.allSections);
175
+ const rendered = await renderMarkdown(file.content, requestedPath, resolver, { errors: [...(snapshot.diagnostics.get(requestedPath) ?? [])] }, file.tree);
176
+ const gitFile = snapshot.git.files.get(requestedPath);
177
+ const gitRendered = gitFile
178
+ ? await renderMarkdown(file.content, requestedPath, resolver, { errors: [...(snapshot.diagnostics.get(requestedPath) ?? [])] }, buildGitDiffTree(gitFile.baseContent, file.content, file.tree))
179
+ : null;
180
+ const errors = [...(snapshot.diagnostics.get(requestedPath) ?? [])];
181
+ return {
182
+ path: requestedPath,
183
+ ...rendered,
184
+ gitHtml: gitRendered?.html ?? null,
185
+ errors,
186
+ backReferences: await renderSectionBackReferences(snapshot.references, file.sections, this.latDir, this.projectRoot, (path) => createMarkdownWikiLinkResolver(this.latDir, path, snapshot.allSections)),
187
+ frontmatter: {
188
+ requireCodeMention: parseFrontmatter(file.content).requireCodeMention === true,
189
+ },
190
+ };
191
+ }
192
+ getSource(requestedPath, requestedSymbol = '', origin, requestedLine = 0) {
193
+ const snapshot = this.snapshotValue;
194
+ return getViewSource(this.latDir, this.projectRoot, requestedPath, requestedSymbol, origin, requestedLine, snapshot.allSections, snapshot.references);
195
+ }
196
+ refresh(paths) {
197
+ if (this.closed)
198
+ return Promise.resolve();
199
+ const normalized = paths.map((path) => path ? projectPath(this.projectRoot, path) : '');
200
+ const refresh = this.refreshTail.then(() => this.applyRefresh(normalized));
201
+ this.refreshTail = refresh.catch(() => { });
202
+ return refresh;
203
+ }
204
+ async close() {
205
+ this.closed = true;
206
+ if (this.debounceTimer)
207
+ clearTimeout(this.debounceTimer);
208
+ this.debounceTimer = null;
209
+ this.watcher?.close();
210
+ this.watcher = null;
211
+ await this.refreshTail;
212
+ this.listeners.clear();
213
+ }
214
+ scheduleRefresh(path) {
215
+ if (this.closed)
216
+ return;
217
+ this.pendingPaths.add(path ? projectPath(this.projectRoot, path) : '');
218
+ if (this.debounceTimer)
219
+ clearTimeout(this.debounceTimer);
220
+ this.debounceTimer = setTimeout(() => {
221
+ this.debounceTimer = null;
222
+ const paths = [...this.pendingPaths];
223
+ this.pendingPaths.clear();
224
+ void this.refresh(paths).catch((error) => {
225
+ process.stderr.write(`lat ui refresh: ${error.message}\n`);
226
+ });
227
+ }, this.options.debounceMs ?? DEFAULT_DEBOUNCE_MS);
228
+ }
229
+ async readMarkdownFile(absolutePath) {
230
+ let realFile;
231
+ try {
232
+ realFile = await realpath(absolutePath);
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ if (!isInside(this.realLatDir, realFile))
238
+ return null;
239
+ const content = await readFile(realFile, 'utf8');
240
+ return parseViewMarkdownFile(absolutePath, content, this.latDir, this.projectRoot);
241
+ }
242
+ async applyRefresh(paths) {
243
+ const fullRefresh = paths.includes('');
244
+ const latPath = projectPath(this.projectRoot, this.latDir);
245
+ const touchesMarkdown = fullRefresh ||
246
+ paths.some((path) => path === latPath || path.startsWith(`${latPath}/`));
247
+ let markdownFiles = new Map(this.snapshotValue.files);
248
+ let codeFiles = new Map(this.codeFiles);
249
+ let markdownChanged = false;
250
+ let codeChanged = false;
251
+ let git = this.snapshotValue.git;
252
+ let gitChanged = false;
253
+ let linkedResourceChanged = false;
254
+ if (touchesMarkdown) {
255
+ const discovered = new Map((await listLatticeFiles(this.latDir)).map((absolutePath) => [
256
+ toPosix(relative(this.latDir, absolutePath)),
257
+ absolutePath,
258
+ ]));
259
+ const changed = new Set();
260
+ for (const path of markdownFiles.keys()) {
261
+ if (!discovered.has(path))
262
+ changed.add(path);
263
+ }
264
+ for (const path of discovered.keys()) {
265
+ if (!markdownFiles.has(path))
266
+ changed.add(path);
267
+ }
268
+ for (const path of paths) {
269
+ if (path === '') {
270
+ for (const markdownPath of discovered.keys())
271
+ changed.add(markdownPath);
272
+ continue;
273
+ }
274
+ if (path === latPath)
275
+ continue;
276
+ if (!path.startsWith(`${latPath}/`))
277
+ continue;
278
+ const markdownPath = path.slice(latPath.length + 1);
279
+ if (markdownPath.toLowerCase().endsWith('.md')) {
280
+ changed.add(markdownPath);
281
+ }
282
+ }
283
+ for (const path of [...changed].sort()) {
284
+ const absolutePath = discovered.get(path);
285
+ if (!absolutePath) {
286
+ markdownChanged = markdownFiles.delete(path) || markdownChanged;
287
+ continue;
288
+ }
289
+ const next = await this.readMarkdownFile(absolutePath);
290
+ if (!next) {
291
+ markdownChanged = markdownFiles.delete(path) || markdownChanged;
292
+ continue;
293
+ }
294
+ if (markdownFiles.get(path)?.content === next.content)
295
+ continue;
296
+ markdownFiles.set(path, next);
297
+ markdownChanged = true;
298
+ }
299
+ linkedResourceChanged = paths.some((path) => path.startsWith(`${latPath}/`) && !path.toLowerCase().endsWith('.md'));
300
+ }
301
+ if (touchesMarkdown && this.gitRepository) {
302
+ try {
303
+ const nextGit = await readViewGitSnapshot(this.gitRepository, markdownFiles);
304
+ gitChanged = !sameViewGitSnapshot(git, nextGit);
305
+ git = nextGit;
306
+ }
307
+ catch (error) {
308
+ process.stderr.write(`lat ui git: ${error.message}\n`);
309
+ }
310
+ }
311
+ const codePaths = paths.filter((path) => path && sourcePath(path) && !obviouslyIgnoredCodePath(path, latPath));
312
+ const refreshCodeScope = fullRefresh ||
313
+ paths.some((path) => path === '.gitignore' || path.endsWith('/.gitignore')) ||
314
+ codePaths.some((path) => !this.codeScope.has(path) && !this.ignoredCodePaths.has(path));
315
+ if (refreshCodeScope) {
316
+ const nextCode = await scanCodeState(this.projectRoot);
317
+ codeFiles = nextCode.files;
318
+ this.codeScope = nextCode.scope;
319
+ this.ignoredCodePaths.clear();
320
+ for (const path of codePaths) {
321
+ if (!this.codeScope.has(path))
322
+ this.ignoredCodePaths.add(path);
323
+ }
324
+ codeChanged = true;
325
+ }
326
+ else {
327
+ for (const path of codePaths) {
328
+ if (this.ignoredCodePaths.has(path) || !this.codeScope.has(path)) {
329
+ continue;
330
+ }
331
+ try {
332
+ const content = await readFile(resolve(this.projectRoot, path), 'utf8');
333
+ const refs = codeRefsFromContent(path, content);
334
+ if (refs.length > 0) {
335
+ codeFiles.set(path, {
336
+ path,
337
+ lines: content.split('\n'),
338
+ refs,
339
+ });
340
+ }
341
+ else {
342
+ codeFiles.delete(path);
343
+ }
344
+ }
345
+ catch {
346
+ this.codeScope.delete(path);
347
+ codeFiles.delete(path);
348
+ }
349
+ codeChanged = true;
350
+ }
351
+ }
352
+ if (!markdownChanged &&
353
+ !codeChanged &&
354
+ !gitChanged &&
355
+ !linkedResourceChanged)
356
+ return;
357
+ this.codeFiles = codeFiles;
358
+ this.snapshotValue = await buildSnapshot(this.latDir, this.projectRoot, markdownFiles, codeFiles, git, this.snapshotValue.generation + 1, this.snapshotValue.markdownGeneration + (markdownChanged ? 1 : 0));
359
+ const change = {
360
+ generation: this.snapshotValue.generation,
361
+ markdownGeneration: this.snapshotValue.markdownGeneration,
362
+ };
363
+ for (const listener of this.listeners)
364
+ listener(change);
365
+ }
366
+ }
367
+ /** Build the initial project snapshot and optionally begin incremental updates. */
368
+ export async function createViewStore(latDir, projectRoot, options = {}) {
369
+ const [realLatDir, markdownPaths, codeState, gitRepository] = await Promise.all([
370
+ realpath(latDir),
371
+ listLatticeFiles(latDir),
372
+ scanCodeState(projectRoot),
373
+ options.git === false
374
+ ? Promise.resolve(null)
375
+ : findViewGitRepository(projectRoot, latDir),
376
+ ]);
377
+ if (markdownPaths.length === 0) {
378
+ throw new Error(`No Markdown files found in ${latDir}`);
379
+ }
380
+ const markdownFiles = new Map();
381
+ await Promise.all(markdownPaths.map(async (absolutePath) => {
382
+ const realFile = await realpath(absolutePath);
383
+ if (!isInside(realLatDir, realFile))
384
+ return;
385
+ const content = await readFile(realFile, 'utf8');
386
+ const parsed = parseViewMarkdownFile(absolutePath, content, latDir, projectRoot);
387
+ markdownFiles.set(parsed.path, parsed);
388
+ }));
389
+ if (markdownFiles.size === 0) {
390
+ throw new Error(`No readable Markdown files found in ${latDir}`);
391
+ }
392
+ let git = emptyViewGitSnapshot();
393
+ if (gitRepository) {
394
+ try {
395
+ git = await readViewGitSnapshot(gitRepository, markdownFiles);
396
+ }
397
+ catch (error) {
398
+ process.stderr.write(`lat ui git: ${error.message}\n`);
399
+ }
400
+ }
401
+ const store = new ViewStore(latDir, projectRoot, realLatDir, await buildSnapshot(latDir, projectRoot, markdownFiles, codeState.files, git, 0, 0), codeState.files, codeState.scope, gitRepository, options);
402
+ store.startWatching();
403
+ return store;
404
+ }