@plebeiantech/lat.md 0.12.3-fork.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/dist/src/cli/check-frontmatter.d.ts +12 -0
- package/dist/src/cli/check-frontmatter.js +44 -0
- package/dist/src/cli/check-mode.d.ts +6 -0
- package/dist/src/cli/check-mode.js +303 -0
- package/dist/src/cli/check-status.d.ts +74 -0
- package/dist/src/cli/check-status.js +217 -0
- package/dist/src/cli/check.d.ts +45 -0
- package/dist/src/cli/check.js +777 -0
- package/dist/src/cli/checklist-menu.d.ts +12 -0
- package/dist/src/cli/checklist-menu.js +111 -0
- package/dist/src/cli/comment-guard.d.ts +16 -0
- package/dist/src/cli/comment-guard.js +111 -0
- package/dist/src/cli/comment-reminder.d.ts +62 -0
- package/dist/src/cli/comment-reminder.js +210 -0
- package/dist/src/cli/context.d.ts +11 -0
- package/dist/src/cli/context.js +58 -0
- package/dist/src/cli/expand.d.ts +8 -0
- package/dist/src/cli/expand.js +94 -0
- package/dist/src/cli/gen-index.d.ts +45 -0
- package/dist/src/cli/gen-index.js +235 -0
- package/dist/src/cli/gen.d.ts +6 -0
- package/dist/src/cli/gen.js +42 -0
- package/dist/src/cli/graph.d.ts +7 -0
- package/dist/src/cli/graph.js +47 -0
- package/dist/src/cli/hook.d.ts +1 -0
- package/dist/src/cli/hook.js +432 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +307 -0
- package/dist/src/cli/init.d.ts +9 -0
- package/dist/src/cli/init.js +1031 -0
- package/dist/src/cli/link-scheme.d.ts +54 -0
- package/dist/src/cli/link-scheme.js +101 -0
- package/dist/src/cli/locate.d.ts +2 -0
- package/dist/src/cli/locate.js +17 -0
- package/dist/src/cli/refs.d.ts +22 -0
- package/dist/src/cli/refs.js +229 -0
- package/dist/src/cli/reindex.d.ts +13 -0
- package/dist/src/cli/reindex.js +136 -0
- package/dist/src/cli/search.d.ts +36 -0
- package/dist/src/cli/search.js +217 -0
- package/dist/src/cli/section.d.ts +43 -0
- package/dist/src/cli/section.js +252 -0
- package/dist/src/cli/select-menu.d.ts +13 -0
- package/dist/src/cli/select-menu.js +100 -0
- package/dist/src/cli/templates.d.ts +1 -0
- package/dist/src/cli/templates.js +15 -0
- package/dist/src/cli/ui.d.ts +11 -0
- package/dist/src/cli/ui.js +34 -0
- package/dist/src/code-refs.d.ts +17 -0
- package/dist/src/code-refs.js +277 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +83 -0
- package/dist/src/context.d.ts +23 -0
- package/dist/src/context.js +11 -0
- package/dist/src/extensions/wiki-link/from-markdown.d.ts +5 -0
- package/dist/src/extensions/wiki-link/from-markdown.js +34 -0
- package/dist/src/extensions/wiki-link/index.d.ts +4 -0
- package/dist/src/extensions/wiki-link/index.js +3 -0
- package/dist/src/extensions/wiki-link/syntax.d.ts +14 -0
- package/dist/src/extensions/wiki-link/syntax.js +121 -0
- package/dist/src/extensions/wiki-link/to-markdown.d.ts +5 -0
- package/dist/src/extensions/wiki-link/to-markdown.js +28 -0
- package/dist/src/extensions/wiki-link/types.d.ts +34 -0
- package/dist/src/extensions/wiki-link/types.js +1 -0
- package/dist/src/format.d.ts +8 -0
- package/dist/src/format.js +43 -0
- package/dist/src/graph-export.d.ts +60 -0
- package/dist/src/graph-export.js +309 -0
- package/dist/src/init-version.d.ts +10 -0
- package/dist/src/init-version.js +49 -0
- package/dist/src/knowledge/bd.d.ts +2 -0
- package/dist/src/knowledge/bd.js +107 -0
- package/dist/src/knowledge/claude-memory.d.ts +13 -0
- package/dist/src/knowledge/claude-memory.js +204 -0
- package/dist/src/knowledge/cq.d.ts +2 -0
- package/dist/src/knowledge/cq.js +74 -0
- package/dist/src/knowledge/index.d.ts +49 -0
- package/dist/src/knowledge/index.js +271 -0
- package/dist/src/knowledge/ranking.d.ts +17 -0
- package/dist/src/knowledge/ranking.js +62 -0
- package/dist/src/knowledge/session.d.ts +46 -0
- package/dist/src/knowledge/session.js +203 -0
- package/dist/src/knowledge/types.d.ts +47 -0
- package/dist/src/knowledge/types.js +10 -0
- package/dist/src/lattice.d.ts +108 -0
- package/dist/src/lattice.js +816 -0
- package/dist/src/mcp/server.d.ts +1 -0
- package/dist/src/mcp/server.js +58 -0
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +18 -0
- package/dist/src/search/db.d.ts +16 -0
- package/dist/src/search/db.js +54 -0
- package/dist/src/search/embedder.d.ts +25 -0
- package/dist/src/search/embedder.js +58 -0
- package/dist/src/search/index.d.ts +9 -0
- package/dist/src/search/index.js +66 -0
- package/dist/src/search/search.d.ts +9 -0
- package/dist/src/search/search.js +16 -0
- package/dist/src/source-parser.d.ts +27 -0
- package/dist/src/source-parser.js +916 -0
- package/dist/src/untrusted.d.ts +24 -0
- package/dist/src/untrusted.js +97 -0
- package/dist/src/version.d.ts +7 -0
- package/dist/src/version.js +39 -0
- package/dist/src/view/client/assets/index-B0n0u03y.css +1 -0
- package/dist/src/view/client/assets/index-xm2_px4q.js +23 -0
- package/dist/src/view/client/index.html +14 -0
- package/dist/src/view/diagnostics.d.ts +5 -0
- package/dist/src/view/diagnostics.js +171 -0
- package/dist/src/view/git-diff.d.ts +3 -0
- package/dist/src/view/git-diff.js +255 -0
- package/dist/src/view/git.d.ts +22 -0
- package/dist/src/view/git.js +193 -0
- package/dist/src/view/highlight.d.ts +2 -0
- package/dist/src/view/highlight.js +3767 -0
- package/dist/src/view/markdown.d.ts +22 -0
- package/dist/src/view/markdown.js +322 -0
- package/dist/src/view/protocol.d.ts +89 -0
- package/dist/src/view/protocol.js +1 -0
- package/dist/src/view/references.d.ts +62 -0
- package/dist/src/view/references.js +345 -0
- package/dist/src/view/repository.d.ts +12 -0
- package/dist/src/view/repository.js +167 -0
- package/dist/src/view/search.d.ts +14 -0
- package/dist/src/view/search.js +64 -0
- package/dist/src/view/server.d.ts +20 -0
- package/dist/src/view/server.js +233 -0
- package/dist/src/view/store.d.ts +57 -0
- package/dist/src/view/store.js +404 -0
- package/dist/src/walk.d.ts +18 -0
- package/dist/src/walk.js +27 -0
- package/package.json +85 -0
- package/templates/AGENTS.md +118 -0
- package/templates/README +1 -0
- package/templates/cursor-rules.md +77 -0
- package/templates/init/lat.md +1 -0
- package/templates/logo-dark.svg +182 -0
- package/templates/logo-light.svg +182 -0
- package/templates/logo.txt +7 -0
- package/templates/opencode-plugin.ts +180 -0
- package/templates/pi-extension.ts +364 -0
- package/templates/skill/SKILL.md +189 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<title>lat.md</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-xm2_px4q.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B0n0u03y.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Section } from '../lattice.js';
|
|
2
|
+
import type { ViewDocumentError } from './protocol.js';
|
|
3
|
+
import type { ViewCodeReferenceFile, ViewParsedMarkdownFile } from './references.js';
|
|
4
|
+
/** Build per-document validation errors entirely from the current view snapshot. */
|
|
5
|
+
export declare function buildViewDiagnostics(markdownFiles: Iterable<ViewParsedMarkdownFile>, codeFiles: Iterable<ViewCodeReferenceFile>, allSections: Section[], projectRoot: string): Promise<ReadonlyMap<string, readonly ViewDocumentError[]>>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { extname, relative, resolve, dirname } from 'node:path';
|
|
3
|
+
import { ambiguousRefMessage, sourceRefError } from '../cli/check.js';
|
|
4
|
+
import { buildFileIndex, buildSectionSlugIndex, flattenSections, parseFrontmatter, resolveRef, } from '../lattice.js';
|
|
5
|
+
import { clearSymbolCache } from '../source-parser.js';
|
|
6
|
+
import { toPosix } from '../walk.js';
|
|
7
|
+
const MAX_BODY_LENGTH = 250;
|
|
8
|
+
function decodeLinkPart(value) {
|
|
9
|
+
try {
|
|
10
|
+
return decodeURIComponent(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function localLinkTarget(url) {
|
|
17
|
+
const value = url.trim();
|
|
18
|
+
if (value.startsWith('/'))
|
|
19
|
+
return null;
|
|
20
|
+
const windowsDrivePath = /^[a-zA-Z]:\\/.test(value);
|
|
21
|
+
if (!windowsDrivePath && /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const queryAt = value.indexOf('?');
|
|
25
|
+
const fragmentAt = value.indexOf('#');
|
|
26
|
+
const pathEnd = Math.min(queryAt === -1 ? value.length : queryAt, fragmentAt === -1 ? value.length : fragmentAt);
|
|
27
|
+
const rawPath = value.slice(0, pathEnd);
|
|
28
|
+
const path = rawPath === '' ? null : decodeLinkPart(rawPath);
|
|
29
|
+
const fragment = fragmentAt === -1 ? null : decodeLinkPart(value.slice(fragmentAt + 1));
|
|
30
|
+
if (rawPath === '' && fragment === null)
|
|
31
|
+
return null;
|
|
32
|
+
if (path?.includes('\\'))
|
|
33
|
+
return { kind: 'invalid-backslash' };
|
|
34
|
+
return { kind: 'target', path, fragment };
|
|
35
|
+
}
|
|
36
|
+
function bodyTextLength(body) {
|
|
37
|
+
return body.replace(/\[\[[^\]]*\]\]/g, '').length;
|
|
38
|
+
}
|
|
39
|
+
function error(line, target, message, options = {}) {
|
|
40
|
+
return {
|
|
41
|
+
anchor: options.anchor || `user-content-markdown-error-${line}`,
|
|
42
|
+
line,
|
|
43
|
+
marker: options.marker ?? 'target',
|
|
44
|
+
message,
|
|
45
|
+
target,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function addError(errors, path, diagnostic) {
|
|
49
|
+
const fileErrors = errors.get(path) ?? [];
|
|
50
|
+
const duplicate = fileErrors.some((candidate) => candidate.line === diagnostic.line &&
|
|
51
|
+
candidate.message === diagnostic.message);
|
|
52
|
+
if (!duplicate) {
|
|
53
|
+
let unique = diagnostic;
|
|
54
|
+
if (diagnostic.marker === 'target' &&
|
|
55
|
+
fileErrors.some((candidate) => candidate.anchor === diagnostic.anchor)) {
|
|
56
|
+
let suffix = 2;
|
|
57
|
+
while (fileErrors.some((candidate) => candidate.anchor === `${diagnostic.anchor}-${suffix}`)) {
|
|
58
|
+
suffix++;
|
|
59
|
+
}
|
|
60
|
+
unique = { ...diagnostic, anchor: `${diagnostic.anchor}-${suffix}` };
|
|
61
|
+
}
|
|
62
|
+
fileErrors.push(unique);
|
|
63
|
+
}
|
|
64
|
+
errors.set(path, fileErrors);
|
|
65
|
+
}
|
|
66
|
+
async function markdownLinkError(file, filesByAbsolutePath, link, projectRoot) {
|
|
67
|
+
const target = localLinkTarget(link.url);
|
|
68
|
+
if (target === null)
|
|
69
|
+
return null;
|
|
70
|
+
if (target.kind === 'invalid-backslash') {
|
|
71
|
+
return error(link.line, link.url, `invalid ${link.kind} (${link.url}) — backslashes are not path separators in Markdown; use "/" instead`);
|
|
72
|
+
}
|
|
73
|
+
const absolutePath = target.path
|
|
74
|
+
? resolve(dirname(file.absolutePath), target.path)
|
|
75
|
+
: file.absolutePath;
|
|
76
|
+
if (!existsSync(absolutePath)) {
|
|
77
|
+
return error(link.line, link.url, `broken ${link.kind} (${link.url}) — file "${toPosix(relative(projectRoot, absolutePath))}" not found`);
|
|
78
|
+
}
|
|
79
|
+
if (!target.fragment ||
|
|
80
|
+
extname(absolutePath).toLowerCase() !== '.md' ||
|
|
81
|
+
link.kind === 'image') {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const destination = filesByAbsolutePath.get(resolve(absolutePath));
|
|
85
|
+
if (!destination)
|
|
86
|
+
return null;
|
|
87
|
+
const headings = new Set(flattenSections(destination.sections).map((section) => section.githubSlug));
|
|
88
|
+
if (headings.has(target.fragment))
|
|
89
|
+
return null;
|
|
90
|
+
return error(link.line, link.url, `broken link (${link.url}) — heading "#${target.fragment}" not found in "${destination.path}"`);
|
|
91
|
+
}
|
|
92
|
+
/** Build per-document validation errors entirely from the current view snapshot. */
|
|
93
|
+
export async function buildViewDiagnostics(markdownFiles, codeFiles, allSections, projectRoot) {
|
|
94
|
+
clearSymbolCache();
|
|
95
|
+
const files = [...markdownFiles];
|
|
96
|
+
const errors = new Map();
|
|
97
|
+
const sections = flattenSections(allSections);
|
|
98
|
+
const sectionIds = new Set(sections.map((section) => section.id.toLowerCase()));
|
|
99
|
+
const fileIndex = buildFileIndex(allSections);
|
|
100
|
+
const slugIndex = buildSectionSlugIndex(allSections);
|
|
101
|
+
const filesByAbsolutePath = new Map(files.map((file) => [resolve(file.absolutePath), file]));
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
for (const ref of file.wikiRefs) {
|
|
104
|
+
const resolved = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
|
|
105
|
+
let message = null;
|
|
106
|
+
if (resolved.ambiguous) {
|
|
107
|
+
message = ambiguousRefMessage(ref.target, resolved.ambiguous, resolved.suggested);
|
|
108
|
+
}
|
|
109
|
+
else if (!sectionIds.has(resolved.resolved.toLowerCase())) {
|
|
110
|
+
message = await sourceRefError(ref.target, projectRoot);
|
|
111
|
+
}
|
|
112
|
+
if (message)
|
|
113
|
+
addError(errors, file.path, error(ref.line, ref.target, message));
|
|
114
|
+
}
|
|
115
|
+
const links = new Map();
|
|
116
|
+
for (const link of file.validationLinks) {
|
|
117
|
+
if ('identifier' in link) {
|
|
118
|
+
addError(errors, file.path, error(link.line, link.identifier, `undefined ${link.kind === 'imageReference' ? 'image' : 'link'} reference (${link.source}) — definition "[${link.identifier}]" not found`, { marker: 'line' }));
|
|
119
|
+
}
|
|
120
|
+
else if (link.kind !== 'definition') {
|
|
121
|
+
links.set(`${link.kind}:${link.line}:${link.url}`, {
|
|
122
|
+
kind: link.kind,
|
|
123
|
+
line: link.line,
|
|
124
|
+
url: link.url,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const link of file.markdownLinks) {
|
|
129
|
+
links.set(`${link.kind}:${link.line}:${link.url}`, link);
|
|
130
|
+
}
|
|
131
|
+
for (const link of links.values()) {
|
|
132
|
+
const diagnostic = await markdownLinkError(file, filesByAbsolutePath, link, projectRoot);
|
|
133
|
+
if (diagnostic)
|
|
134
|
+
addError(errors, file.path, diagnostic);
|
|
135
|
+
}
|
|
136
|
+
for (const section of flattenSections(file.sections)) {
|
|
137
|
+
if (!section.firstParagraph) {
|
|
138
|
+
addError(errors, file.path, error(section.startLine, section.id, `section "${section.id}" has no leading paragraph. Every section must start with a brief overview (≤${MAX_BODY_LENGTH} chars).`, { anchor: section.githubSlug, marker: 'heading' }));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const length = bodyTextLength(section.firstParagraph);
|
|
142
|
+
if (length > MAX_BODY_LENGTH) {
|
|
143
|
+
addError(errors, file.path, error(section.startLine, section.id, `section "${section.id}" leading paragraph is ${length} characters (max ${MAX_BODY_LENGTH}, excluding [[wiki links]]).`, { anchor: section.githubSlug, marker: 'heading' }));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const mentionedSections = new Set();
|
|
148
|
+
for (const file of codeFiles) {
|
|
149
|
+
for (const ref of file.refs) {
|
|
150
|
+
const resolved = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
|
|
151
|
+
if (!resolved.ambiguous &&
|
|
152
|
+
sectionIds.has(resolved.resolved.toLowerCase())) {
|
|
153
|
+
mentionedSections.add(resolved.resolved.toLowerCase());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
if (!parseFrontmatter(file.content).requireCodeMention)
|
|
159
|
+
continue;
|
|
160
|
+
for (const section of flattenSections(file.sections)) {
|
|
161
|
+
if (section.children.length === 0 &&
|
|
162
|
+
!mentionedSections.has(section.id.toLowerCase())) {
|
|
163
|
+
addError(errors, file.path, error(section.startLine, section.id, `section "${section.id}" requires a code mention but none found`, { anchor: section.githubSlug, marker: 'heading' }));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return new Map([...errors].map(([path, diagnostics]) => [
|
|
168
|
+
path,
|
|
169
|
+
diagnostics.sort((left, right) => left.line - right.line || left.message.localeCompare(right.message)),
|
|
170
|
+
]));
|
|
171
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { parse } from '../parser.js';
|
|
2
|
+
const MAX_DIFF_CELLS = 1_000_000;
|
|
3
|
+
const WORDS = /\s+|[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]+/gu;
|
|
4
|
+
function sequenceDiff(oldValues, newValues, key) {
|
|
5
|
+
const oldKeys = oldValues.map(key);
|
|
6
|
+
const newKeys = newValues.map(key);
|
|
7
|
+
if (oldKeys.length * newKeys.length > MAX_DIFF_CELLS) {
|
|
8
|
+
let prefix = 0;
|
|
9
|
+
while (prefix < oldKeys.length &&
|
|
10
|
+
prefix < newKeys.length &&
|
|
11
|
+
oldKeys[prefix] === newKeys[prefix]) {
|
|
12
|
+
prefix++;
|
|
13
|
+
}
|
|
14
|
+
let suffix = 0;
|
|
15
|
+
while (suffix < oldKeys.length - prefix &&
|
|
16
|
+
suffix < newKeys.length - prefix &&
|
|
17
|
+
oldKeys[oldKeys.length - suffix - 1] ===
|
|
18
|
+
newKeys[newKeys.length - suffix - 1]) {
|
|
19
|
+
suffix++;
|
|
20
|
+
}
|
|
21
|
+
return [
|
|
22
|
+
...oldValues.slice(0, prefix).map((oldValue, index) => ({
|
|
23
|
+
kind: 'same',
|
|
24
|
+
oldValue,
|
|
25
|
+
newValue: newValues[index],
|
|
26
|
+
})),
|
|
27
|
+
...oldValues
|
|
28
|
+
.slice(prefix, oldValues.length - suffix)
|
|
29
|
+
.map((value) => ({ kind: 'removed', value })),
|
|
30
|
+
...newValues
|
|
31
|
+
.slice(prefix, newValues.length - suffix)
|
|
32
|
+
.map((value) => ({ kind: 'added', value })),
|
|
33
|
+
...oldValues.slice(oldValues.length - suffix).map((oldValue, index) => ({
|
|
34
|
+
kind: 'same',
|
|
35
|
+
oldValue,
|
|
36
|
+
newValue: newValues[newValues.length - suffix + index],
|
|
37
|
+
})),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
const rows = Array.from({ length: oldKeys.length + 1 }, () => new Uint32Array(newKeys.length + 1));
|
|
41
|
+
for (let oldIndex = oldKeys.length - 1; oldIndex >= 0; oldIndex--) {
|
|
42
|
+
for (let newIndex = newKeys.length - 1; newIndex >= 0; newIndex--) {
|
|
43
|
+
rows[oldIndex][newIndex] =
|
|
44
|
+
oldKeys[oldIndex] === newKeys[newIndex]
|
|
45
|
+
? rows[oldIndex + 1][newIndex + 1] + 1
|
|
46
|
+
: Math.max(rows[oldIndex + 1][newIndex], rows[oldIndex][newIndex + 1]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const changes = [];
|
|
50
|
+
let oldIndex = 0;
|
|
51
|
+
let newIndex = 0;
|
|
52
|
+
while (oldIndex < oldValues.length || newIndex < newValues.length) {
|
|
53
|
+
if (oldIndex < oldValues.length &&
|
|
54
|
+
newIndex < newValues.length &&
|
|
55
|
+
oldKeys[oldIndex] === newKeys[newIndex]) {
|
|
56
|
+
changes.push({
|
|
57
|
+
kind: 'same',
|
|
58
|
+
oldValue: oldValues[oldIndex++],
|
|
59
|
+
newValue: newValues[newIndex++],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else if (newIndex < newValues.length &&
|
|
63
|
+
(oldIndex === oldValues.length ||
|
|
64
|
+
rows[oldIndex][newIndex + 1] > rows[oldIndex + 1][newIndex])) {
|
|
65
|
+
changes.push({ kind: 'added', value: newValues[newIndex++] });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
changes.push({ kind: 'removed', value: oldValues[oldIndex++] });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return changes;
|
|
72
|
+
}
|
|
73
|
+
function withChildren(node) {
|
|
74
|
+
return 'children' in node && Array.isArray(node.children);
|
|
75
|
+
}
|
|
76
|
+
function wikiText(node) {
|
|
77
|
+
return node.data.alias ?? node.value;
|
|
78
|
+
}
|
|
79
|
+
function nodeText(node) {
|
|
80
|
+
if (node.type === 'wikiLink')
|
|
81
|
+
return wikiText(node);
|
|
82
|
+
if ('value' in node && typeof node.value === 'string')
|
|
83
|
+
return node.value;
|
|
84
|
+
return withChildren(node) ? node.children.map(nodeText).join('') : '';
|
|
85
|
+
}
|
|
86
|
+
function wrapperSignature(node) {
|
|
87
|
+
switch (node.type) {
|
|
88
|
+
case 'link':
|
|
89
|
+
return `link:${node.url}:${node.title ?? ''}`;
|
|
90
|
+
case 'linkReference':
|
|
91
|
+
return `link-reference:${node.identifier}:${node.referenceType}`;
|
|
92
|
+
default:
|
|
93
|
+
return node.type;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function inlineUnits(node, prefix = '') {
|
|
97
|
+
if (node.type === 'text') {
|
|
98
|
+
return [...node.value.matchAll(WORDS)].map((match) => ({
|
|
99
|
+
key: `${prefix}text:${match[0]}`,
|
|
100
|
+
node: { ...structuredClone(node), value: match[0] },
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
if (node.type === 'wikiLink') {
|
|
104
|
+
const wiki = node;
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
key: `${prefix}wiki:${wiki.value}:${wiki.data.alias ?? ''}`,
|
|
108
|
+
node: structuredClone(node),
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
if (withChildren(node)) {
|
|
113
|
+
const nextPrefix = `${prefix}${wrapperSignature(node)}>`;
|
|
114
|
+
return node.children.flatMap((child) => inlineUnits(child, nextPrefix).map((unit) => ({
|
|
115
|
+
key: unit.key,
|
|
116
|
+
node: {
|
|
117
|
+
...structuredClone(node),
|
|
118
|
+
children: [unit.node],
|
|
119
|
+
},
|
|
120
|
+
})));
|
|
121
|
+
}
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
key: `${prefix}${wrapperSignature(node)}:${nodeText(node)}`,
|
|
125
|
+
node: structuredClone(node),
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
function wrapInline(node, kind) {
|
|
130
|
+
return {
|
|
131
|
+
type: 'emphasis',
|
|
132
|
+
data: {
|
|
133
|
+
hName: kind === 'added' ? 'ins' : 'del',
|
|
134
|
+
hProperties: { className: [`git-${kind}`] },
|
|
135
|
+
},
|
|
136
|
+
children: [node],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function diffInline(oldChildren, newChildren) {
|
|
140
|
+
const oldUnits = oldChildren.flatMap((child) => inlineUnits(child));
|
|
141
|
+
const newUnits = newChildren.flatMap((child) => inlineUnits(child));
|
|
142
|
+
return sequenceDiff(oldUnits, newUnits, (unit) => unit.key).map((change) => {
|
|
143
|
+
switch (change.kind) {
|
|
144
|
+
case 'same':
|
|
145
|
+
return change.newValue.node;
|
|
146
|
+
case 'added':
|
|
147
|
+
return wrapInline(change.value.node, 'added');
|
|
148
|
+
case 'removed':
|
|
149
|
+
return wrapInline(change.value.node, 'removed');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function sourceForNode(markdown, node) {
|
|
154
|
+
const start = node.position?.start.offset;
|
|
155
|
+
const end = node.position?.end.offset;
|
|
156
|
+
return start === undefined || end === undefined
|
|
157
|
+
? `${node.type}:${nodeText(node)}`
|
|
158
|
+
: `${node.type}:${markdown.slice(start, end)}`;
|
|
159
|
+
}
|
|
160
|
+
function addClass(node, kind) {
|
|
161
|
+
const result = structuredClone(node);
|
|
162
|
+
const properties = result.data?.hProperties ?? {};
|
|
163
|
+
const current = properties.className;
|
|
164
|
+
const classes = Array.isArray(current)
|
|
165
|
+
? current.map(String)
|
|
166
|
+
: current
|
|
167
|
+
? [String(current)]
|
|
168
|
+
: [];
|
|
169
|
+
classes.push(`git-${kind}`);
|
|
170
|
+
result.data = {
|
|
171
|
+
...result.data,
|
|
172
|
+
hName: kind === 'removed' && result.type === 'heading'
|
|
173
|
+
? 'div'
|
|
174
|
+
: result.data?.hName,
|
|
175
|
+
hProperties: { ...properties, className: classes },
|
|
176
|
+
};
|
|
177
|
+
if (kind === 'removed')
|
|
178
|
+
stripPositions(result);
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
function stripPositions(node) {
|
|
182
|
+
delete node.position;
|
|
183
|
+
if (withChildren(node))
|
|
184
|
+
node.children.forEach(stripPositions);
|
|
185
|
+
}
|
|
186
|
+
function pairedNode(oldNode, newNode, oldMarkdown, newMarkdown) {
|
|
187
|
+
if (oldNode.type !== newNode.type)
|
|
188
|
+
return null;
|
|
189
|
+
if ((newNode.type === 'heading' || newNode.type === 'paragraph') &&
|
|
190
|
+
withChildren(oldNode) &&
|
|
191
|
+
withChildren(newNode)) {
|
|
192
|
+
return {
|
|
193
|
+
...structuredClone(newNode),
|
|
194
|
+
children: diffInline(oldNode.children, newNode.children),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if ((newNode.type === 'blockquote' ||
|
|
198
|
+
newNode.type === 'list' ||
|
|
199
|
+
newNode.type === 'listItem') &&
|
|
200
|
+
withChildren(oldNode) &&
|
|
201
|
+
withChildren(newNode)) {
|
|
202
|
+
return {
|
|
203
|
+
...structuredClone(newNode),
|
|
204
|
+
children: diffBlocks(oldNode.children, newNode.children, oldMarkdown, newMarkdown),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
function diffBlocks(oldNodes, newNodes, oldMarkdown, newMarkdown) {
|
|
210
|
+
const changes = sequenceDiff(oldNodes, newNodes, (node) => oldNodes.includes(node)
|
|
211
|
+
? sourceForNode(oldMarkdown, node)
|
|
212
|
+
: sourceForNode(newMarkdown, node));
|
|
213
|
+
const output = [];
|
|
214
|
+
for (let index = 0; index < changes.length;) {
|
|
215
|
+
const change = changes[index];
|
|
216
|
+
if (change.kind === 'same') {
|
|
217
|
+
output.push(structuredClone(change.newValue));
|
|
218
|
+
index++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const removed = [];
|
|
222
|
+
const added = [];
|
|
223
|
+
while (index < changes.length && changes[index].kind !== 'same') {
|
|
224
|
+
const pending = changes[index++];
|
|
225
|
+
if (pending.kind === 'removed')
|
|
226
|
+
removed.push(pending.value);
|
|
227
|
+
if (pending.kind === 'added')
|
|
228
|
+
added.push(pending.value);
|
|
229
|
+
}
|
|
230
|
+
const paired = Math.min(removed.length, added.length);
|
|
231
|
+
let pairedCount = 0;
|
|
232
|
+
while (pairedCount < paired) {
|
|
233
|
+
const node = pairedNode(removed[pairedCount], added[pairedCount], oldMarkdown, newMarkdown);
|
|
234
|
+
if (!node)
|
|
235
|
+
break;
|
|
236
|
+
output.push(node);
|
|
237
|
+
pairedCount++;
|
|
238
|
+
}
|
|
239
|
+
output.push(...removed.slice(pairedCount).map((node) => addClass(node, 'removed')), ...added.slice(pairedCount).map((node) => addClass(node, 'added')));
|
|
240
|
+
}
|
|
241
|
+
return output;
|
|
242
|
+
}
|
|
243
|
+
/** Build a renderable Markdown tree with HEAD deletions and local additions. */
|
|
244
|
+
export function buildGitDiffTree(baseMarkdown, currentMarkdown, currentTree) {
|
|
245
|
+
const oldTree = parse(baseMarkdown);
|
|
246
|
+
const newTree = currentTree
|
|
247
|
+
? structuredClone(currentTree)
|
|
248
|
+
: parse(currentMarkdown);
|
|
249
|
+
const oldNodes = oldTree.children.filter((node) => node.type !== 'yaml');
|
|
250
|
+
const newNodes = newTree.children.filter((node) => node.type !== 'yaml');
|
|
251
|
+
return {
|
|
252
|
+
type: 'root',
|
|
253
|
+
children: diffBlocks(oldNodes, newNodes, baseMarkdown, currentMarkdown),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ViewGitFileStatus } from './protocol.js';
|
|
2
|
+
export type ViewGitFileSnapshot = {
|
|
3
|
+
status: ViewGitFileStatus;
|
|
4
|
+
baseContent: string;
|
|
5
|
+
};
|
|
6
|
+
export type ViewGitSnapshot = {
|
|
7
|
+
available: boolean;
|
|
8
|
+
files: ReadonlyMap<string, ViewGitFileSnapshot>;
|
|
9
|
+
};
|
|
10
|
+
export type ViewGitRepository = {
|
|
11
|
+
root: string;
|
|
12
|
+
latRelative: string;
|
|
13
|
+
};
|
|
14
|
+
export type ViewGitRunner = (cwd: string, args: string[]) => Promise<string>;
|
|
15
|
+
/** Find the Git worktree containing the active lat.md directory. */
|
|
16
|
+
export declare function findViewGitRepository(projectRoot: string, latDir: string, runner?: ViewGitRunner): Promise<ViewGitRepository | null>;
|
|
17
|
+
/** Read the cached HEAD-to-working-tree state for visible lat.md files. */
|
|
18
|
+
export declare function readViewGitSnapshot(repository: ViewGitRepository, currentFiles: ReadonlyMap<string, {
|
|
19
|
+
content: string;
|
|
20
|
+
}>, runner?: ViewGitRunner): Promise<ViewGitSnapshot>;
|
|
21
|
+
export declare function emptyViewGitSnapshot(): ViewGitSnapshot;
|
|
22
|
+
export declare function sameViewGitSnapshot(left: ViewGitSnapshot, right: ViewGitSnapshot): boolean;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, relative, sep } from 'node:path';
|
|
4
|
+
import { toPosix } from '../walk.js';
|
|
5
|
+
const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
6
|
+
const MAX_GIT_OUTPUT = 50 * 1024 * 1024;
|
|
7
|
+
function runGit(cwd, args) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
execFile('git', args, { cwd, encoding: 'utf8', maxBuffer: MAX_GIT_OUTPUT }, (error, stdout) => (error ? reject(error) : resolve(stdout)));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function inside(root, candidate) {
|
|
13
|
+
const path = relative(root, candidate);
|
|
14
|
+
return (path === '' ||
|
|
15
|
+
(!isAbsolute(path) && path !== '..' && !path.startsWith(`..${sep}`)));
|
|
16
|
+
}
|
|
17
|
+
/** Find the Git worktree containing the active lat.md directory. */
|
|
18
|
+
export async function findViewGitRepository(projectRoot, latDir, runner = runGit) {
|
|
19
|
+
try {
|
|
20
|
+
const output = await runner(projectRoot, [
|
|
21
|
+
'rev-parse',
|
|
22
|
+
'--show-toplevel',
|
|
23
|
+
'--is-inside-work-tree',
|
|
24
|
+
]);
|
|
25
|
+
const lines = output.trim().split('\n');
|
|
26
|
+
const reportedRoot = lines[0]?.trim();
|
|
27
|
+
if (!reportedRoot || lines.at(-1)?.trim() !== 'true') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const [root, realLatDir] = await Promise.all([
|
|
31
|
+
realpath(reportedRoot),
|
|
32
|
+
realpath(latDir),
|
|
33
|
+
]);
|
|
34
|
+
if (!inside(root, realLatDir))
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
root,
|
|
38
|
+
latRelative: toPosix(relative(root, realLatDir)),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function vaultPath(repository, path) {
|
|
46
|
+
const normalized = toPosix(path);
|
|
47
|
+
if (!repository.latRelative)
|
|
48
|
+
return normalized;
|
|
49
|
+
const prefix = `${repository.latRelative}/`;
|
|
50
|
+
return normalized.startsWith(prefix) ? normalized.slice(prefix.length) : null;
|
|
51
|
+
}
|
|
52
|
+
function statusFiles(repository, output) {
|
|
53
|
+
const files = new Map();
|
|
54
|
+
const entries = output.split('\0');
|
|
55
|
+
for (let index = 0; index < entries.length; index++) {
|
|
56
|
+
const entry = entries[index];
|
|
57
|
+
if (!entry || entry.length < 4)
|
|
58
|
+
continue;
|
|
59
|
+
const status = entry.slice(0, 2);
|
|
60
|
+
const path = vaultPath(repository, entry.slice(3));
|
|
61
|
+
if (path) {
|
|
62
|
+
files.set(path, status.includes('?') || status.includes('A') ? 'new' : 'modified');
|
|
63
|
+
}
|
|
64
|
+
if (status.includes('R') || status.includes('C'))
|
|
65
|
+
index++;
|
|
66
|
+
}
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
69
|
+
function patchPath(line) {
|
|
70
|
+
const path = line.slice(4);
|
|
71
|
+
if (path === '/dev/null')
|
|
72
|
+
return null;
|
|
73
|
+
return path.startsWith('b/') ? path.slice(2) : path;
|
|
74
|
+
}
|
|
75
|
+
/** Reconstruct each tracked file's HEAD content from one full-context diff. */
|
|
76
|
+
function baseContentsFromPatch(output) {
|
|
77
|
+
const contents = new Map();
|
|
78
|
+
let path = null;
|
|
79
|
+
let oldLines = [];
|
|
80
|
+
let inHunk = false;
|
|
81
|
+
let sawFile = false;
|
|
82
|
+
let oldEndsWithNewline = true;
|
|
83
|
+
let previousWasOldLine = false;
|
|
84
|
+
const finish = () => {
|
|
85
|
+
if (path && sawFile) {
|
|
86
|
+
contents.set(path, `${oldLines.join('\n')}${oldLines.length > 0 && oldEndsWithNewline ? '\n' : ''}`);
|
|
87
|
+
}
|
|
88
|
+
path = null;
|
|
89
|
+
oldLines = [];
|
|
90
|
+
inHunk = false;
|
|
91
|
+
sawFile = false;
|
|
92
|
+
oldEndsWithNewline = true;
|
|
93
|
+
previousWasOldLine = false;
|
|
94
|
+
};
|
|
95
|
+
for (const line of output.split('\n')) {
|
|
96
|
+
if (line.startsWith('diff --git ')) {
|
|
97
|
+
finish();
|
|
98
|
+
sawFile = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (sawFile && line.startsWith('+++ ')) {
|
|
102
|
+
path = patchPath(line);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (sawFile && line.startsWith('@@ ')) {
|
|
106
|
+
inHunk = true;
|
|
107
|
+
previousWasOldLine = false;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!inHunk)
|
|
111
|
+
continue;
|
|
112
|
+
if (line === '\') {
|
|
113
|
+
if (previousWasOldLine)
|
|
114
|
+
oldEndsWithNewline = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const marker = line[0];
|
|
118
|
+
previousWasOldLine = marker === ' ' || marker === '-';
|
|
119
|
+
if (previousWasOldLine)
|
|
120
|
+
oldLines.push(line.slice(1));
|
|
121
|
+
}
|
|
122
|
+
finish();
|
|
123
|
+
return contents;
|
|
124
|
+
}
|
|
125
|
+
async function hasHead(repository, runner) {
|
|
126
|
+
try {
|
|
127
|
+
await runner(repository.root, ['rev-parse', '--verify', 'HEAD']);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Read the cached HEAD-to-working-tree state for visible lat.md files. */
|
|
135
|
+
export async function readViewGitSnapshot(repository, currentFiles, runner = runGit) {
|
|
136
|
+
const baseline = (await hasHead(repository, runner)) ? 'HEAD' : EMPTY_TREE;
|
|
137
|
+
const pathspec = repository.latRelative || '.';
|
|
138
|
+
const relative = repository.latRelative
|
|
139
|
+
? [`--relative=${repository.latRelative}`]
|
|
140
|
+
: [];
|
|
141
|
+
const [statusOutput, patchOutput] = await Promise.all([
|
|
142
|
+
runner(repository.root, [
|
|
143
|
+
'status',
|
|
144
|
+
'--porcelain=v1',
|
|
145
|
+
'-z',
|
|
146
|
+
'--untracked-files=all',
|
|
147
|
+
'--',
|
|
148
|
+
pathspec,
|
|
149
|
+
]),
|
|
150
|
+
runner(repository.root, [
|
|
151
|
+
'-c',
|
|
152
|
+
'core.quotePath=false',
|
|
153
|
+
'diff',
|
|
154
|
+
'--no-ext-diff',
|
|
155
|
+
'--no-color',
|
|
156
|
+
'--unified=1000000',
|
|
157
|
+
...relative,
|
|
158
|
+
baseline,
|
|
159
|
+
'--',
|
|
160
|
+
pathspec,
|
|
161
|
+
]),
|
|
162
|
+
]);
|
|
163
|
+
const statuses = statusFiles(repository, statusOutput);
|
|
164
|
+
const baseContents = baseContentsFromPatch(patchOutput);
|
|
165
|
+
const files = new Map();
|
|
166
|
+
for (const [path, status] of statuses) {
|
|
167
|
+
const current = currentFiles.get(path);
|
|
168
|
+
if (!current)
|
|
169
|
+
continue;
|
|
170
|
+
files.set(path, {
|
|
171
|
+
status,
|
|
172
|
+
baseContent: status === 'new' ? '' : (baseContents.get(path) ?? current.content),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return { available: true, files };
|
|
176
|
+
}
|
|
177
|
+
export function emptyViewGitSnapshot() {
|
|
178
|
+
return { available: false, files: new Map() };
|
|
179
|
+
}
|
|
180
|
+
export function sameViewGitSnapshot(left, right) {
|
|
181
|
+
if (left.available !== right.available ||
|
|
182
|
+
left.files.size !== right.files.size)
|
|
183
|
+
return false;
|
|
184
|
+
for (const [path, file] of left.files) {
|
|
185
|
+
const candidate = right.files.get(path);
|
|
186
|
+
if (!candidate ||
|
|
187
|
+
candidate.status !== file.status ||
|
|
188
|
+
candidate.baseContent !== file.baseContent) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|