@plebeiantech/lat.md 0.12.3-fork.3 → 0.12.3-fork.5

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 (62) hide show
  1. package/README.md +2 -0
  2. package/dist/src/cli/check-mode.js +5 -1
  3. package/dist/src/cli/comment-guard.d.ts +14 -0
  4. package/dist/src/cli/comment-guard.js +10 -33
  5. package/dist/src/cli/comment-reminder.d.ts +65 -6
  6. package/dist/src/cli/comment-reminder.js +164 -28
  7. package/dist/src/cli/index.js +29 -4
  8. package/dist/src/cli/search.js +6 -4
  9. package/dist/src/cli/ui-build.d.ts +4 -0
  10. package/dist/src/cli/ui-build.js +13 -0
  11. package/dist/src/cli/ui.js +18 -2
  12. package/dist/src/code-refs.js +25 -4
  13. package/dist/src/fork/upstream-drift-cli.d.ts +1 -0
  14. package/dist/src/fork/upstream-drift-cli.js +2 -0
  15. package/dist/src/fork/upstream-drift.d.ts +26 -0
  16. package/dist/src/fork/upstream-drift.js +185 -0
  17. package/dist/src/fork/upstream-guard-cli.d.ts +1 -0
  18. package/dist/src/fork/upstream-guard-cli.js +2 -0
  19. package/dist/src/fork/upstream-guard.d.ts +72 -0
  20. package/dist/src/fork/upstream-guard.js +445 -0
  21. package/dist/src/graph-export.d.ts +2 -0
  22. package/dist/src/graph-export.js +31 -12
  23. package/dist/src/knowledge/claude-memory.d.ts +1 -0
  24. package/dist/src/knowledge/claude-memory.js +33 -4
  25. package/dist/src/lattice.d.ts +1 -0
  26. package/dist/src/search/search.d.ts +1 -0
  27. package/dist/src/search/search.js +15 -9
  28. package/dist/src/untrusted.d.ts +0 -6
  29. package/dist/src/untrusted.js +7 -11
  30. package/dist/src/view/client/assets/index-VZwPPiFE.js +334 -0
  31. package/dist/src/view/client/assets/index-W8ktvNj9.css +1 -0
  32. package/dist/src/view/client/assets/logo-CNF8zs47.svg +182 -0
  33. package/dist/src/view/client/index.html +2 -2
  34. package/dist/src/view/git-diff.js +17 -0
  35. package/dist/src/view/graph.d.ts +6 -0
  36. package/dist/src/view/graph.js +176 -0
  37. package/dist/src/view/markdown.d.ts +5 -1
  38. package/dist/src/view/markdown.js +112 -25
  39. package/dist/src/view/protocol.d.ts +43 -0
  40. package/dist/src/view/protocol.js +1 -1
  41. package/dist/src/view/references.d.ts +2 -0
  42. package/dist/src/view/references.js +30 -8
  43. package/dist/src/view/repository.d.ts +5 -2
  44. package/dist/src/view/repository.js +13 -12
  45. package/dist/src/view/search.js +1 -0
  46. package/dist/src/view/server.d.ts +3 -0
  47. package/dist/src/view/server.js +42 -9
  48. package/dist/src/view/source-target.d.ts +8 -0
  49. package/dist/src/view/source-target.js +22 -0
  50. package/dist/src/view/static-build.d.ts +16 -0
  51. package/dist/src/view/static-build.js +408 -0
  52. package/dist/src/view/static-protocol.d.ts +23 -0
  53. package/dist/src/view/static-protocol.js +10 -0
  54. package/dist/src/view/store.d.ts +9 -1
  55. package/dist/src/view/store.js +82 -15
  56. package/dist/src/view/table-of-contents.d.ts +9 -0
  57. package/dist/src/view/table-of-contents.js +103 -0
  58. package/package.json +3 -1
  59. package/templates/AGENTS.md +2 -0
  60. package/templates/skill/SKILL.md +2 -0
  61. package/dist/src/view/client/assets/index-B0n0u03y.css +0 -1
  62. package/dist/src/view/client/assets/index-xm2_px4q.js +0 -23
@@ -8,10 +8,14 @@ import { toPosix } from '../walk.js';
8
8
  import { renderMarkdown } from './markdown.js';
9
9
  import { buildViewDiagnostics } from './diagnostics.js';
10
10
  import { buildGitDiffTree } from './git-diff.js';
11
+ import { buildViewGraph } from './graph.js';
12
+ import { buildViewTableOfContents } from './table-of-contents.js';
11
13
  import { emptyViewGitSnapshot, findViewGitRepository, readViewGitSnapshot, sameViewGitSnapshot, } from './git.js';
14
+ import { DEFAULT_VIEW_LOGO_TEXT } from './protocol.js';
12
15
  import { createMarkdownWikiLinkResolver, getViewSource, ViewDocumentNotFoundError, } from './repository.js';
13
16
  import { buildViewReferenceIndex, parseViewMarkdownFile, renderSectionBackReferences, } from './references.js';
14
17
  const DEFAULT_DEBOUNCE_MS = 75;
18
+ const DEFAULT_GIT_POLL_MS = 2_000;
15
19
  function isInside(root, candidate) {
16
20
  const rel = relative(root, candidate);
17
21
  return (rel === '' ||
@@ -21,6 +25,10 @@ function projectPath(projectRoot, path) {
21
25
  const normalized = isAbsolute(path) ? relative(projectRoot, path) : path;
22
26
  return toPosix(normalized).replace(/^\.\//, '');
23
27
  }
28
+ function excludedCodePath(projectRoot, path, excludedPaths) {
29
+ const absolutePath = resolve(projectRoot, path);
30
+ return excludedPaths.some((root) => isInside(resolve(root), absolutePath));
31
+ }
24
32
  function sourcePath(path) {
25
33
  return SOURCE_EXTENSIONS.has(extname(path).toLowerCase());
26
34
  }
@@ -64,11 +72,14 @@ async function loadCodeReferenceFiles(projectRoot, refs) {
64
72
  }));
65
73
  return files;
66
74
  }
67
- async function scanCodeState(projectRoot) {
75
+ async function scanCodeState(projectRoot, excludedPaths = []) {
68
76
  const scan = await scanCodeRefs(projectRoot);
69
- const scope = new Set(scan.files.map((path) => projectPath(projectRoot, path)));
77
+ const allowed = (path) => !excludedCodePath(projectRoot, path, excludedPaths);
78
+ const files = scan.files.filter(allowed);
79
+ const refs = scan.refs.filter((ref) => allowed(ref.file));
80
+ const scope = new Set(files.map((path) => projectPath(projectRoot, path)));
70
81
  return {
71
- files: await loadCodeReferenceFiles(projectRoot, scan.refs),
82
+ files: await loadCodeReferenceFiles(projectRoot, refs),
72
83
  scope,
73
84
  };
74
85
  }
@@ -89,18 +100,21 @@ function viewIndex(latDir, paths, diagnostics, git) {
89
100
  files: Object.fromEntries([...git.files].map(([path, file]) => [path, file.status])),
90
101
  }
91
102
  : null,
103
+ logoText: DEFAULT_VIEW_LOGO_TEXT,
92
104
  };
93
105
  }
94
106
  async function buildSnapshot(latDir, projectRoot, markdownFiles, codeFiles, git, generation, markdownGeneration) {
95
107
  const files = new Map([...markdownFiles].sort(([left], [right]) => left.localeCompare(right)));
96
108
  const allSections = [...files.values()].flatMap((file) => file.sections);
97
109
  const diagnostics = await buildViewDiagnostics(files.values(), codeFiles.values(), allSections, projectRoot);
110
+ const references = buildViewReferenceIndex(files.values(), codeFiles.values(), allSections);
98
111
  return {
99
112
  generation,
100
113
  markdownGeneration,
101
114
  files,
102
115
  allSections,
103
- references: buildViewReferenceIndex(files.values(), codeFiles.values(), allSections),
116
+ references,
117
+ graph: buildViewGraph(files.values(), codeFiles.values(), allSections, diagnostics, git, generation),
104
118
  diagnostics,
105
119
  git,
106
120
  index: viewIndex(latDir, [...files.keys()], diagnostics, git),
@@ -121,6 +135,8 @@ export class ViewStore {
121
135
  watcher = null;
122
136
  pendingPaths = new Set();
123
137
  debounceTimer = null;
138
+ gitPollTimer = null;
139
+ gitPollQueued = false;
124
140
  refreshTail = Promise.resolve();
125
141
  closed = false;
126
142
  constructor(latDir, projectRoot, realLatDir, snapshot, codeFiles, codeScope, gitRepository, options) {
@@ -140,6 +156,7 @@ export class ViewStore {
140
156
  return this.snapshotValue.markdownGeneration;
141
157
  }
142
158
  startWatching() {
159
+ this.startGitPolling();
143
160
  if (this.options.watch === false || this.watcher)
144
161
  return;
145
162
  try {
@@ -159,6 +176,9 @@ export class ViewStore {
159
176
  getIndex() {
160
177
  return this.snapshotValue.index;
161
178
  }
179
+ getGraph() {
180
+ return this.snapshotValue.graph;
181
+ }
162
182
  async getDocument(requestedPath) {
163
183
  if (!requestedPath ||
164
184
  requestedPath.includes('\\') ||
@@ -171,19 +191,39 @@ export class ViewStore {
171
191
  if (!file) {
172
192
  throw new ViewDocumentNotFoundError('Markdown document not found');
173
193
  }
174
- const resolver = await createMarkdownWikiLinkResolver(this.latDir, requestedPath, snapshot.allSections);
194
+ const resolver = await createMarkdownWikiLinkResolver(this.latDir, requestedPath, snapshot.allSections, snapshot.references);
175
195
  const rendered = await renderMarkdown(file.content, requestedPath, resolver, { errors: [...(snapshot.diagnostics.get(requestedPath) ?? [])] }, file.tree);
196
+ const errors = [...(snapshot.diagnostics.get(requestedPath) ?? [])];
176
197
  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))
198
+ const gitTree = gitFile
199
+ ? buildGitDiffTree(gitFile.baseContent, file.content, file.tree)
200
+ : null;
201
+ const gitRendered = gitTree
202
+ ? await renderMarkdown(file.content, requestedPath, resolver, { errors }, gitTree)
179
203
  : null;
180
- const errors = [...(snapshot.diagnostics.get(requestedPath) ?? [])];
181
204
  return {
182
205
  path: requestedPath,
183
206
  ...rendered,
184
207
  gitHtml: gitRendered?.html ?? null,
208
+ tableOfContents: buildViewTableOfContents(file.sections, file.tree, {
209
+ errors,
210
+ gitTree,
211
+ }),
212
+ graphNodeIds: Object.fromEntries(snapshot.graph.nodes
213
+ .filter((node) => node.documentPath === requestedPath && node.kind === 'document')
214
+ .map((node) => {
215
+ const hash = new URL(node.url, 'http://lat.local').hash.slice(1);
216
+ let headingId = hash;
217
+ try {
218
+ headingId = decodeURIComponent(hash);
219
+ }
220
+ catch {
221
+ // Keep malformed fragments as-is; they cannot collide with valid ids.
222
+ }
223
+ return [headingId, node.id];
224
+ })),
185
225
  errors,
186
- backReferences: await renderSectionBackReferences(snapshot.references, file.sections, this.latDir, this.projectRoot, (path) => createMarkdownWikiLinkResolver(this.latDir, path, snapshot.allSections)),
226
+ backReferences: await renderSectionBackReferences(snapshot.references, file.sections, this.latDir, this.projectRoot, (path) => createMarkdownWikiLinkResolver(this.latDir, path, snapshot.allSections, snapshot.references)),
187
227
  frontmatter: {
188
228
  requireCodeMention: parseFrontmatter(file.content).requireCodeMention === true,
189
229
  },
@@ -197,7 +237,10 @@ export class ViewStore {
197
237
  if (this.closed)
198
238
  return Promise.resolve();
199
239
  const normalized = paths.map((path) => path ? projectPath(this.projectRoot, path) : '');
200
- const refresh = this.refreshTail.then(() => this.applyRefresh(normalized));
240
+ return this.queueRefresh(normalized, false);
241
+ }
242
+ queueRefresh(paths, pollGit) {
243
+ const refresh = this.refreshTail.then(() => this.applyRefresh(paths, pollGit));
201
244
  this.refreshTail = refresh.catch(() => { });
202
245
  return refresh;
203
246
  }
@@ -206,6 +249,9 @@ export class ViewStore {
206
249
  if (this.debounceTimer)
207
250
  clearTimeout(this.debounceTimer);
208
251
  this.debounceTimer = null;
252
+ if (this.gitPollTimer)
253
+ clearInterval(this.gitPollTimer);
254
+ this.gitPollTimer = null;
209
255
  this.watcher?.close();
210
256
  this.watcher = null;
211
257
  await this.refreshTail;
@@ -226,6 +272,24 @@ export class ViewStore {
226
272
  });
227
273
  }, this.options.debounceMs ?? DEFAULT_DEBOUNCE_MS);
228
274
  }
275
+ startGitPolling() {
276
+ const interval = this.options.gitPollMs ?? DEFAULT_GIT_POLL_MS;
277
+ if (!this.gitRepository || interval <= 0 || this.gitPollTimer)
278
+ return;
279
+ this.gitPollTimer = setInterval(() => {
280
+ if (this.closed || this.gitPollQueued)
281
+ return;
282
+ this.gitPollQueued = true;
283
+ void this.queueRefresh([], true)
284
+ .catch((error) => {
285
+ process.stderr.write(`lat ui git: ${error.message}\n`);
286
+ })
287
+ .finally(() => {
288
+ this.gitPollQueued = false;
289
+ });
290
+ }, interval);
291
+ this.gitPollTimer.unref();
292
+ }
229
293
  async readMarkdownFile(absolutePath) {
230
294
  let realFile;
231
295
  try {
@@ -239,7 +303,7 @@ export class ViewStore {
239
303
  const content = await readFile(realFile, 'utf8');
240
304
  return parseViewMarkdownFile(absolutePath, content, this.latDir, this.projectRoot);
241
305
  }
242
- async applyRefresh(paths) {
306
+ async applyRefresh(paths, pollGit) {
243
307
  const fullRefresh = paths.includes('');
244
308
  const latPath = projectPath(this.projectRoot, this.latDir);
245
309
  const touchesMarkdown = fullRefresh ||
@@ -298,7 +362,7 @@ export class ViewStore {
298
362
  }
299
363
  linkedResourceChanged = paths.some((path) => path.startsWith(`${latPath}/`) && !path.toLowerCase().endsWith('.md'));
300
364
  }
301
- if (touchesMarkdown && this.gitRepository) {
365
+ if ((touchesMarkdown || pollGit) && this.gitRepository) {
302
366
  try {
303
367
  const nextGit = await readViewGitSnapshot(this.gitRepository, markdownFiles);
304
368
  gitChanged = !sameViewGitSnapshot(git, nextGit);
@@ -308,12 +372,15 @@ export class ViewStore {
308
372
  process.stderr.write(`lat ui git: ${error.message}\n`);
309
373
  }
310
374
  }
311
- const codePaths = paths.filter((path) => path && sourcePath(path) && !obviouslyIgnoredCodePath(path, latPath));
375
+ const codePaths = paths.filter((path) => path &&
376
+ sourcePath(path) &&
377
+ !obviouslyIgnoredCodePath(path, latPath) &&
378
+ !excludedCodePath(this.projectRoot, path, this.options.codeExcludePaths ?? []));
312
379
  const refreshCodeScope = fullRefresh ||
313
380
  paths.some((path) => path === '.gitignore' || path.endsWith('/.gitignore')) ||
314
381
  codePaths.some((path) => !this.codeScope.has(path) && !this.ignoredCodePaths.has(path));
315
382
  if (refreshCodeScope) {
316
- const nextCode = await scanCodeState(this.projectRoot);
383
+ const nextCode = await scanCodeState(this.projectRoot, this.options.codeExcludePaths);
317
384
  codeFiles = nextCode.files;
318
385
  this.codeScope = nextCode.scope;
319
386
  this.ignoredCodePaths.clear();
@@ -369,7 +436,7 @@ export async function createViewStore(latDir, projectRoot, options = {}) {
369
436
  const [realLatDir, markdownPaths, codeState, gitRepository] = await Promise.all([
370
437
  realpath(latDir),
371
438
  listLatticeFiles(latDir),
372
- scanCodeState(projectRoot),
439
+ scanCodeState(projectRoot, options.codeExcludePaths),
373
440
  options.git === false
374
441
  ? Promise.resolve(null)
375
442
  : findViewGitRepository(projectRoot, latDir),
@@ -0,0 +1,9 @@
1
+ import type { Root } from 'mdast';
2
+ import { type Section } from '../lattice.js';
3
+ import type { ViewDocumentError, ViewDocumentTocItem } from './protocol.js';
4
+ export type ViewTableOfContentsOptions = {
5
+ errors?: readonly ViewDocumentError[];
6
+ gitTree?: Root | null;
7
+ };
8
+ /** Project rendered headings into the document's local navigation. */
9
+ export declare function buildViewTableOfContents(sections: Section[], tree: Root, options?: ViewTableOfContentsOptions): ViewDocumentTocItem[];
@@ -0,0 +1,103 @@
1
+ import { visit } from 'unist-util-visit';
2
+ import { flattenSections } from '../lattice.js';
3
+ function headingText(node) {
4
+ if (!node || typeof node !== 'object')
5
+ return '';
6
+ const value = node;
7
+ if (value.type === 'text' || value.type === 'inlineCode') {
8
+ return value.value ?? '';
9
+ }
10
+ if (value.type === 'image' || value.type === 'imageReference') {
11
+ return value.alt ?? '';
12
+ }
13
+ if (value.type === 'wikiLink') {
14
+ return value.data?.alias ?? value.value ?? '';
15
+ }
16
+ return value.children?.map(headingText).join('') ?? '';
17
+ }
18
+ function sectionAtLine(sections, line) {
19
+ let owner = null;
20
+ for (const section of sections) {
21
+ if (line < section.startLine || line > section.endLine)
22
+ continue;
23
+ if (!owner || section.depth >= owner.depth)
24
+ owner = section;
25
+ }
26
+ return owner;
27
+ }
28
+ function classNames(node) {
29
+ const value = node.data?.hProperties?.className;
30
+ if (Array.isArray(value))
31
+ return value.map(String);
32
+ return value ? [String(value)] : [];
33
+ }
34
+ function hasGitChanges(node) {
35
+ if (classNames(node).some((name) => name.startsWith('git-'))) {
36
+ return true;
37
+ }
38
+ if (!('children' in node) || !Array.isArray(node.children))
39
+ return false;
40
+ return node.children.some((child) => hasGitChanges(child));
41
+ }
42
+ function gitChangedSections(sections, tree) {
43
+ const changed = new Set();
44
+ if (!tree)
45
+ return changed;
46
+ const headingStack = [];
47
+ const rootSection = sections[0] ?? null;
48
+ for (const node of tree.children) {
49
+ if (node.type === 'heading') {
50
+ while (headingStack.length > 0 &&
51
+ headingStack[headingStack.length - 1].depth >= node.depth) {
52
+ headingStack.pop();
53
+ }
54
+ const line = node.position?.start.line;
55
+ const section = line
56
+ ? (sections.find((candidate) => candidate.startLine === line && candidate.depth === node.depth) ?? null)
57
+ : null;
58
+ if (section)
59
+ headingStack.push(section);
60
+ if (hasGitChanges(node)) {
61
+ const owner = section ?? headingStack[headingStack.length - 1] ?? rootSection;
62
+ if (owner)
63
+ changed.add(owner.id);
64
+ }
65
+ continue;
66
+ }
67
+ if (!hasGitChanges(node))
68
+ continue;
69
+ const line = node.position?.start.line;
70
+ const owner = (line ? sectionAtLine(sections, line) : null) ??
71
+ headingStack[headingStack.length - 1] ??
72
+ rootSection;
73
+ if (owner)
74
+ changed.add(owner.id);
75
+ }
76
+ return changed;
77
+ }
78
+ /** Project rendered headings into the document's local navigation. */
79
+ export function buildViewTableOfContents(sections, tree, options = {}) {
80
+ const flatSections = flattenSections(sections);
81
+ const titles = [];
82
+ visit(tree, 'heading', (node) => {
83
+ titles.push(headingText(node));
84
+ });
85
+ const errorCounts = new Map();
86
+ for (const error of options.errors ?? []) {
87
+ const owner = sectionAtLine(flatSections, error.line);
88
+ if (owner)
89
+ errorCounts.set(owner.id, (errorCounts.get(owner.id) ?? 0) + 1);
90
+ }
91
+ const gitChanges = gitChangedSections(flatSections, options.gitTree);
92
+ return flatSections.flatMap((section, index) => section.githubSlug
93
+ ? [
94
+ {
95
+ id: section.githubSlug,
96
+ title: titles[index] || section.heading,
97
+ depth: section.depth,
98
+ errorCount: errorCounts.get(section.id) ?? 0,
99
+ hasGitChanges: gitChanges.has(section.id),
100
+ },
101
+ ]
102
+ : []);
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plebeiantech/lat.md",
3
- "version": "0.12.3-fork.3",
3
+ "version": "0.12.3-fork.5",
4
4
  "description": "A knowledge graph for your codebase, written in markdown",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,12 +32,14 @@
32
32
  "@vitejs/plugin-react": "^5.0.0",
33
33
  "@types/mdast": "^4.0.4",
34
34
  "@types/node": "^25.3.0",
35
+ "graphology": "^0.26.0",
35
36
  "highlight.js": "^11.12.0",
36
37
  "mdast-util-from-markdown": "^2.0.3",
37
38
  "micromark-util-types": "^2.0.2",
38
39
  "prettier": "^3.8.1",
39
40
  "react": "^19.0.0",
40
41
  "react-dom": "^19.0.0",
42
+ "sigma": "^3.0.3",
41
43
  "tsx": "^4.21.0",
42
44
  "typescript": "^5.7.0",
43
45
  "vite": "^7.0.0",
@@ -17,6 +17,8 @@ After EVERY task, before responding to the user:
17
17
 
18
18
  This project uses [lat.md](https://www.npmjs.com/package/lat.md) to maintain a structured knowledge graph of its architecture, design decisions, and test specs in the `lat.md/` directory. It is a set of cross-linked markdown files that describe **what** this project does and **why** — the domain concepts, key design decisions, business logic, and test specifications. Use it to ground your work in the actual architecture rather than guessing. Do not treat `lat.md/` as a journal or changelog; it should be a focused snapshot of current or planned state, and should not grow just to note insignificant details.
19
19
 
20
+ Project-specific lat documentation belongs in `lat.md/`. Do not modify this generated instruction file or a generated `lat-md` `SKILL.md` to record project guidance: both are owned by lat tooling and may be replaced by a later `lat init`.
21
+
20
22
  # Commands
21
23
 
22
24
  ```bash
@@ -10,6 +10,8 @@ description: >-
10
10
 
11
11
  This skill covers the syntax, structure rules, and conventions for writing `lat.md/` files. Load it whenever you need to create or edit sections in the `lat.md/` directory.
12
12
 
13
+ Project-specific lat documentation belongs in `lat.md/`. Do not modify this generated skill or a generated `AGENTS.md` to record project guidance: both are owned by lat tooling and may be replaced by a later `lat init`.
14
+
13
15
  ## What belongs in lat.md
14
16
 
15
17
  `lat.md/` files describe **what** the project does and **why** — domain concepts, key design decisions, business logic, and test specifications. They do NOT duplicate source code. Think of each section as an anchor that source code references back to.
@@ -1 +0,0 @@
1
- :root{color:#25231f;background:#f8f7f3;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-synthesis:none;text-rendering:optimizeLegibility;--background: #f8f7f3;--panel: #f0eee8;--panel-border: #dedbd1;--text: #25231f;--muted: #777268;--accent: #7357d9;--accent-soft: #e7e0ff;--danger: #b72f3d;--danger-soft: #f8dfe2;--danger-border: #e7a5ad;--warning: #a87000;--warning-soft: #f5e7b8;--success: #2f7a48;--success-soft: #dceede;--git-notification: #e47f19;--code: #ece9e1;--syntax-built-in: #9a3f75;--syntax-comment: #777268;--syntax-keyword: #6c4bc2;--syntax-number: #a85414;--syntax-string: #397348;--syntax-title: #26649a}@media(prefers-color-scheme:dark){:root{color:#e8e6df;background:#181816;--background: #181816;--panel: #201f1c;--panel-border: #34322d;--text: #e8e6df;--muted: #9c978d;--accent: #ad9aff;--accent-soft: #332b52;--danger: #ff7e8a;--danger-soft: #49262b;--danger-border: #713941;--warning: #e9bd4c;--warning-soft: #43391e;--success: #83d79a;--success-soft: #213d29;--git-notification: #f0a044;--code: #25241f;--syntax-built-in: #e6a2cc;--syntax-comment: #88847c;--syntax-keyword: #c2adff;--syntax-number: #efb271;--syntax-string: #9fd0a5;--syntax-title: #8fc6ef}}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;color:var(--text);background:var(--background)}a{color:var(--accent)}.app-shell{display:grid;grid-template-columns:260px minmax(0,1fr);min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;padding:28px 18px;overflow-y:auto;background:var(--panel);border-right:1px solid var(--panel-border)}.sidebar-header{display:flex;margin:0 10px 34px;align-items:center;justify-content:space-between}.brand{display:inline-block;color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:21px;font-weight:750;letter-spacing:-.04em;text-decoration:none}.brand span{color:var(--accent)}.sidebar-actions{display:flex;align-items:center;gap:3px}.sidebar-search,.sidebar-git{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;color:var(--muted);cursor:pointer;background:transparent;border:0;border-radius:6px}.sidebar-search:hover,.sidebar-search[aria-current=page],.sidebar-git:hover{color:var(--accent);background:var(--accent-soft)}.sidebar-git[aria-pressed=true]{color:var(--warning);background:var(--warning-soft)}.sidebar-git{position:relative}.sidebar-git[data-has-changes=true]:after{position:absolute;top:3px;right:3px;width:6px;height:6px;content:"";background:var(--git-notification);border-radius:50%;box-shadow:0 0 0 1.5px var(--panel)}.sidebar-search svg,.sidebar-git svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-linecap:round;stroke-width:1.8}.file-tree{display:grid;min-width:0;gap:2px}.tree-directory>summary{display:flex;padding:7px 10px;align-items:center;gap:7px;color:var(--muted);font-size:12px;font-weight:650;cursor:pointer;list-style:none;border-radius:7px}.tree-directory>summary::-webkit-details-marker{display:none}.tree-directory>summary>a,.tree-directory>summary>span{min-width:0;flex:1;color:inherit;text-decoration:none}.tree-directory>summary>a,.tree-directory>summary>span{display:flex;align-items:center;gap:8px}.tree-directory>summary:before{content:"›";display:inline-block;width:10px;font-size:15px;line-height:1;transform-origin:center;transition:transform .12s ease}.tree-directory[open]>summary:before{transform:rotate(90deg)}.tree-directory>summary:hover{color:var(--text);background:color-mix(in srgb,var(--accent-soft) 55%,transparent)}.tree-children{display:grid;margin-left:15px;padding-left:7px;gap:2px;border-left:1px solid var(--panel-border)}.tree-directory[open]>.tree-children{margin-top:2px}.document-link{display:flex;padding:7px 10px;align-items:center;gap:8px;overflow:hidden;color:var(--muted);font-size:13px;line-height:1.35;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;border-radius:7px}.document-link-name{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.document-state-disc{width:7px;height:7px;flex:0 0 auto;border-radius:50%}.document-state-disc.has-errors{background:var(--danger);box-shadow:0 0 0 2px color-mix(in srgb,var(--danger) 12%,transparent)}.document-state-disc.git-modified{background:var(--warning);box-shadow:0 0 0 2px color-mix(in srgb,var(--warning) 12%,transparent)}.document-state-disc.git-new{background:var(--success);box-shadow:0 0 0 2px color-mix(in srgb,var(--success) 12%,transparent)}.document-state-disc.has-errors.git-modified{background:linear-gradient(90deg,var(--danger) 0 50%,var(--warning) 50% 100%)}.document-state-disc.has-errors.git-new{background:linear-gradient(90deg,var(--danger) 0 50%,var(--success) 50% 100%)}.document-link:hover,.document-link.active{color:var(--text);background:var(--accent-soft)}.document-link.active{font-weight:650}.main{width:100%;max-width:980px;min-width:0;padding:46px clamp(28px,6vw,82px) 100px}.main.restoring-history-scroll{visibility:hidden}.document-header{max-width:760px;margin-bottom:34px}.document-metadata{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.document-path{color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px}.document-flag{padding:3px 8px;color:var(--accent);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;font-weight:700;letter-spacing:.025em;background:var(--accent-soft);border:1px solid color-mix(in srgb,var(--accent) 25%,transparent);border-radius:999px}.document-error-toggle{display:inline-flex;padding:3px 8px;align-items:center;color:var(--danger);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;font-weight:700;letter-spacing:.025em;cursor:pointer;background:var(--danger-soft);border:1px solid var(--danger-border);border-radius:999px}.document-error-toggle:hover,.document-error-toggle[aria-expanded=true]{color:var(--text)}.document-error-panel{margin-top:12px;overflow:hidden;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:color-mix(in srgb,var(--danger-soft) 55%,var(--background));border:1px solid var(--danger-border);border-radius:8px}.document-error-header{padding:9px 13px 7px;color:var(--danger);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;font-weight:750;letter-spacing:.08em;text-transform:uppercase}.document-error-item{display:grid;padding:10px 13px 11px;gap:5px;color:inherit;text-decoration:none}.document-error-item+.document-error-item{border-top:1px solid var(--danger-border)}.document-error-item:hover{background:color-mix(in srgb,var(--danger-soft) 75%,transparent)}.document-error-location{color:var(--danger);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;font-weight:700}.document-error-message{color:var(--muted);font-size:12px;line-height:1.45;white-space:pre-wrap}.source-context{position:relative;z-index:1;width:calc(100% - 5.25rem);max-width:760px;margin:12px 16px 12px 4.25rem;overflow:visible;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:color-mix(in srgb,var(--code) 72%,transparent);border:1px solid var(--panel-border);border-radius:8px}.source-context.has-references:not(.references-open){margin-bottom:24px}.source-context-header{display:flex;min-height:36px;padding:7px 10px 7px 13px;align-items:center;gap:12px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.source-reference-toggle{position:relative;z-index:1;display:flex;height:0;align-items:center;gap:10px}.source-reference-toggle:before,.source-reference-toggle:after{height:1px;flex:1;content:"";background:var(--panel-border);visibility:hidden}.source-context.references-open .source-reference-toggle:before,.source-context.references-open .source-reference-toggle:after{visibility:visible}.source-reference-toggle button{display:inline-flex;padding:4px 7px;align-items:center;gap:6px;color:var(--accent);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;font-weight:650;line-height:1.3;letter-spacing:.02em;text-transform:none;cursor:pointer;background:var(--accent-soft);border:0;border-radius:5px}.source-reference-toggle button:hover{color:var(--text)}.source-reference-toggle button>span{min-width:16px;padding:1px 4px;color:var(--muted);text-align:center;background:color-mix(in srgb,var(--background) 65%,transparent);border-radius:999px}.source-context-current{padding:0 13px 13px}.source-context-current>a,.source-reference-item>a{color:var(--text);text-decoration:none}.source-breadcrumbs{display:flex;align-items:center;gap:5px;overflow:hidden;color:var(--accent);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;font-weight:650;white-space:nowrap}.source-breadcrumbs>span{display:inline-flex;min-width:0;align-items:center;gap:5px}.source-breadcrumbs>span>span{color:var(--muted)}.source-context-current>a:hover .source-breadcrumbs,.source-reference-item>a:hover .source-breadcrumbs{text-decoration:underline;text-underline-offset:3px}.source-context-paragraph p{margin:8px 0 0;color:var(--text);font-size:13px;line-height:1.5}.source-context-paragraph a,.source-reference-paragraph a{text-underline-offset:2px}.source-reference-list{padding:17px 0 3px}.source-reference-item{padding:10px 13px}.source-reference-item+.source-reference-item{border-top:1px solid var(--panel-border)}.source-reference-paragraph p{margin:5px 0 0;color:var(--muted);font-size:12px;line-height:1.4}.source-code{max-width:100%;padding:10px 0;overflow-x:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;line-height:1.55;background:var(--code);border:1px solid var(--panel-border);border-radius:10px;overflow-anchor:none}.source-collapse{display:flex;min-width:max-content;min-height:34px;align-items:center;color:var(--muted);background:color-mix(in srgb,var(--panel) 52%,transparent);border-top:1px solid color-mix(in srgb,var(--panel-border) 72%,transparent);border-bottom:1px solid color-mix(in srgb,var(--panel-border) 72%,transparent)}.source-collapse:first-child{margin-top:-10px;border-top:0;border-radius:10px 10px 0 0}.source-collapse:last-child{margin-bottom:-10px;border-bottom:0;border-radius:0 0 10px 10px}.source-collapse>span{flex:0 0 4.25rem;padding-right:14px;text-align:right;-webkit-user-select:none;user-select:none}.source-collapse button{padding:6px 20px 6px 0;color:var(--muted);font:inherit;font-size:11px;cursor:pointer;background:transparent;border:0}.source-collapse button:hover{color:var(--accent)}.source-line{display:flex;min-width:max-content}.source-line.focused{background:color-mix(in srgb,var(--accent-soft) 72%,transparent)}.source-line-number{flex:0 0 4.25rem;padding-right:14px;color:var(--muted);text-align:right;-webkit-user-select:none;user-select:none}.source-line.focused .source-line-number{color:var(--accent)}.source-line-content{flex:1 0 auto;padding-right:20px;color:var(--text);white-space:pre}.source-code .hljs-comment,.source-code .hljs-quote{color:var(--syntax-comment);font-style:italic}.source-code .hljs-keyword,.source-code .hljs-literal,.source-code .hljs-selector-tag,.source-code .hljs-section{color:var(--syntax-keyword)}.source-code .hljs-string,.source-code .hljs-regexp,.source-code .hljs-addition,.source-code .hljs-attribute{color:var(--syntax-string)}.source-code .hljs-number,.source-code .hljs-symbol,.source-code .hljs-bullet{color:var(--syntax-number)}.source-code .hljs-title,.source-code .hljs-function,.source-code .hljs-class,.source-code .hljs-type{color:var(--syntax-title)}.source-code .hljs-built_in,.source-code .hljs-variable.language_,.source-code .hljs-meta{color:var(--syntax-built-in)}.source-code .hljs-deletion{color:#bd3d3d}.source-code .hljs-emphasis{font-style:italic}.source-code .hljs-strong{font-weight:700}.markdown{max-width:760px;font-size:16px;line-height:1.72}.markdown h1,.markdown h2,.markdown h3,.markdown h4{margin:2em 0 .65em;line-height:1.2;letter-spacing:-.025em;scroll-margin-top:24px}.section-back-reference-toggle{display:inline-flex;padding:3px 5px 3px 7px;margin-left:10px;align-items:center;gap:5px;color:var(--accent);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;font-weight:650;line-height:1.3;letter-spacing:.01em;vertical-align:middle;cursor:pointer;background:var(--accent-soft);border:0;border-radius:5px}.section-back-reference-toggle:hover,.section-back-reference-toggle[aria-expanded=true]{color:var(--text)}.section-back-reference-count{min-width:16px;padding:1px 4px;color:var(--muted);text-align:center;background:color-mix(in srgb,var(--background) 65%,transparent);border-radius:999px}.section-back-reference-panel{margin:-.15em 0 1.5em;overflow:hidden;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:color-mix(in srgb,var(--code) 72%,transparent);border:1px solid var(--panel-border);border-radius:8px}.section-back-reference-panel[hidden]{display:none}.section-back-reference-header{padding:9px 13px 7px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.section-back-reference-item{padding:10px 13px 11px}.section-back-reference-item+.section-back-reference-item{border-top:1px solid var(--panel-border)}.section-back-reference-location{display:inline-flex;align-items:center;gap:5px;color:var(--accent);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px;font-weight:650;line-height:1.35;text-decoration:none}.section-back-reference-location:hover{text-decoration:underline;text-underline-offset:3px}.section-back-reference-location>span[aria-hidden=true]{color:var(--muted)}.section-back-reference-paragraph p{margin:5px 0 0;color:var(--muted);font-size:12px;line-height:1.45}.section-back-reference-paragraph a{text-underline-offset:2px}.section-back-reference-code .code-link-language{margin-right:0;font-size:7px}.markdown .section-back-reference-code>code{display:block;padding:7px 9px;margin-top:7px;overflow-x:auto;color:var(--muted);font-size:11px;line-height:1.4;white-space:pre;border:1px solid color-mix(in srgb,var(--panel-border) 75%,transparent)}.markdown h1{margin-top:0;font-size:clamp(32px,5vw,46px)}.markdown h2{padding-top:.4em;font-size:26px;border-top:1px solid var(--panel-border)}.markdown h3{font-size:20px}.markdown p,.markdown ul,.markdown ol,.markdown blockquote,.markdown pre{margin:0 0 1.25em}.markdown li+li{margin-top:.35em}.markdown a{text-decoration-thickness:1px;text-underline-offset:3px}.markdown .markdown-error{color:var(--danger);scroll-margin-top:24px;text-decoration-color:currentColor}.markdown p.markdown-error,.markdown li.markdown-error,.markdown blockquote.markdown-error,.markdown pre.markdown-error,.markdown h1.markdown-error,.markdown h2.markdown-error,.markdown h3.markdown-error,.markdown h4.markdown-error,.markdown h5.markdown-error,.markdown h6.markdown-error{padding-left:10px;background:color-mix(in srgb,var(--danger-soft) 42%,transparent);box-shadow:inset 3px 0 0 var(--danger)}.markdown a.markdown-error,.markdown span.markdown-error{padding:.08em .2em;margin:-.08em -.2em;background:var(--danger-soft);border-radius:3px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.markdown ins.git-added,.markdown del.git-removed{padding:.08em .12em;margin:-.08em -.04em;border-radius:3px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.markdown ins.git-added{color:var(--success);text-decoration:none;background:var(--success-soft)}.markdown del.git-removed{color:var(--danger);background:var(--danger-soft);text-decoration-thickness:1px}.markdown p.git-added,.markdown blockquote.git-added,.markdown pre.git-added,.markdown h1.git-added,.markdown h2.git-added,.markdown h3.git-added,.markdown h4.git-added,.markdown h5.git-added,.markdown h6.git-added,.markdown li.git-added{padding-left:10px;color:var(--success);background:color-mix(in srgb,var(--success-soft) 55%,transparent);box-shadow:inset 3px 0 0 var(--success)}.markdown p.git-removed,.markdown div.git-removed,.markdown blockquote.git-removed,.markdown pre.git-removed,.markdown li.git-removed{padding-left:10px;color:var(--danger);text-decoration:line-through;background:color-mix(in srgb,var(--danger-soft) 55%,transparent);box-shadow:inset 3px 0 0 var(--danger)}.wiki-link-context{color:var(--muted)}a.wiki-link-segmented{text-decoration:none}a.wiki-link-segmented>.wiki-link-context,a.wiki-link-segmented>.wiki-link-leaf{text-decoration-line:underline;text-decoration-color:currentColor;text-decoration-thickness:1px;text-underline-offset:3px}.code-link-language{display:inline-flex;width:1.55em;height:1.55em;margin-right:.38em;align-items:center;justify-content:center;color:#fff;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.62em;font-style:normal;font-weight:800;line-height:1;text-decoration:none;vertical-align:.12em;background:var(--muted);border-radius:3px}.code-language-ts{background:#3178c6}.code-language-js{color:#25231f;background:#f0db4f}.code-language-py{background:#3776ab}.code-language-rs{background:#9a4b2f}.code-language-go{color:#15343b;background:#6ad7e5}.code-language-c{background:#5968ba}a.wiki-link-active{padding:.12em .28em;margin:-.12em -.28em;background:var(--accent-soft);border-radius:4px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.markdown blockquote{margin-left:0;padding-left:18px;color:var(--muted);border-left:3px solid var(--panel-border)}.markdown code{padding:.16em .36em;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.9em;background:var(--code);border-radius:5px}.markdown pre{padding:18px 20px;overflow-x:auto;background:var(--code);border:1px solid var(--panel-border);border-radius:10px}.markdown pre code{padding:0;background:transparent}.markdown img{max-width:100%}.markdown hr{margin:2.5em 0;border:0;border-top:1px solid var(--panel-border)}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;white-space:nowrap;border:0;clip:rect(0,0,0,0)}.search-page{width:100%;max-width:760px}.search-input-shell{display:flex;height:52px;padding:0 16px;align-items:center;gap:11px;color:var(--muted);background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;box-shadow:0 1px 1px color-mix(in srgb,var(--text) 5%,transparent)}.search-input-shell:focus-within{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 55%,var(--panel-border));box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent)}.search-input-shell svg{width:20px;height:20px;flex:0 0 auto;fill:none;stroke:currentColor;stroke-linecap:round;stroke-width:1.8}.search-input-shell input{width:100%;min-width:0;padding:0;color:var(--text);font:inherit;font-size:17px;line-height:1;background:transparent;border:0;outline:0}.search-input-shell input::placeholder{color:var(--muted)}.search-input-shell input::-webkit-search-cancel-button{cursor:pointer}.search-status{min-height:42px;padding:12px 2px 8px;color:var(--muted);font-size:12px}.search-results{display:grid;gap:10px}.search-result{display:block;padding:16px 18px;color:var(--text);text-decoration:none;background:color-mix(in srgb,var(--panel) 72%,transparent);border:1px solid var(--panel-border);border-radius:9px;transition:border-color .12s ease,background .12s ease,transform .12s ease}.search-result:hover{background:var(--panel);border-color:color-mix(in srgb,var(--accent) 38%,var(--panel-border));transform:translateY(-1px)}.search-result-breadcrumbs{display:flex;align-items:center;gap:5px;overflow:hidden;color:var(--accent);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;font-weight:650;white-space:nowrap}.search-result-breadcrumbs>span{display:inline-flex;min-width:0;align-items:center;gap:5px}.search-result-breadcrumbs>span>span{color:var(--muted)}.search-result h2{margin:7px 0 0;font-size:18px;line-height:1.3;letter-spacing:-.015em}.search-result p{margin:6px 0 0;color:var(--muted);font-size:13px;line-height:1.5}.state{display:flex;min-height:50vh;align-items:center;justify-content:center;color:var(--muted)}.state.error{flex-direction:column;gap:8px;text-align:center}.state.error strong{color:var(--text)}@media(max-width:760px){.app-shell{display:block}.sidebar{position:static;width:100%;height:auto;padding:18px;border-right:0;border-bottom:1px solid var(--panel-border)}.sidebar-header{margin-bottom:18px}.sidebar nav{display:none}.main{padding-top:28px}.source-context{width:calc(100% - 24px);margin-right:12px;margin-left:12px}}