@plebeiantech/lat.md 0.12.3-fork.7 → 0.12.3-fork.9

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.
@@ -9,4 +9,13 @@ export declare const MODE_DIRS: Record<DiataxisMode, string>;
9
9
  * content, so the `require-mode` gate below never asks one to declare a mode.
10
10
  */
11
11
  export declare function indexNameFor(dirName: string): string;
12
- export declare function checkMode(latticeDir: string, projectRoot?: string): Promise<CheckError[]>;
12
+ export type CheckModeOptions = {
13
+ /**
14
+ * Enforce as if the root index said this, whatever it actually says. Used to
15
+ * price adoption — running once each way and taking the difference is what
16
+ * the offer in `lat init` reports, and it keeps that number defined by this
17
+ * checker rather than by a second copy of its rules.
18
+ */
19
+ requireMode?: boolean;
20
+ };
21
+ export declare function checkMode(latticeDir: string, projectRoot?: string, options?: CheckModeOptions): Promise<CheckError[]>;
@@ -190,7 +190,7 @@ export function indexNameFor(dirName) {
190
190
  return dirName.endsWith('.md') ? dirName : dirName + '.md';
191
191
  }
192
192
  /**
193
- * Whether the root index turns the mode requirement on.
193
+ * What the root index says about the mode requirement.
194
194
  *
195
195
  * Opt-in, and deliberately read from the tree rather than from a flag or an
196
196
  * environment variable: the rule belongs to a documentation set, not to a
@@ -198,21 +198,36 @@ export function indexNameFor(dirName) {
198
198
  * `lat init` stamps the flag into the root index it scaffolds, so a project
199
199
  * set up after this exists is gated from its first commit while an older one
200
200
  * is untouched until someone adds the line.
201
+ *
202
+ * The raw value is returned rather than a boolean so the caller can tell an
203
+ * absent flag from one that was set to something this never reads. `yes` and
204
+ * `1` are strings to a YAML 1.2 parser, and enforcing nothing on them while
205
+ * saying nothing about them is how a project ends up believing the gate is on.
201
206
  */
202
- async function requireModeEnabled(latticeDir) {
207
+ async function readRequireModeFlag(latticeDir) {
203
208
  const indexPath = join(latticeDir, indexNameFor(basename(latticeDir)));
204
209
  try {
205
210
  const content = await readFile(indexPath, 'utf-8');
206
- return parseFrontmatter(content).raw['require-mode'] === true;
211
+ return parseFrontmatter(content).raw['require-mode'];
207
212
  }
208
213
  catch {
209
- return false;
214
+ return undefined;
210
215
  }
211
216
  }
212
- export async function checkMode(latticeDir, projectRoot = dirname(latticeDir)) {
217
+ export async function checkMode(latticeDir, projectRoot = dirname(latticeDir), options = {}) {
213
218
  const files = await listLatticeFiles(latticeDir);
214
219
  const errors = [];
215
- const requireMode = await requireModeEnabled(latticeDir);
220
+ const flag = await readRequireModeFlag(latticeDir);
221
+ const requireMode = options.requireMode ?? flag === true;
222
+ if (flag !== undefined && typeof flag !== 'boolean') {
223
+ const indexPath = join(latticeDir, indexNameFor(basename(latticeDir)));
224
+ errors.push({
225
+ file: relative(process.cwd(), indexPath),
226
+ line: 1,
227
+ target: toPosix(relative(projectRoot, indexPath)).replace(/\.md$/, ''),
228
+ message: `require-mode is ${JSON.stringify(flag)} — it must be true or false.\n Anything else is enforced as off, so the gate this line looks like it turns on is not running.`,
229
+ });
230
+ }
216
231
  for (const file of files) {
217
232
  const content = await readFile(file, 'utf-8');
218
233
  const relPath = relative(process.cwd(), file);
@@ -1,31 +1,88 @@
1
+ /** What the root index says about the gate today. */
2
+ export type RequireModeState = 'on' | 'off' | 'unset' | 'invalid';
1
3
  /**
2
- * Turn the gate on in a root index, whatever frontmatter it already has.
4
+ * Read the gate straight from a root index's frontmatter.
3
5
  *
4
- * Bailing on any existing frontmatter — the first version of this — produced a
5
- * silent no-op: a root index carrying `lat: tags: [...]` could never opt in,
6
- * and `lat init` re-offered on every run because the flag it had just written
7
- * was not there. Merging is what makes the offer terminate.
6
+ * Four states rather than a boolean, because `checkMode` enforces on `=== true`
7
+ * and anything looser here disagrees with it. A root index reading
8
+ * `require-mode: yes` is a string to this parser: treating that as "set" left
9
+ * the user believing the gate was on, `checkMode` enforcing nothing, and the
10
+ * one prompt that would have said so permanently silenced by the same value.
11
+ * `invalid` is reported rather than assumed either way.
12
+ */
13
+ export declare function requireModeState(content: string): RequireModeState;
14
+ /** What `planRequireMode` decided, before anything is written or asked. */
15
+ export type StampPlan =
16
+ /** Already `true`; nothing to do. */
17
+ {
18
+ kind: 'on';
19
+ }
20
+ /** Explicitly `false`. A deliberate opt-out is not re-offered. */
21
+ | {
22
+ kind: 'off';
23
+ }
24
+ /** The edited document, ready to write. */
25
+ | {
26
+ kind: 'stamped';
27
+ content: string;
28
+ movedRootKey: boolean;
29
+ }
30
+ /** Present but neither `true` nor `false`. */
31
+ | {
32
+ kind: 'invalid';
33
+ detail: string;
34
+ }
35
+ /** The frontmatter does not parse, so no field on it is read at all. */
36
+ | {
37
+ kind: 'unparseable';
38
+ detail: string;
39
+ }
40
+ /** `lat:` holds something that cannot carry a mapping key. */
41
+ | {
42
+ kind: 'unsupported';
43
+ detail: string;
44
+ };
45
+ /**
46
+ * Decide what turning the gate on in this document would do, without doing it.
47
+ *
48
+ * Parsed and re-emitted through the YAML document API rather than edited line
49
+ * by line. Line surgery looked cheaper — it preserves the file byte for byte
50
+ * except where it inserts — but it has to answer, from raw text, every question
51
+ * the parser already answers: which line opens the block, which lines are the
52
+ * `lat:` mapping's own children rather than a later key's, how far they are
53
+ * indented, and whether the value on the `lat:` line is a flow mapping, an
54
+ * anchor or a block scalar. Each wrong answer was its own defect: a flow
55
+ * mapping and an anchor were refused although both merge cleanly, an empty
56
+ * `lat:` beside a sibling list was refused because the scan walked past the end
57
+ * of the block, and one shape produced frontmatter that no longer parsed at
58
+ * all. `parseDocument` answers all of it, and round-trips comments and blank
59
+ * lines while doing so.
8
60
  *
9
- * Edited line by line rather than parsed and re-emitted, because re-emitting
10
- * reformats the whole block and discards its comments. The one shape that
11
- * cannot be edited safely — `lat:` carrying a flow mapping on its own line —
12
- * is returned unchanged, and the caller falls back to printing the edit.
61
+ * The one thing the document API does not give for free is that the fields
62
+ * already there survive, so that is asserted below rather than assumed.
13
63
  */
64
+ export declare function planRequireMode(content: string): StampPlan;
65
+ /** The edited document when the gate can be turned on, the input when not. */
14
66
  export declare function stampRequireMode(content: string): string;
15
- /** Whether the gate is on, read straight from a root index's frontmatter. */
16
- export declare function requireModeSet(content: string): boolean;
17
67
  /**
18
68
  * Append the four mode directories to the root index.
19
69
  *
20
70
  * Without this a fresh `lat init` leaves `lat check index` reporting four
21
71
  * missing entries, which is a poor first impression of a tool whose whole
22
- * pitch is that the check passes.
72
+ * pitch is that the check passes. Each directory is tested for separately: an
73
+ * index that already links `reference/` used to suppress the other three, so
74
+ * the three unlinked directories this had just created turned a passing
75
+ * `lat check index` into a failing one.
23
76
  */
24
77
  export declare function listModeDirs(content: string): string;
25
78
  /**
26
- * Scaffold the four mode directories and turn the gate on. Called only on the
27
- * run that creates `lat.md/`, so an existing tree is never restructured
28
- * underneath its owner.
79
+ * Scaffold the four mode directories and turn the gate on.
80
+ *
81
+ * Nothing at all happens when the gate cannot land in the root index. A tree
82
+ * left with four new directories and the gate still off is worse than one left
83
+ * alone: `checkMode` passes on it, so nothing ever says the adoption stopped
84
+ * half way — and if the index was not rewritten to list them, `lat check index`
85
+ * fails instead, on four directories the user never asked for.
29
86
  */
30
87
  export declare function writeForkScaffold(latDir: string): void;
31
88
  /**
@@ -36,9 +93,14 @@ export declare function writeForkScaffold(latDir: string): void;
36
93
  * for every project that needs it. Restructuring someone's tree without asking
37
94
  * is still wrong, so this asks.
38
95
  *
39
- * Silent when the flag is already set, when a previous run was told no, and
40
- * when there is no TTY to ask — the non-interactive path prints the count and
41
- * the manual edit instead, and records nothing, so a later interactive run
42
- * still offers.
96
+ * Whether the flag *can* be written is decided before anything is asked and
97
+ * before anything is written. Asking first cost a tree four directories and a
98
+ * rewritten index in exchange for a gate that never landed, and the user was
99
+ * told in the same breath that nothing could be edited.
100
+ *
101
+ * Silent when the flag is already set either way, when a previous run was told
102
+ * no, and when there is no TTY to ask — the non-interactive path prints the
103
+ * count and the manual edit instead, and records nothing, so a later
104
+ * interactive run still offers.
43
105
  */
44
106
  export declare function offerRequireMode(latDir: string, interactive: boolean, ask: (message: string) => Promise<boolean>): Promise<void>;
@@ -1,8 +1,9 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { listLatticeFiles, parseFrontmatter } from '../lattice.js';
3
- import { basename, join, relative, sep } from 'node:path';
2
+ import { basename, dirname, join } from 'node:path';
4
3
  import { styleText } from 'node:util';
5
- import { DIATAXIS_MODES, MODE_DIRS, indexNameFor } from './check-mode.js';
4
+ import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
5
+ import { parseFrontmatter } from '../lattice.js';
6
+ import { DIATAXIS_MODES, MODE_DIRS, checkMode, indexNameFor, } from './check-mode.js';
6
7
  /**
7
8
  * Give a freshly created `lat.md/` the Diátaxis shape, and turn the gate on.
8
9
  *
@@ -71,84 +72,213 @@ function indexBody(dir) {
71
72
  'from a decision belong in `how-to/`, and the facts it fixes belong in ' +
72
73
  '`reference/`.\n');
73
74
  }
75
+ // ── Turning the gate on in a root index ──────────────────────────────
74
76
  /**
75
- * Turn the gate on in a root index, whatever frontmatter it already has.
77
+ * The frontmatter fence, copied from `parseFrontmatter` rather than written
78
+ * afresh.
76
79
  *
77
- * Bailing on any existing frontmatter — the first version of this — produced a
78
- * silent no-op: a root index carrying `lat: tags: [...]` could never opt in,
79
- * and `lat init` re-offered on every run because the flag it had just written
80
- * was not there. Merging is what makes the offer terminate.
80
+ * The two readers have to agree on what counts as frontmatter. When they did
81
+ * not — this pattern once required the closing fence to end its line, which
82
+ * upstream's does not — a root index closed with `----` was frontmatter to the
83
+ * parser and no frontmatter here, so a whole second block was prepended above
84
+ * the real one. The original block still sat in the file, still looked live,
85
+ * and every field it declared was silently no longer read.
86
+ */
87
+ const FENCE = /^---\n([\s\S]*?)\n---/;
88
+ /**
89
+ * `lineWidth: 0` stops long values being wrapped onto continuation lines, and
90
+ * `flowCollectionPadding: false` keeps `[a, b]` from becoming `[ a, b ]`. Both
91
+ * exist to keep the diff to the line that changed.
92
+ */
93
+ const STRINGIFY = { flowCollectionPadding: false, lineWidth: 0 };
94
+ /**
95
+ * Read the gate straight from a root index's frontmatter.
81
96
  *
82
- * Edited line by line rather than parsed and re-emitted, because re-emitting
83
- * reformats the whole block and discards its comments. The one shape that
84
- * cannot be edited safely — `lat:` carrying a flow mapping on its own line —
85
- * is returned unchanged, and the caller falls back to printing the edit.
97
+ * Four states rather than a boolean, because `checkMode` enforces on `=== true`
98
+ * and anything looser here disagrees with it. A root index reading
99
+ * `require-mode: yes` is a string to this parser: treating that as "set" left
100
+ * the user believing the gate was on, `checkMode` enforcing nothing, and the
101
+ * one prompt that would have said so permanently silenced by the same value.
102
+ * `invalid` is reported rather than assumed either way.
86
103
  */
87
- export function stampRequireMode(content) {
88
- const fence = content.match(/^---\n([\s\S]*?)\n---[ \t]*(\r?\n|$)/);
104
+ export function requireModeState(content) {
105
+ const value = parseFrontmatter(content).raw['require-mode'];
106
+ if (value === undefined)
107
+ return 'unset';
108
+ if (value === true)
109
+ return 'on';
110
+ if (value === false)
111
+ return 'off';
112
+ return 'invalid';
113
+ }
114
+ function shapeOf(node) {
115
+ if (isSeq(node))
116
+ return 'a list';
117
+ if (isScalar(node))
118
+ return `the value ${JSON.stringify(node.value)}`;
119
+ return 'something other than a mapping';
120
+ }
121
+ /**
122
+ * Decide what turning the gate on in this document would do, without doing it.
123
+ *
124
+ * Parsed and re-emitted through the YAML document API rather than edited line
125
+ * by line. Line surgery looked cheaper — it preserves the file byte for byte
126
+ * except where it inserts — but it has to answer, from raw text, every question
127
+ * the parser already answers: which line opens the block, which lines are the
128
+ * `lat:` mapping's own children rather than a later key's, how far they are
129
+ * indented, and whether the value on the `lat:` line is a flow mapping, an
130
+ * anchor or a block scalar. Each wrong answer was its own defect: a flow
131
+ * mapping and an anchor were refused although both merge cleanly, an empty
132
+ * `lat:` beside a sibling list was refused because the scan walked past the end
133
+ * of the block, and one shape produced frontmatter that no longer parsed at
134
+ * all. `parseDocument` answers all of it, and round-trips comments and blank
135
+ * lines while doing so.
136
+ *
137
+ * The one thing the document API does not give for free is that the fields
138
+ * already there survive, so that is asserted below rather than assumed.
139
+ */
140
+ export function planRequireMode(content) {
141
+ const state = requireModeState(content);
142
+ if (state === 'on')
143
+ return { kind: 'on' };
144
+ if (state === 'off')
145
+ return { kind: 'off' };
146
+ if (state === 'invalid') {
147
+ const value = parseFrontmatter(content).raw['require-mode'];
148
+ return {
149
+ kind: 'invalid',
150
+ detail: `require-mode is ${JSON.stringify(value)}; it must be true or false`,
151
+ };
152
+ }
153
+ const fence = content.match(FENCE);
154
+ // Every branch below produces `stamped` and then falls through to the same
155
+ // gate. Returning early from any of them is how the last version let the
156
+ // "no frontmatter here" branch write a second block above frontmatter the
157
+ // parser was reading perfectly well.
158
+ let stamped;
159
+ let movedRootKey = false;
89
160
  if (!fence) {
90
- return `---\nlat:\n require-mode: true\n---\n\n${content.replace(/^\n+/, '')}`;
161
+ stamped = `---\nlat:\n require-mode: true\n---\n\n${content.replace(/^\n+/, '')}`;
91
162
  }
92
- const body = fence[1];
93
- if (/^\s*require-mode\s*:/m.test(body))
94
- return content;
95
- const lines = body.split('\n');
96
- const latAt = lines.findIndex((line) => /^lat\s*:/.test(line));
97
- let merged;
98
- if (latAt === -1) {
99
- merged = [...lines, 'lat:', ' require-mode: true'];
163
+ else {
164
+ const doc = parseDocument(fence[1]);
165
+ if (doc.errors.length > 0) {
166
+ return {
167
+ kind: 'unparseable',
168
+ detail: doc.errors[0].message.split('\n')[0],
169
+ };
170
+ }
171
+ const node = doc.get('lat', true);
172
+ if (node === undefined || node === null) {
173
+ doc.set('lat', doc.createNode({ 'require-mode': true }));
174
+ }
175
+ else if (isScalar(node) && node.value === null) {
176
+ // `lat:` with nothing under it. Replacing the key in place keeps its
177
+ // position and any comment attached to it.
178
+ doc.set('lat', doc.createNode({ 'require-mode': true }));
179
+ }
180
+ else if (isMap(node)) {
181
+ doc.setIn(['lat', 'require-mode'], true);
182
+ }
183
+ else {
184
+ return {
185
+ kind: 'unsupported',
186
+ detail: `\`lat:\` holds ${shapeOf(node)}, which cannot take \`require-mode\``,
187
+ };
188
+ }
189
+ // A root-level `require-mode` is dead — upstream reads fields only under
190
+ // `lat:` — and `checkFrontmatter` reports it as misplaced. Once the nested
191
+ // key exists that report stops firing, because the diagnostic asks whether
192
+ // the field is missing from `lat:`. Leaving the dead key behind would mean
193
+ // silently ending the one message that would ever have mentioned it, so it
194
+ // is moved rather than shadowed.
195
+ movedRootKey = doc.has('require-mode');
196
+ if (movedRootKey)
197
+ doc.delete('require-mode');
198
+ stamped = `---\n${doc.toString(STRINGIFY)}---${content.slice(fence[0].length)}`;
100
199
  }
101
- else if (lines[latAt].replace(/^lat\s*:/, '').trim() !== '') {
102
- // `lat: {mode: reference}` or `lat: &anchor` — a line-based insert would
103
- // corrupt it, so say nothing and let the caller print the edit.
104
- return content;
200
+ // The invariant, asserted rather than argued.
201
+ //
202
+ // Setting the flag is worth nothing if the merge quietly drops a field that
203
+ // was already there: an edit that lands `require-mode: true` and loses
204
+ // `require-code-mention: true` turns one check on and another off, and `lat
205
+ // check` then passes on a document that used to be enforced. Checking that
206
+ // the output parses is not enough to catch it — the losing edit parses fine.
207
+ // So every key read before must read back the same afterwards.
208
+ const before = parseFrontmatter(content);
209
+ const after = parseFrontmatter(stamped);
210
+ if ((after.problems ?? []).some((problem) => problem.kind === 'parse-error')) {
211
+ return {
212
+ kind: 'unsupported',
213
+ detail: 'the merged frontmatter did not parse',
214
+ };
105
215
  }
106
- else {
107
- // Siblings must share indentation, so take it from the existing first
108
- // child rather than assuming two spaces.
109
- const child = lines
110
- .slice(latAt + 1)
111
- .find((line) => line.trim() !== '' && /^\s/.test(line));
112
- const indent = child ? (child.match(/^\s+/)?.[0] ?? ' ') : ' ';
113
- merged = [
114
- ...lines.slice(0, latAt + 1),
115
- `${indent}require-mode: true`,
116
- ...lines.slice(latAt + 1),
117
- ];
216
+ if (after.raw['require-mode'] !== true) {
217
+ return {
218
+ kind: 'unsupported',
219
+ detail: 'the flag did not land where it is read',
220
+ };
118
221
  }
119
- const end = fence[0].length;
120
- return `---\n${merged.join('\n')}\n---${fence[2] || '\n'}${content.slice(end)}`;
222
+ for (const [key, value] of Object.entries(before.raw)) {
223
+ if (JSON.stringify(after.raw[key]) !== JSON.stringify(value)) {
224
+ return {
225
+ kind: 'unsupported',
226
+ detail: `the merge would change \`${key}\``,
227
+ };
228
+ }
229
+ }
230
+ return { kind: 'stamped', content: stamped, movedRootKey };
121
231
  }
122
- /** Whether the gate is on, read straight from a root index's frontmatter. */
123
- export function requireModeSet(content) {
124
- return parseFrontmatter(content).raw['require-mode'] !== undefined;
232
+ /** The edited document when the gate can be turned on, the input when not. */
233
+ export function stampRequireMode(content) {
234
+ const plan = planRequireMode(content);
235
+ return plan.kind === 'stamped' ? plan.content : content;
125
236
  }
126
237
  /**
127
238
  * Append the four mode directories to the root index.
128
239
  *
129
240
  * Without this a fresh `lat init` leaves `lat check index` reporting four
130
241
  * missing entries, which is a poor first impression of a tool whose whole
131
- * pitch is that the check passes.
242
+ * pitch is that the check passes. Each directory is tested for separately: an
243
+ * index that already links `reference/` used to suppress the other three, so
244
+ * the three unlinked directories this had just created turned a passing
245
+ * `lat check index` into a failing one.
132
246
  */
133
247
  export function listModeDirs(content) {
134
- const entries = DIATAXIS_MODES.map((mode) => {
135
- const dir = MODE_DIRS[mode];
248
+ const missing = DIATAXIS_MODES.map((mode) => MODE_DIRS[mode]).filter((dir) => !content.includes(`(${dir}/`));
249
+ if (missing.length === 0)
250
+ return content;
251
+ const entries = missing
252
+ .map((dir) => {
136
253
  const { title, lead } = MODE_INDEX[dir];
137
254
  return `- [${title}](${dir}/${dir}.md) — ${lead}`;
138
- }).join('\n');
139
- if (content.includes(`(${MODE_DIRS.reference}/`))
140
- return content;
255
+ })
256
+ .join('\n');
141
257
  let base = content;
142
258
  if (!base.endsWith('\n'))
143
259
  base += '\n';
144
- return `${base}\nEvery document belongs in exactly one of these.\n\n${entries}\n`;
260
+ const intro = missing.length === DIATAXIS_MODES.length
261
+ ? '\nEvery document belongs in exactly one of these.\n\n'
262
+ : '\n';
263
+ return `${base}${intro}${entries}\n`;
145
264
  }
146
265
  /**
147
- * Scaffold the four mode directories and turn the gate on. Called only on the
148
- * run that creates `lat.md/`, so an existing tree is never restructured
149
- * underneath its owner.
266
+ * Scaffold the four mode directories and turn the gate on.
267
+ *
268
+ * Nothing at all happens when the gate cannot land in the root index. A tree
269
+ * left with four new directories and the gate still off is worse than one left
270
+ * alone: `checkMode` passes on it, so nothing ever says the adoption stopped
271
+ * half way — and if the index was not rewritten to list them, `lat check index`
272
+ * fails instead, on four directories the user never asked for.
150
273
  */
151
274
  export function writeForkScaffold(latDir) {
275
+ const rootIndex = join(latDir, indexNameFor(basename(latDir)));
276
+ const current = existsSync(rootIndex)
277
+ ? readFileSync(rootIndex, 'utf-8')
278
+ : null;
279
+ const stamped = current === null ? null : stampRequireMode(current);
280
+ if (stamped !== null && requireModeState(stamped) !== 'on')
281
+ return;
152
282
  const created = [];
153
283
  for (const mode of DIATAXIS_MODES) {
154
284
  const dir = MODE_DIRS[mode];
@@ -160,12 +290,10 @@ export function writeForkScaffold(latDir) {
160
290
  writeFileSync(indexPath, indexBody(dir));
161
291
  created.push(`${dir}/`);
162
292
  }
163
- const rootIndex = join(latDir, indexNameFor(basename(latDir)));
164
- if (existsSync(rootIndex)) {
165
- const current = readFileSync(rootIndex, 'utf-8');
166
- const stamped = listModeDirs(stampRequireMode(current));
167
- if (stamped !== current)
168
- writeFileSync(rootIndex, stamped);
293
+ if (current !== null && stamped !== null) {
294
+ const listed = listModeDirs(stamped);
295
+ if (listed !== current)
296
+ writeFileSync(rootIndex, listed);
169
297
  }
170
298
  if (created.length > 0) {
171
299
  console.log(styleText('green', 'Scaffolded Diátaxis modes') +
@@ -179,47 +307,94 @@ export function writeForkScaffold(latDir) {
179
307
  * Where a declined offer is remembered.
180
308
  *
181
309
  * Fork-owned rather than a field on upstream's `lat_init.json`, so recording
182
- * it costs no edit to `src/init-version.ts`. `lat.md/.cache/` is already in
183
- * the scaffolded `.gitignore`, so the marker stays out of the repository.
310
+ * it costs no edit to `src/init-version.ts`.
184
311
  */
185
- function declinePath(latDir) {
312
+ function markerPath(latDir) {
186
313
  return join(latDir, '.cache', 'lat_fork.json');
187
314
  }
188
- function declined(latDir) {
315
+ function readForkMeta(latDir) {
189
316
  try {
190
- const raw = JSON.parse(readFileSync(declinePath(latDir), 'utf-8'));
191
- return (typeof raw === 'object' &&
192
- raw !== null &&
193
- raw['require_mode_declined'] === true);
317
+ const raw = JSON.parse(readFileSync(markerPath(latDir), 'utf-8'));
318
+ // An array is an object too, and spreading one produces `{"0": ...}` —
319
+ // which then overwrites the file with something no reader understands.
320
+ if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
321
+ return raw;
322
+ }
194
323
  }
195
324
  catch {
196
- return false;
325
+ // Absent or unreadable — nothing has been settled yet.
197
326
  }
327
+ return {};
198
328
  }
199
- function recordDecline(latDir) {
200
- mkdirSync(join(latDir, '.cache'), { recursive: true });
201
- writeFileSync(declinePath(latDir), JSON.stringify({ require_mode_declined: true }, null, 2) + '\n');
329
+ function declined(latDir) {
330
+ return readForkMeta(latDir).require_mode_declined === true;
202
331
  }
203
- /** Documents the gate would reject today: not an index, no mode either way. */
204
- async function unmodedDocuments(latDir) {
205
- const modeDirs = new Set(DIATAXIS_MODES.map((mode) => MODE_DIRS[mode]));
206
- const files = await listLatticeFiles(latDir);
207
- const offenders = [];
208
- for (const file of files) {
209
- const rel = relative(latDir, file).split(sep).join('/');
210
- const segments = rel.split('/');
211
- const fileName = segments.pop();
212
- const holder = segments.length === 0 ? basename(latDir) : segments.at(-1);
213
- if (fileName === indexNameFor(holder))
214
- continue;
215
- if (segments.length > 0 && modeDirs.has(segments[0]))
216
- continue;
217
- const declaredMode = parseFrontmatter(readFileSync(file, 'utf-8')).raw['mode'];
218
- if (typeof declaredMode === 'string')
219
- continue;
220
- offenders.push(rel);
332
+ /**
333
+ * Remember a no.
334
+ *
335
+ * The marker is a local answer, not a project fact: committed, it would tell
336
+ * every other clone that this project had already declined. `lat.md/.gitignore`
337
+ * covers `.cache/`, but that file is written only by the run that *creates*
338
+ * `lat.md/` — and this path exists for trees `lat init` did not create — so the
339
+ * cache is made to ignore itself rather than trusting a file that may not be
340
+ * there.
341
+ *
342
+ * Failure to record is reported and survived. This runs after the tree has
343
+ * already been restructured, and an unwritable cache directory is not a reason
344
+ * to abort `lat init` with a stack trace at that point.
345
+ */
346
+ function recordDeclined(latDir) {
347
+ try {
348
+ const cacheDir = join(latDir, '.cache');
349
+ mkdirSync(cacheDir, { recursive: true });
350
+ const selfIgnore = join(cacheDir, '.gitignore');
351
+ if (!existsSync(selfIgnore))
352
+ writeFileSync(selfIgnore, '*\n');
353
+ writeFileSync(markerPath(latDir), JSON.stringify({ ...readForkMeta(latDir), require_mode_declined: true }, null, 2) + '\n');
354
+ }
355
+ catch (err) {
356
+ console.log(' ' +
357
+ styleText('dim', `Could not record the answer (${err.message}) — the offer will repeat.`));
358
+ }
359
+ }
360
+ /**
361
+ * What adopting the gate would newly cost, measured with the gate itself.
362
+ *
363
+ * Counted by running `checkMode` twice and taking the difference rather than
364
+ * by re-implementing its classification here. The hand-written version drifted
365
+ * immediately: it skipped every document inside a mode directory before
366
+ * reading what that document declared, so two files with byte-identical
367
+ * frontmatter were counted or not according to where they sat, and it reported
368
+ * one offender against two errors. Advertising a document as settled and then
369
+ * failing it is the failure worth avoiding, and only the checker knows.
370
+ */
371
+ async function adoptionCost(latDir) {
372
+ const projectRoot = dirname(latDir);
373
+ const key = (error) => `${error.file}:${error.line}:${error.message}`;
374
+ const [gated, ungated] = await Promise.all([
375
+ checkMode(latDir, projectRoot, { requireMode: true }),
376
+ checkMode(latDir, projectRoot, { requireMode: false }),
377
+ ]);
378
+ const already = new Set(ungated.map(key));
379
+ return [
380
+ ...new Set(gated.filter((e) => !already.has(key(e))).map((e) => e.target)),
381
+ ];
382
+ }
383
+ /** Say what is wrong with a root index the gate cannot be written into. */
384
+ function reportUnstampable(latDir, plan) {
385
+ const where = `${basename(latDir)}/${indexNameFor(basename(latDir))}`;
386
+ console.log('');
387
+ console.log(styleText('bold', 'Diátaxis modes'));
388
+ console.log(' ' +
389
+ styleText('dim', `require-mode cannot be set in ${where}: `) +
390
+ ('detail' in plan ? plan.detail : ''));
391
+ if (plan.kind === 'unparseable') {
392
+ // Worth more than the offer that prompted it. Every `lat:` field on a
393
+ // document whose frontmatter does not parse is silently ignored, so this
394
+ // root index is already not being read.
395
+ console.log(' ' +
396
+ styleText('dim', 'Until that parses, no `lat:` field on this document is read at all.'));
221
397
  }
222
- return offenders;
223
398
  }
224
399
  /**
225
400
  * Offer the gate to a tree `lat init` did not create.
@@ -229,20 +404,39 @@ async function unmodedDocuments(latDir) {
229
404
  * for every project that needs it. Restructuring someone's tree without asking
230
405
  * is still wrong, so this asks.
231
406
  *
232
- * Silent when the flag is already set, when a previous run was told no, and
233
- * when there is no TTY to ask — the non-interactive path prints the count and
234
- * the manual edit instead, and records nothing, so a later interactive run
235
- * still offers.
407
+ * Whether the flag *can* be written is decided before anything is asked and
408
+ * before anything is written. Asking first cost a tree four directories and a
409
+ * rewritten index in exchange for a gate that never landed, and the user was
410
+ * told in the same breath that nothing could be edited.
411
+ *
412
+ * Silent when the flag is already set either way, when a previous run was told
413
+ * no, and when there is no TTY to ask — the non-interactive path prints the
414
+ * count and the manual edit instead, and records nothing, so a later
415
+ * interactive run still offers.
236
416
  */
237
417
  export async function offerRequireMode(latDir, interactive, ask) {
238
418
  const rootIndex = join(latDir, indexNameFor(basename(latDir)));
239
419
  if (!existsSync(rootIndex))
240
420
  return;
241
- if (requireModeSet(readFileSync(rootIndex, 'utf-8')))
421
+ let current;
422
+ try {
423
+ current = readFileSync(rootIndex, 'utf-8');
424
+ }
425
+ catch {
426
+ return;
427
+ }
428
+ const plan = planRequireMode(current);
429
+ if (plan.kind === 'on' || plan.kind === 'off')
430
+ return;
431
+ if (plan.kind !== 'stamped') {
432
+ // Not recorded and not silenced: this is a defect in the document with a
433
+ // fix the user can apply, and it stops being printed the moment they do.
434
+ reportUnstampable(latDir, plan);
242
435
  return;
436
+ }
243
437
  if (declined(latDir))
244
438
  return;
245
- const offenders = await unmodedDocuments(latDir);
439
+ const offenders = await adoptionCost(latDir);
246
440
  console.log('');
247
441
  console.log(styleText('bold', 'Diátaxis modes'));
248
442
  console.log(' ' +
@@ -274,23 +468,19 @@ export async function offerRequireMode(latDir, interactive, ask) {
274
468
  }
275
469
  console.log('');
276
470
  if (!(await ask(' Turn require-mode on for this project?'))) {
277
- recordDecline(latDir);
471
+ recordDeclined(latDir);
278
472
  console.log(' ' +
279
473
  styleText('dim', 'Skipped. Add') +
280
474
  ' require-mode: true ' +
281
475
  styleText('dim', 'to the root index whenever you want it.'));
282
476
  return;
283
477
  }
284
- writeForkScaffold(latDir);
285
- // The one frontmatter shape `stampRequireMode` refuses to edit. Saying so
286
- // beats re-offering forever on a flag that never lands.
287
- if (!requireModeSet(readFileSync(rootIndex, 'utf-8'))) {
478
+ writeFileSync(rootIndex, plan.content);
479
+ if (plan.movedRootKey) {
288
480
  console.log(' ' +
289
- styleText('yellow', 'Could not edit the frontmatter safely.') +
290
- styleText('dim', ' Add this under its `lat:` mapping by hand:'));
291
- console.log(' ' + styleText('cyan', 'require-mode: true'));
292
- return;
481
+ styleText('dim', 'Moved the stray root-level require-mode under `lat:`, where it is read.'));
293
482
  }
483
+ writeForkScaffold(latDir);
294
484
  if (offenders.length > 0) {
295
485
  console.log(' ' +
296
486
  styleText('dim', 'Run') +
@@ -5,11 +5,27 @@ import { loadAllSections, flattenSections } from '../lattice.js';
5
5
  function hashContent(text) {
6
6
  return createHash('sha256').update(text).digest('hex');
7
7
  }
8
- async function sectionContent(section, projectRoot) {
9
- const filePath = join(projectRoot, section.filePath);
10
- const content = await readFile(filePath, 'utf-8');
11
- const lines = content.split('\n');
12
- return lines.slice(section.startLine - 1, section.endLine).join('\n');
8
+ /**
9
+ * Read each lat.md file once, keyed by project-relative path. Section text is
10
+ * sliced from these lines by range: reading the file per section instead is
11
+ * O(sections × file size) — a 3.5 MB file holding 12k sections was re-read 12k
12
+ * times (~42 GB of string work) on every search.
13
+ */
14
+ async function loadFileLines(sections, projectRoot) {
15
+ const lines = new Map();
16
+ for (const { filePath } of sections) {
17
+ if (lines.has(filePath))
18
+ continue;
19
+ const content = await readFile(join(projectRoot, filePath), 'utf-8');
20
+ lines.set(filePath, content.split('\n'));
21
+ }
22
+ return lines;
23
+ }
24
+ function sectionContent(section, lines) {
25
+ return lines
26
+ .get(section.filePath)
27
+ .slice(section.startLine - 1, section.endLine)
28
+ .join('\n');
13
29
  }
14
30
  export async function indexSections(latDir, db, embedder, onProgress) {
15
31
  const projectRoot = dirname(latDir);
@@ -17,8 +33,9 @@ export async function indexSections(latDir, db, embedder, onProgress) {
17
33
  const flat = flattenSections(allSections);
18
34
  // Build current state: id -> { section, content, hash }
19
35
  const current = new Map();
36
+ const lines = await loadFileLines(flat, projectRoot);
20
37
  for (const s of flat) {
21
- const text = await sectionContent(s, projectRoot);
38
+ const text = sectionContent(s, lines);
22
39
  current.set(s.id, { section: s, content: text, hash: hashContent(text) });
23
40
  }
24
41
  // Get existing hashes from DB
@@ -0,0 +1 @@
1
+ :root{color:#171717;background:#fff;font-family:Geist,Geist Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-synthesis:none;text-rendering:optimizeLegibility;color-scheme:light dark;--background: #fff;--panel: #fafafa;--sidebar: #fafafa;--sidebar-selection: #eaeaea;--panel-border: #eaeaea;--text: #171717;--muted: #666;--accent: #171717;--accent-soft: #ededed;--link: #0070f3;--danger: #e5484d;--danger-soft: #ffebe9;--danger-border: #f3b8b8;--warning: #b7791f;--warning-soft: #fff4ce;--success: #2e7d46;--success-soft: #e9f9ee;--git-notification: #e47f19;--code: #f5f5f5;--context-surface: #f7f7f7;--inline-code: #ededed;--reference-control: #e5e5e5;--graph-document: #0070f3;--graph-code: #d97706;--syntax-built-in: #9a3f75;--syntax-comment: #737373;--syntax-keyword: #6c4bc2;--syntax-number: #a85414;--syntax-string: #397348;--syntax-title: #26649a}@media(prefers-color-scheme:dark){:root{color:#ededed;background:#000;--background: #000;--panel: #0a0a0a;--sidebar: #000;--sidebar-selection: #242424;--panel-border: #262626;--text: #ededed;--muted: #888;--accent: #fff;--accent-soft: #1f1f1f;--link: rgb(82, 168, 255);--danger: #ff6166;--danger-soft: #2a1314;--danger-border: #5c2628;--warning: #f5a623;--warning-soft: #2a210f;--success: #62c073;--success-soft: #12261a;--git-notification: #f0a044;--code: #111;--context-surface: #111;--inline-code: #1f1f1f;--reference-control: #262626;--graph-document: rgb(82, 168, 255);--graph-code: #f5a623;--syntax-built-in: #e6a2cc;--syntax-comment: #888;--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);-webkit-font-smoothing:antialiased}::selection{color:var(--background);background:var(--text)}a{color:var(--link)}.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(--sidebar);border-right:0}.sidebar-header{display:flex;margin:0 2px 34px 10px;align-items:center;justify-content:space-between}.mobile-navigation-trigger{display:none}.brand{display:inline-flex;min-width:0;align-items:center;overflow:hidden;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;text-overflow:ellipsis;white-space:nowrap}.brand-logo{display:block;width:auto;height:21px;flex:none}.brand span{color:inherit}.sidebar-actions{display:flex;flex:none;align-items:center;gap:3px}.sidebar-search,.sidebar-graph,.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-graph:hover,.sidebar-graph[aria-current=page],.sidebar-git:hover{color:var(--accent);background:var(--sidebar-selection)}.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(--sidebar)}.sidebar-search svg,.sidebar-graph 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:var(--sidebar-selection)}.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(--sidebar-selection)}.document-link.active{font-weight:650}.main{--document-gutter: clamp(28px, 3vw, 48px);--document-toc-gutter: calc(var(--document-gutter) + 16px);width:100%;max-width:1280px;min-width:0;padding:28px var(--document-gutter) 100px}.main.restoring-history-scroll{visibility:hidden}.document-header{max-width:760px;margin-bottom:34px}.document-layout{display:grid;grid-template-columns:minmax(0,760px) 286px;grid-template-areas:"document toc";gap:var(--document-toc-gutter);align-items:start}.document-column{grid-area:document;min-width:0}.document-toc{grid-area:toc;position:sticky;top:28px;height:calc(100vh - 56px);box-sizing:border-box;padding-right:16px;overflow:hidden}.document-toc-title{display:flex;width:100%;min-height:24px;padding:0;align-items:center;gap:9px;color:var(--muted);font-family:inherit;font-size:13px;font-weight:650;text-align:left;background:transparent;border:0}.document-toc-toggle{display:none}.document-toc-title svg{width:17px;height:17px;flex:0 0 auto;fill:none;stroke:currentColor;stroke-linecap:round;stroke-width:2}.document-toc-list{display:block;max-height:calc(100% - 38px);margin-top:14px;overflow-y:auto;overscroll-behavior:contain;border-left:1px solid var(--panel-border);scrollbar-width:none}.document-toc-list::-webkit-scrollbar{display:none}.document-toc-link{position:relative;display:flex;padding:7px 0 7px calc(14px + var(--toc-depth) * 13px);align-items:center;gap:7px;color:var(--muted);font-size:12.5px;font-weight:520;line-height:17px;text-decoration:none;transition:color .14s ease;overflow-wrap:anywhere}.document-toc-link-label{min-width:0;flex:1}.document-toc-states{display:inline-flex;padding-right:2px;flex:0 0 auto;align-items:center;gap:4px}.document-toc-state{width:6px;height:6px;border-radius:50%;box-shadow:0 0 0 2px color-mix(in srgb,currentColor 12%,transparent)}.document-toc-state.git{color:var(--git-notification);background:currentColor}.document-toc-state.error{color:var(--danger);background:currentColor}.document-toc-link:before{position:absolute;top:4px;bottom:4px;left:-1px;width:2px;content:"";background:transparent;border-radius:2px}.document-toc-link:hover{color:var(--text)}.document-toc-link[data-depth="1"]{font-weight:650}.document-toc-link[aria-current=location]{color:var(--link)}.document-toc-link[aria-current=location]:before{background:var(--link)}.document-metadata{display:flex;min-height:28px;align-items:center;gap:10px;flex-wrap:wrap}.document-path{min-width:0;overflow-wrap:anywhere;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:Geist,Geist Sans,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:Geist,Geist Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:var(--context-surface);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(--reference-control);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(--link);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}.document-metadata+.source-code{margin-top:14px}.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(--reference-control);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:Geist,Geist Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:var(--context-surface);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(--link);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{font-size:26px}.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{overflow-wrap:anywhere;text-decoration-line:underline;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 ul.git-added,.markdown ol.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 pre:has(>code.git-added){color:var(--success);background:color-mix(in srgb,var(--success-soft) 55%,var(--code));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 ul.git-removed,.markdown ol.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)}.markdown pre:has(>code.git-removed){color:var(--danger);text-decoration:line-through;background:color-mix(in srgb,var(--danger-soft) 55%,var(--code));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,a.wiki-link-code:not(.wiki-link-segmented) .code-link-leading{text-decoration-line:underline;text-decoration-color:currentColor;text-decoration-thickness:1px;text-underline-offset:3px}.wiki-link-ref-count{display:inline-flex;min-width:1.45em;height:1.45em;padding:0 .34em;margin-left:.4em;align-items:center;justify-content:center;color:var(--muted);font-size:.66em;font-style:normal;font-weight:750;line-height:1;text-decoration:none;vertical-align:.15em;background:color-mix(in srgb,var(--muted) 14%,transparent);border-radius:999px}.markdown a:hover .wiki-link-ref-count{color:var(--text)}.external-link-icon{display:inline-block;width:.78em;height:.78em;margin-left:.28em;vertical-align:.02em;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat}.code-link-language{display:inline-flex;width:1.55em;height:1.55em;margin-right:.38em;align-items:center;justify-content:center;color:var(--background);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(--text);border-radius:3px}.code-link-leading{display:inline-flex;align-items:baseline;white-space:nowrap}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(--inline-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:6px;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:6px;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(--link);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)}.graph-shell{position:relative;height:100vh;overflow:hidden;background:var(--panel)}.graph-topbar{position:absolute;z-index:8;inset:0 auto auto 0;width:50%;height:84px;pointer-events:none}.graph-topbar-graph{display:flex;min-width:0;padding:0 28px;align-items:center;gap:14px;height:100%;pointer-events:auto}.graph-topbar-graph .graph-header{display:flex;min-width:0;flex:0 0 212px;align-items:center;justify-content:space-between}.graph-topbar-graph .graph-tools{min-width:0}.graph-workspace{display:grid;height:100vh;min-height:0;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.graph-pane,.graph-inspector{position:relative;min-width:0;min-height:0}.graph-pane{overflow:hidden;border-right:1px solid var(--panel-border)}.graph-canvas-shell,.graph-canvas{position:absolute;inset:0}.graph-canvas-shell{background:radial-gradient(circle at center,color-mix(in srgb,var(--accent-soft) 22%,transparent),transparent 58%),var(--panel)}.graph-tools{display:flex;min-width:0;flex:1 1 auto;align-items:center;gap:8px}.graph-filter{display:flex;min-width:100px;height:36px;flex:1 1 220px;align-items:center;background:color-mix(in srgb,var(--background) 72%,transparent);border:1px solid var(--panel-border);border-radius:8px}.graph-filter:focus-within{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)}.graph-filter input{width:auto;min-width:0;flex:1 1 auto;padding:0 12px;color:var(--text);font:inherit;font-size:13px;background:transparent;border:0;outline:0}.graph-filter input::placeholder{color:var(--muted)}.graph-kind-filters{display:flex;flex:0 0 auto;align-items:center;gap:5px}.graph-kind-filters label{position:relative;display:inline-flex;cursor:pointer}.graph-kind-filters input{position:absolute;opacity:0;pointer-events:none}.graph-kind-filters label span{padding:4px 7px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:9px;font-weight:700;text-transform:capitalize;background:color-mix(in srgb,var(--background) 82%,transparent);border:1px solid var(--panel-border);border-radius:999px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.graph-kind-filters input:checked+span{color:var(--accent);background:var(--accent-soft);border-color:color-mix(in srgb,var(--accent) 30%,var(--panel-border))}.graph-kind-filters input:focus-visible+span{outline:2px solid var(--accent);outline-offset:2px}.graph-node-count{flex:0 0 auto;padding-right:10px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:9px;font-weight:650;white-space:nowrap}.graph-fit{position:absolute;z-index:4;right:18px;bottom:18px;padding:5px 8px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:9px;font-weight:700;cursor:pointer;background:color-mix(in srgb,var(--background) 84%,transparent);border:1px solid var(--panel-border);border-radius:6px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.graph-fit:hover{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 35%,var(--panel-border))}.graph-legend{position:absolute;z-index:4;bottom:20px;left:18px;display:flex;gap:12px;color:var(--muted);font-size:9px;font-weight:650}.graph-legend span{display:inline-flex;align-items:center;gap:5px}.graph-legend span:before{width:6px;height:6px;content:"";background:currentColor;border-radius:50%}.graph-legend .document{color:var(--graph-document)}.graph-legend .code{color:var(--graph-code)}.graph-legend .document input:checked+span{color:var(--graph-document)}.graph-legend .code input:checked+span{color:var(--graph-code)}.graph-legend input:not(:checked)+span{opacity:.42}.graph-no-results{position:absolute;z-index:3;inset:0;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:12px;pointer-events:none}.graph-inspector{background:var(--panel)}.graph-inspector-scroll{height:100%;overflow-y:auto;overscroll-behavior:contain}.graph-inspector-empty{display:flex;height:100%;padding:40px;align-items:center;justify-content:center;color:var(--muted);font-size:12px;line-height:1.5;text-align:center}.graph-inspector-document,.graph-inspector-source{padding:28px 28px 80px}.graph-inspector-document .markdown{max-width:none}.graph-inspector-document>.document-metadata{margin-bottom:28px}.graph-inspector-source .source-context{width:calc(100% - 5.25rem)}.graph-state{height:calc(100vh - 84px);min-height:0}@media(prefers-reduced-motion:reduce){.document-toc-link,.document-toc-chevron{transition:none}}@media(max-width:1100px)and (min-width:64rem){.graph-topbar-graph{padding:0 18px 0 28px;gap:8px}.graph-node-count{display:none}}@media(max-width:1340px){.document-layout{display:flex;flex-direction:column;gap:0;align-items:stretch}.document-toc{position:static;width:100%;max-width:760px;height:auto;padding:0;margin-bottom:28px;overflow:visible}.document-toc-heading{display:none}.document-toc-toggle{display:flex;min-height:52px;padding:0 16px;cursor:pointer;background:var(--panel);border:1px solid var(--panel-border);border-radius:7px}.document-toc-toggle:hover,.document-toc-toggle[aria-expanded=true]{color:var(--text)}.document-toc-chevron{margin-left:auto;color:var(--muted);font-size:18px;line-height:1;transform:rotate(90deg);transition:transform .12s ease}.document-toc[data-expanded=true] .document-toc-chevron{transform:rotate(-90deg)}.document-toc-list{display:none;max-height:min(52vh,420px);padding:8px 0;margin-top:0;background:var(--panel);border:1px solid var(--panel-border);border-top:0;border-radius:0 0 7px 7px}.document-toc[data-expanded=true] .document-toc-toggle{border-radius:7px 7px 0 0}.document-toc[data-expanded=true] .document-toc-list{display:block}}@media(min-width:64rem)and (max-width:1340px){.sidebar-header{min-height:42px}.document-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,286px);grid-template-areas:"metadata toc" "document document";column-gap:var(--document-toc-gutter);row-gap:34px;align-items:start}.document-column{display:contents}.document-header{grid-area:metadata;min-width:0;margin:0}.document-metadata,.document-toc-toggle{min-height:42px}.document-toc{grid-area:toc;position:relative;z-index:1;top:0;max-width:286px;margin:0}.document-toc-list{position:absolute;top:100%;right:0;left:0}.document-toc-states{padding-right:14px}.markdown{grid-area:document;min-width:0}}@media not (min-width:64rem){body.mobile-navigation-open{overflow:hidden}.app-shell{display:block}.sidebar{position:sticky;z-index:20;top:0;width:100%;height:104px;padding:0;overflow:visible;border-right:0;border-bottom:1px solid var(--panel-border)}.sidebar-header{height:48px;padding:0 16px;margin:0}.sidebar-header .brand{font-size:18px}.sidebar-header .brand-logo{height:18px}.sidebar-actions{gap:5px}.sidebar-search,.sidebar-graph,.sidebar-git{width:36px;height:36px}.mobile-navigation-trigger{display:flex;width:100%;height:56px;padding:0 18px;align-items:center;gap:13px;overflow:hidden;color:var(--muted);font:inherit;font-size:13px;text-align:left;cursor:pointer;background:transparent;border:0;border-top:1px solid var(--panel-border)}.mobile-navigation-trigger:hover,.mobile-navigation-trigger[aria-expanded=true]{color:var(--text)}.mobile-navigation-trigger svg{width:18px;height:18px;flex:0 0 auto;fill:none;stroke:currentColor;stroke-linecap:round;stroke-width:1.8}.mobile-navigation-context{display:flex;min-width:0;align-items:center;gap:9px}.mobile-navigation-context>span:first-child{color:var(--text);font-weight:600}.mobile-navigation-context>span[aria-hidden=true]{color:var(--muted)}.mobile-navigation-current{min-width:0;overflow:hidden;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.sidebar nav{display:none}.sidebar[data-mobile-navigation-open=true] nav{position:fixed;z-index:19;inset:104px 0 0;display:block;padding:16px 16px max(28px,env(safe-area-inset-bottom));overflow-y:auto;overscroll-behavior:contain;background:var(--sidebar);outline:0}.sidebar[data-mobile-navigation-open=true] .file-tree{max-width:560px;margin:0 auto}.sidebar[data-mobile-navigation-open=true] .tree-directory>summary,.sidebar[data-mobile-navigation-open=true] .document-link{min-height:44px;padding-top:10px;padding-bottom:10px}.main{--mobile-gutter: clamp(20px, 5vw, 48px);padding:28px clamp(20px,5vw,48px) 80px}.document-toc{position:sticky;z-index:10;top:104px;width:calc(100% + var(--mobile-gutter) * 2);max-width:none;margin:-28px calc(var(--mobile-gutter) * -1) 28px;background:var(--background)}.document-toc-toggle{min-height:56px;padding:0 var(--mobile-gutter);background:color-mix(in srgb,var(--background) 94%,transparent);border-top:0;border-right:0;border-left:0;border-radius:0;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}.document-toc[data-expanded=true] .document-toc-toggle{border-radius:0}.document-toc-list{max-height:calc(100vh - 160px);max-height:calc(100dvh - 160px);padding-right:var(--mobile-gutter);padding-left:var(--mobile-gutter);background:var(--background);border-right:0;border-left:0;border-radius:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown .markdown-error{scroll-margin-top:180px}.source-context{width:calc(100% - 24px);margin-right:12px;margin-left:12px}.graph-shell{height:auto;min-height:100vh;min-height:100dvh;overflow:visible}.graph-topbar{position:sticky;top:0;width:100%;height:auto;background:color-mix(in srgb,var(--panel) 92%,transparent);border-bottom:1px solid var(--panel-border);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);pointer-events:auto}.graph-topbar-graph{min-height:64px;padding:10px 16px;border-right:0}.graph-topbar-graph .graph-header{flex-basis:190px}.graph-header .brand{font-size:18px}.graph-header .brand-logo{height:18px}.graph-header .sidebar-search,.graph-header .sidebar-graph,.graph-header .sidebar-git{width:36px;height:36px}.graph-workspace{display:block;height:auto}.graph-pane{height:min(58vh,520px);height:min(58svh,520px);min-height:320px;border-right:0;border-bottom:1px solid var(--panel-border)}.graph-canvas{touch-action:none}.graph-filter input{font-size:16px}.graph-kind-filters label span{min-height:32px;padding:7px 10px}.graph-fit{min-width:44px;min-height:32px}.graph-inspector{min-height:55vh;min-height:55svh}.graph-inspector-scroll{height:auto;overflow:visible}.graph-inspector-empty{min-height:45vh}.graph-inspector-document,.graph-inspector-source{padding-right:20px;padding-left:20px}.graph-inspector-source .source-context{width:calc(100% - 24px)}.graph-state{height:auto;min-height:50vh}}@media(max-width:600px){.main{--mobile-gutter: 18px;padding-right:18px;padding-left:18px}.markdown{font-size:15.5px}.markdown h1{font-size:32px}.markdown h2{font-size:24px}.graph-topbar-graph{flex-wrap:wrap}.graph-topbar-graph .graph-header,.graph-topbar-graph .graph-tools,.graph-filter{flex-basis:100%}.graph-pane{height:52vh;height:52svh;min-height:300px}}
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="color-scheme" content="light dark" />
7
7
  <title>lat.md</title>
8
- <script type="module" crossorigin src="/assets/index-VZwPPiFE.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-W8ktvNj9.css">
8
+ <script type="module" crossorigin src="/assets/index-KWi67IMz.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-C3iaZyYT.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plebeiantech/lat.md",
3
- "version": "0.12.3-fork.7",
3
+ "version": "0.12.3-fork.9",
4
4
  "description": "A knowledge graph for your codebase, written in markdown",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,11 +27,11 @@
27
27
  "templates"
28
28
  ],
29
29
  "devDependencies": {
30
+ "@types/mdast": "^4.0.4",
31
+ "@types/node": "^25.3.0",
30
32
  "@types/react": "^19.0.0",
31
33
  "@types/react-dom": "^19.0.0",
32
34
  "@vitejs/plugin-react": "^5.0.0",
33
- "@types/mdast": "^4.0.4",
34
- "@types/node": "^25.3.0",
35
35
  "graphology": "^0.26.0",
36
36
  "highlight.js": "^11.12.0",
37
37
  "mdast-util-from-markdown": "^2.0.3",
@@ -54,13 +54,13 @@
54
54
  "github-slugger": "^2.0.0",
55
55
  "ignore-walk": "^8.0.0",
56
56
  "mdast-util-to-markdown": "^2.1.0",
57
+ "rehype-sanitize": "^6.0.0",
58
+ "rehype-slug": "^6.0.0",
59
+ "rehype-stringify": "^10.0.1",
57
60
  "remark-frontmatter": "^5.0.0",
58
61
  "remark-parse": "^11.0.0",
59
62
  "remark-rehype": "^11.1.2",
60
63
  "remark-stringify": "^11.0.0",
61
- "rehype-sanitize": "^6.0.0",
62
- "rehype-slug": "^6.0.0",
63
- "rehype-stringify": "^10.0.1",
64
64
  "unified": "^11.0.0",
65
65
  "unist-util-visit": "^5.0.0",
66
66
  "web-tree-sitter": "^0.26.6",
@@ -1 +0,0 @@
1
- :root{color:#171717;background:#fff;font-family:Geist,Geist Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-synthesis:none;text-rendering:optimizeLegibility;color-scheme:light dark;--background: #fff;--panel: #fafafa;--sidebar: #fafafa;--sidebar-selection: #eaeaea;--panel-border: #eaeaea;--text: #171717;--muted: #666;--accent: #171717;--accent-soft: #ededed;--link: #0070f3;--danger: #e5484d;--danger-soft: #ffebe9;--danger-border: #f3b8b8;--warning: #b7791f;--warning-soft: #fff4ce;--success: #2e7d46;--success-soft: #e9f9ee;--git-notification: #e47f19;--code: #f5f5f5;--context-surface: #f7f7f7;--inline-code: #ededed;--reference-control: #e5e5e5;--graph-document: #0070f3;--graph-code: #d97706;--syntax-built-in: #9a3f75;--syntax-comment: #737373;--syntax-keyword: #6c4bc2;--syntax-number: #a85414;--syntax-string: #397348;--syntax-title: #26649a}@media(prefers-color-scheme:dark){:root{color:#ededed;background:#000;--background: #000;--panel: #0a0a0a;--sidebar: #000;--sidebar-selection: #242424;--panel-border: #262626;--text: #ededed;--muted: #888;--accent: #fff;--accent-soft: #1f1f1f;--link: rgb(82, 168, 255);--danger: #ff6166;--danger-soft: #2a1314;--danger-border: #5c2628;--warning: #f5a623;--warning-soft: #2a210f;--success: #62c073;--success-soft: #12261a;--git-notification: #f0a044;--code: #111;--context-surface: #111;--inline-code: #1f1f1f;--reference-control: #262626;--graph-document: rgb(82, 168, 255);--graph-code: #f5a623;--syntax-built-in: #e6a2cc;--syntax-comment: #888;--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);-webkit-font-smoothing:antialiased}::selection{color:var(--background);background:var(--text)}a{color:var(--link)}.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(--sidebar);border-right:0}.sidebar-header{display:flex;margin:0 2px 34px 10px;align-items:center;justify-content:space-between}.mobile-navigation-trigger{display:none}.brand{display:inline-flex;min-width:0;align-items:center;overflow:hidden;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;text-overflow:ellipsis;white-space:nowrap}.brand-logo{display:block;width:auto;height:21px;flex:none}.brand span{color:inherit}.sidebar-actions{display:flex;flex:none;align-items:center;gap:3px}.sidebar-search,.sidebar-graph,.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-graph:hover,.sidebar-graph[aria-current=page],.sidebar-git:hover{color:var(--accent);background:var(--sidebar-selection)}.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(--sidebar)}.sidebar-search svg,.sidebar-graph 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:var(--sidebar-selection)}.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(--sidebar-selection)}.document-link.active{font-weight:650}.main{--document-gutter: clamp(28px, 3vw, 48px);--document-toc-gutter: calc(var(--document-gutter) + 16px);width:100%;max-width:1280px;min-width:0;padding:28px var(--document-gutter) 100px}.main.restoring-history-scroll{visibility:hidden}.document-header{max-width:760px;margin-bottom:34px}.document-layout{display:grid;grid-template-columns:minmax(0,760px) 286px;grid-template-areas:"document toc";gap:var(--document-toc-gutter);align-items:start}.document-column{grid-area:document;min-width:0}.document-toc{grid-area:toc;position:sticky;top:28px;height:calc(100vh - 56px);box-sizing:border-box;padding-right:16px;overflow:hidden}.document-toc-title{display:flex;width:100%;min-height:24px;padding:0;align-items:center;gap:9px;color:var(--muted);font-family:inherit;font-size:13px;font-weight:650;text-align:left;background:transparent;border:0}.document-toc-toggle{display:none}.document-toc-title svg{width:17px;height:17px;flex:0 0 auto;fill:none;stroke:currentColor;stroke-linecap:round;stroke-width:2}.document-toc-list{display:block;max-height:calc(100% - 38px);margin-top:14px;overflow-y:auto;overscroll-behavior:contain;border-left:1px solid var(--panel-border);scrollbar-width:none}.document-toc-list::-webkit-scrollbar{display:none}.document-toc-link{position:relative;display:flex;padding:7px 0 7px calc(14px + var(--toc-depth) * 13px);align-items:center;gap:7px;color:var(--muted);font-size:12.5px;font-weight:520;line-height:17px;text-decoration:none;transition:color .14s ease;overflow-wrap:anywhere}.document-toc-link-label{min-width:0;flex:1}.document-toc-states{display:inline-flex;padding-right:2px;flex:0 0 auto;align-items:center;gap:4px}.document-toc-state{width:6px;height:6px;border-radius:50%;box-shadow:0 0 0 2px color-mix(in srgb,currentColor 12%,transparent)}.document-toc-state.git{color:var(--git-notification);background:currentColor}.document-toc-state.error{color:var(--danger);background:currentColor}.document-toc-link:before{position:absolute;top:4px;bottom:4px;left:-1px;width:2px;content:"";background:transparent;border-radius:2px}.document-toc-link:hover{color:var(--text)}.document-toc-link[data-depth="1"]{font-weight:650}.document-toc-link[aria-current=location]{color:var(--link)}.document-toc-link[aria-current=location]:before{background:var(--link)}.document-metadata{display:flex;min-height:28px;align-items:center;gap:10px;flex-wrap:wrap}.document-path{min-width:0;overflow-wrap:anywhere;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:Geist,Geist Sans,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:Geist,Geist Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:var(--context-surface);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(--reference-control);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(--link);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}.document-metadata+.source-code{margin-top:14px}.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(--reference-control);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:Geist,Geist Sans,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:var(--context-surface);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(--link);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{font-size:26px}.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{overflow-wrap:anywhere;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 ul.git-added,.markdown ol.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 pre:has(>code.git-added){color:var(--success);background:color-mix(in srgb,var(--success-soft) 55%,var(--code));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 ul.git-removed,.markdown ol.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)}.markdown pre:has(>code.git-removed){color:var(--danger);text-decoration:line-through;background:color-mix(in srgb,var(--danger-soft) 55%,var(--code));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}.wiki-link-ref-count{display:inline-flex;min-width:1.45em;height:1.45em;padding:0 .34em;margin-left:.4em;align-items:center;justify-content:center;color:var(--muted);font-size:.66em;font-style:normal;font-weight:750;line-height:1;text-decoration:none;vertical-align:.15em;background:color-mix(in srgb,var(--muted) 14%,transparent);border-radius:999px}.markdown a:hover .wiki-link-ref-count{color:var(--text)}.external-link-icon{display:inline-block;width:.78em;height:.78em;margin-left:.28em;vertical-align:.02em;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat}.code-link-language{display:inline-flex;width:1.55em;height:1.55em;margin-right:.38em;align-items:center;justify-content:center;color:var(--background);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(--text);border-radius:3px}.code-link-leading{display:inline-flex;align-items:baseline;white-space:nowrap}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(--inline-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:6px;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:6px;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(--link);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)}.graph-shell{position:relative;height:100vh;overflow:hidden;background:var(--panel)}.graph-topbar{position:absolute;z-index:8;inset:0 auto auto 0;width:50%;height:84px;pointer-events:none}.graph-topbar-graph{display:flex;min-width:0;padding:0 28px;align-items:center;gap:14px;height:100%;pointer-events:auto}.graph-topbar-graph .graph-header{display:flex;min-width:0;flex:0 0 212px;align-items:center;justify-content:space-between}.graph-topbar-graph .graph-tools{min-width:0}.graph-workspace{display:grid;height:100vh;min-height:0;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.graph-pane,.graph-inspector{position:relative;min-width:0;min-height:0}.graph-pane{overflow:hidden;border-right:1px solid var(--panel-border)}.graph-canvas-shell,.graph-canvas{position:absolute;inset:0}.graph-canvas-shell{background:radial-gradient(circle at center,color-mix(in srgb,var(--accent-soft) 22%,transparent),transparent 58%),var(--panel)}.graph-tools{display:flex;min-width:0;flex:1 1 auto;align-items:center;gap:8px}.graph-filter{display:flex;min-width:100px;height:36px;flex:1 1 220px;align-items:center;background:color-mix(in srgb,var(--background) 72%,transparent);border:1px solid var(--panel-border);border-radius:8px}.graph-filter:focus-within{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)}.graph-filter input{width:auto;min-width:0;flex:1 1 auto;padding:0 12px;color:var(--text);font:inherit;font-size:13px;background:transparent;border:0;outline:0}.graph-filter input::placeholder{color:var(--muted)}.graph-kind-filters{display:flex;flex:0 0 auto;align-items:center;gap:5px}.graph-kind-filters label{position:relative;display:inline-flex;cursor:pointer}.graph-kind-filters input{position:absolute;opacity:0;pointer-events:none}.graph-kind-filters label span{padding:4px 7px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:9px;font-weight:700;text-transform:capitalize;background:color-mix(in srgb,var(--background) 82%,transparent);border:1px solid var(--panel-border);border-radius:999px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.graph-kind-filters input:checked+span{color:var(--accent);background:var(--accent-soft);border-color:color-mix(in srgb,var(--accent) 30%,var(--panel-border))}.graph-kind-filters input:focus-visible+span{outline:2px solid var(--accent);outline-offset:2px}.graph-node-count{flex:0 0 auto;padding-right:10px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:9px;font-weight:650;white-space:nowrap}.graph-fit{position:absolute;z-index:4;right:18px;bottom:18px;padding:5px 8px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:9px;font-weight:700;cursor:pointer;background:color-mix(in srgb,var(--background) 84%,transparent);border:1px solid var(--panel-border);border-radius:6px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.graph-fit:hover{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 35%,var(--panel-border))}.graph-legend{position:absolute;z-index:4;bottom:20px;left:18px;display:flex;gap:12px;color:var(--muted);font-size:9px;font-weight:650}.graph-legend span{display:inline-flex;align-items:center;gap:5px}.graph-legend span:before{width:6px;height:6px;content:"";background:currentColor;border-radius:50%}.graph-legend .document{color:var(--graph-document)}.graph-legend .code{color:var(--graph-code)}.graph-legend .document input:checked+span{color:var(--graph-document)}.graph-legend .code input:checked+span{color:var(--graph-code)}.graph-legend input:not(:checked)+span{opacity:.42}.graph-no-results{position:absolute;z-index:3;inset:0;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:12px;pointer-events:none}.graph-inspector{background:var(--panel)}.graph-inspector-scroll{height:100%;overflow-y:auto;overscroll-behavior:contain}.graph-inspector-empty{display:flex;height:100%;padding:40px;align-items:center;justify-content:center;color:var(--muted);font-size:12px;line-height:1.5;text-align:center}.graph-inspector-document,.graph-inspector-source{padding:28px 28px 80px}.graph-inspector-document .markdown{max-width:none}.graph-inspector-document>.document-metadata{margin-bottom:28px}.graph-inspector-source .source-context{width:calc(100% - 5.25rem)}.graph-state{height:calc(100vh - 84px);min-height:0}@media(prefers-reduced-motion:reduce){.document-toc-link,.document-toc-chevron{transition:none}}@media(max-width:1100px)and (min-width:64rem){.graph-topbar-graph{padding:0 18px 0 28px;gap:8px}.graph-node-count{display:none}}@media(max-width:1340px){.document-layout{display:flex;flex-direction:column;gap:0}.document-toc{position:static;width:100%;max-width:760px;height:auto;padding:0;margin-bottom:28px;overflow:visible}.document-toc-heading{display:none}.document-toc-toggle{display:flex;min-height:52px;padding:0 16px;cursor:pointer;background:var(--panel);border:1px solid var(--panel-border);border-radius:7px}.document-toc-toggle:hover,.document-toc-toggle[aria-expanded=true]{color:var(--text)}.document-toc-chevron{margin-left:auto;color:var(--muted);font-size:18px;line-height:1;transform:rotate(90deg);transition:transform .12s ease}.document-toc[data-expanded=true] .document-toc-chevron{transform:rotate(-90deg)}.document-toc-list{display:none;max-height:min(52vh,420px);padding:8px 0;margin-top:0;background:var(--panel);border:1px solid var(--panel-border);border-top:0;border-radius:0 0 7px 7px}.document-toc[data-expanded=true] .document-toc-toggle{border-radius:7px 7px 0 0}.document-toc[data-expanded=true] .document-toc-list{display:block}}@media not (min-width:64rem){body.mobile-navigation-open{overflow:hidden}.app-shell{display:block}.sidebar{position:sticky;z-index:20;top:0;width:100%;height:104px;padding:0;overflow:visible;border-right:0;border-bottom:1px solid var(--panel-border)}.sidebar-header{height:48px;padding:0 16px;margin:0}.sidebar-header .brand{font-size:18px}.sidebar-header .brand-logo{height:18px}.sidebar-actions{gap:5px}.sidebar-search,.sidebar-graph,.sidebar-git{width:36px;height:36px}.mobile-navigation-trigger{display:flex;width:100%;height:56px;padding:0 18px;align-items:center;gap:13px;overflow:hidden;color:var(--muted);font:inherit;font-size:13px;text-align:left;cursor:pointer;background:transparent;border:0;border-top:1px solid var(--panel-border)}.mobile-navigation-trigger:hover,.mobile-navigation-trigger[aria-expanded=true]{color:var(--text)}.mobile-navigation-trigger svg{width:18px;height:18px;flex:0 0 auto;fill:none;stroke:currentColor;stroke-linecap:round;stroke-width:1.8}.mobile-navigation-context{display:flex;min-width:0;align-items:center;gap:9px}.mobile-navigation-context>span:first-child{color:var(--text);font-weight:600}.mobile-navigation-context>span[aria-hidden=true]{color:var(--muted)}.mobile-navigation-current{min-width:0;overflow:hidden;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.sidebar nav{display:none}.sidebar[data-mobile-navigation-open=true] nav{position:fixed;z-index:19;inset:104px 0 0;display:block;padding:16px 16px max(28px,env(safe-area-inset-bottom));overflow-y:auto;overscroll-behavior:contain;background:var(--sidebar);outline:0}.sidebar[data-mobile-navigation-open=true] .file-tree{max-width:560px;margin:0 auto}.sidebar[data-mobile-navigation-open=true] .tree-directory>summary,.sidebar[data-mobile-navigation-open=true] .document-link{min-height:44px;padding-top:10px;padding-bottom:10px}.main{--mobile-gutter: clamp(20px, 5vw, 48px);padding:28px clamp(20px,5vw,48px) 80px}.document-toc{position:sticky;z-index:10;top:104px;width:calc(100% + var(--mobile-gutter) * 2);max-width:none;margin:-28px calc(var(--mobile-gutter) * -1) 28px;background:var(--background)}.document-toc-toggle{min-height:56px;padding:0 var(--mobile-gutter);background:color-mix(in srgb,var(--background) 94%,transparent);border-right:0;border-left:0;border-radius:0;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}.document-toc[data-expanded=true] .document-toc-toggle{border-radius:0}.document-toc-list{max-height:calc(100vh - 160px);max-height:calc(100dvh - 160px);padding-right:var(--mobile-gutter);padding-left:var(--mobile-gutter);background:var(--background);border-right:0;border-left:0;border-radius:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown .markdown-error{scroll-margin-top:180px}.source-context{width:calc(100% - 24px);margin-right:12px;margin-left:12px}.graph-shell{height:auto;min-height:100vh;min-height:100dvh;overflow:visible}.graph-topbar{position:sticky;top:0;width:100%;height:auto;background:color-mix(in srgb,var(--panel) 92%,transparent);border-bottom:1px solid var(--panel-border);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);pointer-events:auto}.graph-topbar-graph{min-height:64px;padding:10px 16px;border-right:0}.graph-topbar-graph .graph-header{flex-basis:190px}.graph-header .brand{font-size:18px}.graph-header .brand-logo{height:18px}.graph-header .sidebar-search,.graph-header .sidebar-graph,.graph-header .sidebar-git{width:36px;height:36px}.graph-workspace{display:block;height:auto}.graph-pane{height:min(58vh,520px);height:min(58svh,520px);min-height:320px;border-right:0;border-bottom:1px solid var(--panel-border)}.graph-canvas{touch-action:none}.graph-filter input{font-size:16px}.graph-kind-filters label span{min-height:32px;padding:7px 10px}.graph-fit{min-width:44px;min-height:32px}.graph-inspector{min-height:55vh;min-height:55svh}.graph-inspector-scroll{height:auto;overflow:visible}.graph-inspector-empty{min-height:45vh}.graph-inspector-document,.graph-inspector-source{padding-right:20px;padding-left:20px}.graph-inspector-source .source-context{width:calc(100% - 24px)}.graph-state{height:auto;min-height:50vh}}@media(max-width:600px){.main{--mobile-gutter: 18px;padding-right:18px;padding-left:18px}.markdown{font-size:15.5px}.markdown h1{font-size:32px}.markdown h2{font-size:24px}.graph-topbar-graph{flex-wrap:wrap}.graph-topbar-graph .graph-header,.graph-topbar-graph .graph-tools,.graph-filter{flex-basis:100%}.graph-pane{height:52vh;height:52svh;min-height:300px}}