@h1v35/hivex 0.1.0 → 0.2.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/README.md +68 -126
- package/docs/CONTEXT.md +20 -32
- package/docs/README.md +4 -8
- package/docs/adr/0003-independent-bun-installation.md +5 -19
- package/docs/adr/0010-practical-knowledge-assistance.md +28 -81
- package/docs/adr/0011-shared-knowledge-and-selective-history.md +27 -0
- package/docs/engineering.md +46 -142
- package/package.json +31 -10
- package/skills/hivex/SKILL.md +30 -72
- package/skills/hivex/references/markdown.md +16 -31
- package/src/cli/diagnostic.ts +21 -11
- package/src/cli.ts +50 -32
- package/src/documents.ts +522 -315
- package/src/errors.ts +8 -6
- package/src/implementation.ts +185 -87
- package/src/ingestion-units.ts +107 -64
- package/src/knowledge-maintenance.ts +35 -22
- package/src/knowledge-model.ts +386 -267
- package/src/knowledge-serialization.ts +239 -0
- package/src/knowledge-snapshot.ts +163 -0
- package/src/knowledge-store.ts +645 -442
- package/src/knowledge.ts +1073 -696
- package/src/markdown.ts +107 -45
- package/src/model/connection.ts +134 -76
- package/src/model/failure.ts +46 -23
- package/src/model/invoke.ts +346 -166
- package/src/model/profile.ts +201 -103
- package/src/model/rpc-error.ts +21 -0
- package/src/model/server.ts +151 -82
- package/src/model/thread.ts +24 -14
- package/src/model/transcript.ts +87 -46
- package/src/ordering.ts +9 -0
- package/src/retrieval/lexical.ts +64 -41
- package/src/review.ts +83 -55
- package/src/runtime.d.ts +4 -0
- package/src/snapshot-command.ts +105 -0
package/src/documents.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { rawMarkdownLines, lineContent } from './markdown.ts';
|
|
2
1
|
import { lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
-
import
|
|
2
|
+
import pathModule from 'node:path';
|
|
4
3
|
import { parseArgs } from 'node:util';
|
|
5
|
-
import {
|
|
4
|
+
import { compareSerializedStrings } from './ordering.ts';
|
|
6
5
|
import { HivexError } from './errors.ts';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import {
|
|
7
|
+
describeMarkdown,
|
|
8
|
+
hash,
|
|
9
|
+
isMarkdownPath,
|
|
10
|
+
lineContent,
|
|
11
|
+
rawMarkdownLines,
|
|
12
|
+
} from './markdown.ts';
|
|
13
|
+
|
|
14
|
+
export interface Document {
|
|
10
15
|
id: string;
|
|
11
16
|
path: string;
|
|
12
17
|
title: string;
|
|
@@ -14,562 +19,764 @@ export type Document = {
|
|
|
14
19
|
hash: string;
|
|
15
20
|
status: string | null;
|
|
16
21
|
links: string[];
|
|
17
|
-
|
|
22
|
+
historical: boolean;
|
|
23
|
+
}
|
|
18
24
|
|
|
19
|
-
export
|
|
25
|
+
export interface Project {
|
|
20
26
|
root: string;
|
|
21
27
|
snapshot: string;
|
|
28
|
+
currentSnapshot: string;
|
|
22
29
|
documents: Document[];
|
|
30
|
+
currentDocuments: Document[];
|
|
31
|
+
historicalDocuments: Document[];
|
|
23
32
|
warnings: { path: string; message: string }[];
|
|
24
|
-
}
|
|
33
|
+
}
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
interface Config {
|
|
36
|
+
include: string[];
|
|
37
|
+
exclude: string[];
|
|
38
|
+
history: string[];
|
|
39
|
+
}
|
|
40
|
+
interface Candidate {
|
|
41
|
+
absolutePath: string;
|
|
42
|
+
path: string;
|
|
43
|
+
}
|
|
44
|
+
interface ParsedDocument {
|
|
45
|
+
document: Document;
|
|
46
|
+
rawLinks: string[];
|
|
47
|
+
}
|
|
48
|
+
interface CommandOptions {
|
|
30
49
|
root: string;
|
|
31
50
|
maxBytes: number;
|
|
32
|
-
from
|
|
33
|
-
to
|
|
51
|
+
from?: number;
|
|
52
|
+
to?: number;
|
|
34
53
|
limit: number;
|
|
35
54
|
cursor?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
55
|
+
}
|
|
56
|
+
interface ParsedValues {
|
|
57
|
+
[key: string]: string | undefined;
|
|
38
58
|
root?: string;
|
|
39
|
-
'max-bytes'?: string;
|
|
40
|
-
limit?: string;
|
|
41
59
|
cursor?: string;
|
|
42
60
|
from?: string;
|
|
61
|
+
limit?: string;
|
|
43
62
|
to?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
}
|
|
64
|
+
interface CollectionContext {
|
|
65
|
+
config: Config;
|
|
66
|
+
root: string;
|
|
67
|
+
warnings: Project['warnings'];
|
|
68
|
+
}
|
|
69
|
+
interface ContinuationOptions {
|
|
70
|
+
lineEnd: number;
|
|
71
|
+
maxBytes: number;
|
|
72
|
+
requestedEnd: number;
|
|
73
|
+
totalLines: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const defaultInclude = ['**/*.md', '**/*.markdown', '**/*.mdown'];
|
|
77
|
+
const defaultMaxBytes = 16_384;
|
|
78
|
+
const maxOutputBytes = 65_536;
|
|
79
|
+
const maxSourceBytes = 32 * 1024 * 1024;
|
|
80
|
+
const maxCorpusBytes = 64 * 1024 * 1024;
|
|
81
|
+
const maxDocuments = 2048;
|
|
82
|
+
const maxPatterns = 64;
|
|
83
|
+
const origin = 'current-worktree';
|
|
84
|
+
const protectedDirectories = new Set(['.git', '.hivex', 'node_modules']);
|
|
85
|
+
const excludedDirectories = new Set(['vendor', 'dist', 'build']);
|
|
56
86
|
const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
57
87
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
const fail = function fail(
|
|
89
|
+
code: string,
|
|
90
|
+
message: string,
|
|
91
|
+
details?: Record<string, unknown>
|
|
92
|
+
): never {
|
|
93
|
+
throw new HivexError({ code, details, message });
|
|
94
|
+
};
|
|
61
95
|
|
|
62
|
-
function pathFor(root: string, absolutePath: string) {
|
|
63
|
-
return relative(root, absolutePath).
|
|
64
|
-
}
|
|
96
|
+
const pathFor = function pathFor(root: string, absolutePath: string) {
|
|
97
|
+
return pathModule.relative(root, absolutePath).replaceAll('\\', '/');
|
|
98
|
+
};
|
|
65
99
|
|
|
66
|
-
function decodeUtf8(bytes: Buffer,
|
|
67
|
-
if (bytes.byteLength > limit)
|
|
100
|
+
const decodeUtf8 = function decodeUtf8(bytes: Buffer, sourcePath: string, limit: number) {
|
|
101
|
+
if (bytes.byteLength > limit) {
|
|
68
102
|
fail('DOCUMENT_TOO_LARGE', `Markdown source exceeds ${limit} bytes`, {
|
|
69
|
-
path,
|
|
70
103
|
actualBytes: bytes.byteLength,
|
|
71
104
|
maxBytes: limit,
|
|
105
|
+
path: sourcePath,
|
|
72
106
|
});
|
|
107
|
+
}
|
|
73
108
|
try {
|
|
74
109
|
return decoder.decode(bytes);
|
|
75
110
|
} catch {
|
|
76
|
-
fail('INVALID_UTF8', 'Markdown source is not valid UTF-8', {
|
|
111
|
+
return fail('INVALID_UTF8', 'Markdown source is not valid UTF-8', {
|
|
112
|
+
path: sourcePath,
|
|
113
|
+
});
|
|
77
114
|
}
|
|
78
|
-
}
|
|
115
|
+
};
|
|
79
116
|
|
|
80
|
-
function readUtf8(absolutePath: string,
|
|
117
|
+
const readUtf8 = function readUtf8(absolutePath: string, sourcePath: string, limit: number) {
|
|
81
118
|
try {
|
|
82
|
-
return decodeUtf8(readFileSync(absolutePath),
|
|
119
|
+
return decodeUtf8(readFileSync(absolutePath), sourcePath, limit);
|
|
83
120
|
} catch (error) {
|
|
84
|
-
if (error instanceof HivexError)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
121
|
+
if (error instanceof HivexError) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
return fail('SOURCE_READ_FAILED', 'Unable to read Markdown source', {
|
|
125
|
+
path: sourcePath,
|
|
126
|
+
reason: Error.isError(error) ? error.message : 'unknown read failure',
|
|
88
127
|
});
|
|
89
128
|
}
|
|
90
|
-
}
|
|
129
|
+
};
|
|
91
130
|
|
|
92
|
-
function validatePattern(pattern: unknown, field: string, index: number) {
|
|
93
|
-
if (typeof pattern !== 'string' || !pattern.trim())
|
|
94
|
-
fail('INVALID_CONFIG', `${field}[${index}] must be a non-empty relative glob`);
|
|
131
|
+
const validatePattern = function validatePattern(pattern: unknown, field: string, index: number) {
|
|
132
|
+
if (typeof pattern !== 'string' || !pattern.trim()) {
|
|
133
|
+
return fail('INVALID_CONFIG', `${field}[${index}] must be a non-empty relative glob`);
|
|
134
|
+
}
|
|
95
135
|
const normalized = pattern.replaceAll('\\', '/');
|
|
96
136
|
const segments = normalized.split('/');
|
|
97
137
|
if (
|
|
98
|
-
isAbsolute(normalized) ||
|
|
138
|
+
pathModule.isAbsolute(normalized) ||
|
|
99
139
|
normalized.startsWith('/') ||
|
|
100
|
-
normalized.includes('\0') ||
|
|
140
|
+
normalized.includes('\u{0}') ||
|
|
101
141
|
segments.includes('..')
|
|
102
|
-
)
|
|
103
|
-
fail('INVALID_CONFIG', `${field}[${index}] must stay inside the project root`);
|
|
142
|
+
) {
|
|
143
|
+
return fail('INVALID_CONFIG', `${field}[${index}] must stay inside the project root`);
|
|
144
|
+
}
|
|
104
145
|
try {
|
|
105
|
-
new Bun.Glob(normalized);
|
|
146
|
+
const glob = new Bun.Glob(normalized);
|
|
147
|
+
glob.match('');
|
|
106
148
|
} catch (error) {
|
|
107
|
-
fail('INVALID_CONFIG', `${field}[${index}] is not a valid glob`, {
|
|
108
|
-
reason: error
|
|
149
|
+
return fail('INVALID_CONFIG', `${field}[${index}] is not a valid glob`, {
|
|
150
|
+
reason: Error.isError(error) ? error.message : 'invalid glob',
|
|
109
151
|
});
|
|
110
152
|
}
|
|
111
153
|
return normalized;
|
|
112
|
-
}
|
|
154
|
+
};
|
|
113
155
|
|
|
114
|
-
function patterns(value: unknown, field: string, fallback: string[]) {
|
|
115
|
-
if (value === undefined)
|
|
116
|
-
|
|
117
|
-
|
|
156
|
+
const patterns = function patterns(value: unknown, field: string, fallback: string[]) {
|
|
157
|
+
if (value === undefined) {
|
|
158
|
+
return [...fallback];
|
|
159
|
+
}
|
|
160
|
+
if (!Array.isArray(value) || value.length > maxPatterns) {
|
|
161
|
+
return fail('INVALID_CONFIG', `${field} must contain at most ${maxPatterns} relative globs`);
|
|
162
|
+
}
|
|
118
163
|
return value.map((pattern, index) => validatePattern(pattern, field, index));
|
|
119
|
-
}
|
|
164
|
+
};
|
|
120
165
|
|
|
121
|
-
function
|
|
122
|
-
const
|
|
166
|
+
const readConfigBytes = function readConfigBytes(configPath: string) {
|
|
167
|
+
const stat = lstatSync(configPath);
|
|
168
|
+
if (stat.isSymbolicLink()) {
|
|
169
|
+
return fail('INVALID_CONFIG', 'hivex.json must not be a symlink');
|
|
170
|
+
}
|
|
171
|
+
if (!stat.isFile()) {
|
|
172
|
+
return fail('INVALID_CONFIG', 'hivex.json must be a regular file');
|
|
173
|
+
}
|
|
174
|
+
return readFileSync(configPath);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const configText = function configText(root: string) {
|
|
178
|
+
const configPath = pathModule.join(root, 'hivex.json');
|
|
123
179
|
let bytes: Buffer;
|
|
124
180
|
try {
|
|
125
|
-
|
|
126
|
-
if (stat.isSymbolicLink()) fail('INVALID_CONFIG', 'hivex.json must not be a symlink');
|
|
127
|
-
if (!stat.isFile()) fail('INVALID_CONFIG', 'hivex.json must be a regular file');
|
|
128
|
-
bytes = readFileSync(path);
|
|
181
|
+
bytes = readConfigBytes(configPath);
|
|
129
182
|
} catch (error) {
|
|
130
|
-
if (error instanceof HivexError)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
183
|
+
if (error instanceof HivexError) {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
if (Error.isError(error) && 'code' in error && error.code === 'ENOENT') {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return fail('INVALID_CONFIG', 'Unable to read hivex.json', {
|
|
190
|
+
reason: Error.isError(error) ? error.message : 'unknown read failure',
|
|
134
191
|
});
|
|
135
192
|
}
|
|
136
193
|
return decodeUtf8(bytes, 'hivex.json', 64 * 1024);
|
|
137
|
-
}
|
|
194
|
+
};
|
|
138
195
|
|
|
139
|
-
function
|
|
196
|
+
const isRecord = function isRecord(value: unknown): value is Record<string, unknown> {
|
|
197
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const parseConfig = function parseConfig(text: string): Config {
|
|
140
201
|
let value: unknown;
|
|
141
202
|
try {
|
|
142
|
-
value = JSON.parse(text.replace(/^\
|
|
203
|
+
value = JSON.parse(text.replace(/^\u{FEFF}/u, ''));
|
|
143
204
|
} catch (error) {
|
|
144
|
-
fail('INVALID_CONFIG', 'hivex.json must contain valid JSON', {
|
|
145
|
-
reason: error
|
|
205
|
+
return fail('INVALID_CONFIG', 'hivex.json must contain valid JSON', {
|
|
206
|
+
reason: Error.isError(error) ? error.message : 'invalid JSON',
|
|
146
207
|
});
|
|
147
208
|
}
|
|
148
|
-
if (!
|
|
149
|
-
fail('INVALID_CONFIG', 'hivex.json must contain an object');
|
|
150
|
-
|
|
151
|
-
if ('collections' in
|
|
152
|
-
fail(
|
|
209
|
+
if (!isRecord(value)) {
|
|
210
|
+
return fail('INVALID_CONFIG', 'hivex.json must contain an object');
|
|
211
|
+
}
|
|
212
|
+
if ('collections' in value) {
|
|
213
|
+
return fail(
|
|
153
214
|
'LEGACY_CONFIGURATION',
|
|
154
|
-
'hivex.json uses legacy collections; replace it with include and exclude globs'
|
|
215
|
+
'hivex.json uses legacy collections; replace it with include and exclude globs'
|
|
155
216
|
);
|
|
156
|
-
|
|
157
|
-
|
|
217
|
+
}
|
|
218
|
+
const record = value;
|
|
219
|
+
const unknown = Object.keys(record).filter(
|
|
220
|
+
(key) => !['exclude', 'history', 'include'].includes(key)
|
|
221
|
+
);
|
|
222
|
+
if (unknown.length) {
|
|
223
|
+
return fail('INVALID_CONFIG', `hivex.json has unsupported field: ${unknown[0]}`);
|
|
224
|
+
}
|
|
158
225
|
return {
|
|
159
|
-
|
|
160
|
-
|
|
226
|
+
exclude: patterns(value.exclude, 'exclude', []),
|
|
227
|
+
history: patterns(value.history, 'history', []),
|
|
228
|
+
include: patterns(value.include, 'include', defaultInclude),
|
|
161
229
|
};
|
|
162
|
-
}
|
|
230
|
+
};
|
|
163
231
|
|
|
164
|
-
function configFrom(root: string): Config {
|
|
232
|
+
const configFrom = function configFrom(root: string): Config {
|
|
165
233
|
const text = configText(root);
|
|
166
|
-
if (text === null)
|
|
234
|
+
if (text === null) {
|
|
235
|
+
return { exclude: [], history: [], include: [...defaultInclude] };
|
|
236
|
+
}
|
|
167
237
|
return parseConfig(text);
|
|
168
|
-
}
|
|
238
|
+
};
|
|
169
239
|
|
|
170
|
-
function
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
240
|
+
const isExcludedName = function isExcludedName(name: string, config: Config) {
|
|
241
|
+
if (protectedDirectories.has(name)) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
if (!excludedDirectories.has(name) && !name.startsWith('.')) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return [...config.include, ...config.history].every(
|
|
248
|
+
(pattern) => !pattern.split('/').includes(name)
|
|
249
|
+
);
|
|
250
|
+
};
|
|
175
251
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
252
|
+
const isMatch = function isMatch(pathName: string, patternsToMatch: string[]) {
|
|
253
|
+
return patternsToMatch.some((pattern) => {
|
|
254
|
+
const glob = new Bun.Glob(pattern);
|
|
255
|
+
return glob.match(pathName);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const isExcludedSubtree = function isExcludedSubtree(pathName: string, config: Config) {
|
|
260
|
+
const subtrees = config.exclude.filter(
|
|
261
|
+
(pattern) => pattern.endsWith('/**') && !pattern.startsWith('!')
|
|
262
|
+
);
|
|
263
|
+
return isMatch(`${pathName}/`, subtrees);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const collectCandidates = function collectCandidates(current: string, context: CollectionContext) {
|
|
267
|
+
const { config, root, warnings } = context;
|
|
182
268
|
const candidates: Candidate[] = [];
|
|
183
269
|
let entries;
|
|
184
270
|
try {
|
|
185
|
-
entries = readdirSync(current, { withFileTypes: true }).
|
|
186
|
-
left.name.localeCompare(right.name)
|
|
271
|
+
entries = readdirSync(current, { withFileTypes: true }).toSorted((left, right) =>
|
|
272
|
+
left.name.localeCompare(right.name)
|
|
187
273
|
);
|
|
188
274
|
} catch (error) {
|
|
189
275
|
warnings.push({
|
|
276
|
+
message: `Unable to inspect directory: ${Error.isError(error) ? error.message : 'unknown error'}`,
|
|
190
277
|
path: pathFor(root, current) || '.',
|
|
191
|
-
message: `Unable to inspect directory: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
192
278
|
});
|
|
193
279
|
return candidates;
|
|
194
280
|
}
|
|
195
281
|
|
|
196
282
|
for (const entry of entries) {
|
|
197
|
-
|
|
198
|
-
const path = pathFor(root, absolutePath);
|
|
199
|
-
if (excludedName(entry.name, config)) continue;
|
|
200
|
-
if (entry.isSymbolicLink()) {
|
|
201
|
-
warnings.push({ path, message: 'Skipped symbolic link' });
|
|
283
|
+
if (isExcludedName(entry.name, config)) {
|
|
202
284
|
continue;
|
|
203
285
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
286
|
+
const absolutePath = pathModule.join(current, entry.name);
|
|
287
|
+
const path = pathFor(root, absolutePath);
|
|
288
|
+
if (entry.isSymbolicLink()) {
|
|
289
|
+
warnings.push({ message: 'Skipped symbolic link', path });
|
|
290
|
+
} else if (entry.isDirectory()) {
|
|
291
|
+
if (!isExcludedSubtree(path, config)) {
|
|
292
|
+
candidates.push(...collectCandidates(absolutePath, context));
|
|
293
|
+
}
|
|
294
|
+
} else if (entry.isFile()) {
|
|
295
|
+
candidates.push({ absolutePath, path });
|
|
207
296
|
}
|
|
208
|
-
if (entry.isFile()) candidates.push({ absolutePath, path });
|
|
209
297
|
}
|
|
210
298
|
return candidates;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function matches(path: string, patternsToMatch: string[]) {
|
|
214
|
-
return patternsToMatch.some((pattern) => new Bun.Glob(pattern).match(path));
|
|
215
|
-
}
|
|
299
|
+
};
|
|
216
300
|
|
|
217
|
-
function selected(candidates: Candidate[], config: Config) {
|
|
218
|
-
|
|
219
|
-
.filter(({ path }) => isMarkdownPath(
|
|
220
|
-
.filter(({ path }) =>
|
|
221
|
-
.
|
|
222
|
-
|
|
223
|
-
|
|
301
|
+
const selected = function selected(candidates: Candidate[], config: Config) {
|
|
302
|
+
const available = candidates
|
|
303
|
+
.filter(({ path: pathName }) => isMarkdownPath(pathName))
|
|
304
|
+
.filter(({ path: pathName }) => !isMatch(pathName, config.exclude))
|
|
305
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
306
|
+
return {
|
|
307
|
+
current: available.filter(
|
|
308
|
+
({ path: pathName }) =>
|
|
309
|
+
isMatch(pathName, config.include) && !isMatch(pathName, config.history)
|
|
310
|
+
),
|
|
311
|
+
historical: available.filter(({ path: pathName }) => isMatch(pathName, config.history)),
|
|
312
|
+
};
|
|
313
|
+
};
|
|
224
314
|
|
|
225
|
-
|
|
226
|
-
|
|
315
|
+
const parseCandidate = function parseCandidate(
|
|
316
|
+
candidate: Candidate,
|
|
317
|
+
isHistorical: boolean
|
|
318
|
+
): ParsedDocument {
|
|
319
|
+
const text = readUtf8(candidate.absolutePath, candidate.path, maxSourceBytes);
|
|
227
320
|
const source = describeMarkdown(candidate.path, text);
|
|
228
321
|
return {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
322
|
+
document: {
|
|
323
|
+
hash: hash(text),
|
|
324
|
+
historical: isHistorical,
|
|
325
|
+
id: candidate.path,
|
|
326
|
+
links: [],
|
|
327
|
+
path: candidate.path,
|
|
328
|
+
status: source.status,
|
|
329
|
+
text,
|
|
330
|
+
title: source.title,
|
|
331
|
+
},
|
|
236
332
|
rawLinks: source.links,
|
|
237
333
|
};
|
|
238
|
-
}
|
|
334
|
+
};
|
|
239
335
|
|
|
240
|
-
function warningFor(path: string, error: unknown) {
|
|
336
|
+
const warningFor = function warningFor(path: string, error: unknown) {
|
|
241
337
|
return {
|
|
338
|
+
message: Error.isError(error) ? error.message : 'Unable to parse Markdown source',
|
|
242
339
|
path,
|
|
243
|
-
message: error instanceof Error ? error.message : 'Unable to parse Markdown source',
|
|
244
340
|
};
|
|
245
|
-
}
|
|
341
|
+
};
|
|
246
342
|
|
|
247
|
-
function linkPath(root: string, source: Document, rawLink: string
|
|
248
|
-
if (!rawLink || rawLink.startsWith('#') || /^[a-z][a-z0-9+.-]*:/
|
|
249
|
-
|
|
343
|
+
const linkPath = function linkPath(root: string, source: Document, rawLink: string) {
|
|
344
|
+
if (!rawLink || rawLink.startsWith('#') || /^[a-z][a-z0-9+.-]*:/iu.test(rawLink)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const fragment = rawLink.search(/[?#]/u);
|
|
250
348
|
const target = fragment === -1 ? rawLink : rawLink.slice(0, fragment);
|
|
251
|
-
if (!target)
|
|
349
|
+
if (!target) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
252
352
|
let decoded: string;
|
|
253
353
|
try {
|
|
254
354
|
decoded = decodeURIComponent(target);
|
|
255
355
|
} catch {
|
|
256
356
|
return null;
|
|
257
357
|
}
|
|
258
|
-
const absoluteTarget = resolve(
|
|
358
|
+
const absoluteTarget = pathModule.resolve(
|
|
359
|
+
pathModule.dirname(pathModule.join(root, source.path)),
|
|
360
|
+
decoded
|
|
361
|
+
);
|
|
259
362
|
const relativeTarget = pathFor(root, absoluteTarget);
|
|
260
|
-
if (
|
|
261
|
-
!relativeTarget ||
|
|
262
|
-
relativeTarget === '.' ||
|
|
263
|
-
relativeTarget.startsWith('../') ||
|
|
264
|
-
isAbsolute(relativeTarget) ||
|
|
265
|
-
!ids.has(relativeTarget)
|
|
266
|
-
)
|
|
363
|
+
if (!relativeTarget || relativeTarget === '.') {
|
|
267
364
|
return null;
|
|
365
|
+
}
|
|
366
|
+
if (relativeTarget.startsWith('../') || pathModule.isAbsolute(relativeTarget)) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (!isMarkdownPath(relativeTarget)) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
268
372
|
return relativeTarget;
|
|
269
|
-
}
|
|
373
|
+
};
|
|
270
374
|
|
|
271
|
-
function resolveLinks(root: string, documents: ParsedDocument[]) {
|
|
272
|
-
const
|
|
273
|
-
for (const document of documents) {
|
|
375
|
+
const resolveLinks = function resolveLinks(root: string, documents: ParsedDocument[]) {
|
|
376
|
+
for (const { document, rawLinks } of documents) {
|
|
274
377
|
const links = new Set<string>();
|
|
275
|
-
for (const rawLink of
|
|
276
|
-
const link = linkPath(root, document, rawLink
|
|
277
|
-
if (link)
|
|
378
|
+
for (const rawLink of rawLinks) {
|
|
379
|
+
const link = linkPath(root, document, rawLink);
|
|
380
|
+
if (link !== null) {
|
|
381
|
+
links.add(link);
|
|
382
|
+
}
|
|
278
383
|
}
|
|
279
384
|
document.links = [...links];
|
|
280
385
|
}
|
|
281
|
-
}
|
|
386
|
+
};
|
|
282
387
|
|
|
283
|
-
function snapshotFor(documents: Document[], config: Config) {
|
|
388
|
+
const snapshotFor = function snapshotFor(documents: Document[], config: Config) {
|
|
284
389
|
const identities = documents
|
|
285
390
|
.map((document) => `${document.id}\0${document.hash}`)
|
|
286
|
-
.
|
|
391
|
+
.toSorted(compareSerializedStrings)
|
|
287
392
|
.join('\n');
|
|
288
|
-
const selection = JSON.stringify(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
393
|
+
const selection = JSON.stringify(
|
|
394
|
+
Object.fromEntries([
|
|
395
|
+
['include', [...config.include].toSorted(compareSerializedStrings)],
|
|
396
|
+
['exclude', [...config.exclude].toSorted(compareSerializedStrings)],
|
|
397
|
+
['history', [...config.history].toSorted(compareSerializedStrings)],
|
|
398
|
+
[
|
|
399
|
+
'ignoredDirectories',
|
|
400
|
+
[...protectedDirectories, ...excludedDirectories].toSorted(compareSerializedStrings),
|
|
401
|
+
],
|
|
402
|
+
['markdownExtensions', ['.md', '.markdown', '.mdown']],
|
|
403
|
+
])
|
|
404
|
+
);
|
|
294
405
|
return hash(`${identities}\nselection\0${selection}`);
|
|
295
|
-
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const validateRoot = function validateRoot(requested: string) {
|
|
409
|
+
const stat = lstatSync(requested);
|
|
410
|
+
if (stat.isSymbolicLink()) {
|
|
411
|
+
return fail('INVALID_ROOT', 'Project root must not be a symlink');
|
|
412
|
+
}
|
|
413
|
+
if (!stat.isDirectory()) {
|
|
414
|
+
return fail('INVALID_ROOT', 'Project root must be a directory');
|
|
415
|
+
}
|
|
416
|
+
return requested;
|
|
417
|
+
};
|
|
296
418
|
|
|
297
|
-
function absoluteRoot(root: string) {
|
|
298
|
-
if (!root.trim())
|
|
299
|
-
|
|
419
|
+
const absoluteRoot = function absoluteRoot(root: string) {
|
|
420
|
+
if (!root.trim()) {
|
|
421
|
+
fail('INVALID_ROOT', 'Project root must be a non-empty path');
|
|
422
|
+
}
|
|
423
|
+
const requested = pathModule.resolve(root);
|
|
300
424
|
try {
|
|
301
|
-
|
|
302
|
-
if (stat.isSymbolicLink()) fail('INVALID_ROOT', 'Project root must not be a symlink');
|
|
303
|
-
if (!stat.isDirectory()) fail('INVALID_ROOT', 'Project root must be a directory');
|
|
304
|
-
return requested;
|
|
425
|
+
return validateRoot(requested);
|
|
305
426
|
} catch (error) {
|
|
306
|
-
if (error instanceof HivexError)
|
|
307
|
-
|
|
427
|
+
if (error instanceof HivexError) {
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
return fail('INVALID_ROOT', 'Project root is not readable', {
|
|
431
|
+
reason: Error.isError(error) ? error.message : 'unknown root failure',
|
|
308
432
|
root: requested,
|
|
309
|
-
reason: error instanceof Error ? error.message : 'unknown root failure',
|
|
310
433
|
});
|
|
311
434
|
}
|
|
312
|
-
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const parseCandidateWithinBudget = function parseCandidateWithinBudget(
|
|
438
|
+
candidate: Candidate,
|
|
439
|
+
isHistorical: boolean,
|
|
440
|
+
sourceBytes: number
|
|
441
|
+
) {
|
|
442
|
+
if (sourceBytes + lstatSync(candidate.absolutePath).size > maxCorpusBytes) {
|
|
443
|
+
return fail(
|
|
444
|
+
'CORPUS_LIMIT',
|
|
445
|
+
'Selected Markdown exceeds the 64 MiB memory budget; narrow include paths'
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return parseCandidate(candidate, isHistorical);
|
|
449
|
+
};
|
|
313
450
|
|
|
314
|
-
export function loadProject(root: string): Project {
|
|
451
|
+
export const loadProject = function loadProject(root: string): Project {
|
|
315
452
|
const projectRoot = absoluteRoot(root);
|
|
316
453
|
const config = configFrom(projectRoot);
|
|
317
454
|
const warnings: Project['warnings'] = [];
|
|
318
|
-
const candidates = collectCandidates(projectRoot,
|
|
319
|
-
|
|
455
|
+
const candidates = collectCandidates(projectRoot, {
|
|
456
|
+
config,
|
|
457
|
+
root: projectRoot,
|
|
458
|
+
warnings,
|
|
459
|
+
});
|
|
460
|
+
const selection = selected(candidates, config);
|
|
461
|
+
const historicalPaths = new Set(selection.historical.map((candidate) => candidate.path));
|
|
462
|
+
const selectedCandidates = [...selection.current, ...selection.historical];
|
|
320
463
|
const parsed: ParsedDocument[] = [];
|
|
321
464
|
let sourceBytes = 0;
|
|
322
|
-
for (const candidate of selectedCandidates.slice(0,
|
|
465
|
+
for (const candidate of selectedCandidates.slice(0, maxDocuments)) {
|
|
323
466
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
parsed.push(document);
|
|
467
|
+
const parsedDocument = parseCandidateWithinBudget(
|
|
468
|
+
candidate,
|
|
469
|
+
historicalPaths.has(candidate.path),
|
|
470
|
+
sourceBytes
|
|
471
|
+
);
|
|
472
|
+
sourceBytes += Buffer.byteLength(parsedDocument.document.text);
|
|
473
|
+
parsed.push(parsedDocument);
|
|
332
474
|
} catch (error) {
|
|
333
475
|
warnings.push(warningFor(candidate.path, error));
|
|
334
476
|
}
|
|
335
477
|
}
|
|
336
|
-
if (selectedCandidates.length >
|
|
478
|
+
if (selectedCandidates.length > maxDocuments) {
|
|
337
479
|
warnings.push({
|
|
480
|
+
message: `Only the first ${maxDocuments} Markdown sources were loaded`,
|
|
338
481
|
path: '.',
|
|
339
|
-
message: `Only the first ${MAX_DOCUMENTS} Markdown sources were loaded`,
|
|
340
482
|
});
|
|
483
|
+
}
|
|
341
484
|
resolveLinks(projectRoot, parsed);
|
|
342
|
-
const documents = parsed
|
|
485
|
+
const documents = parsed
|
|
486
|
+
.map(({ document }) => document)
|
|
487
|
+
.toSorted((left, right) => left.path.localeCompare(right.path));
|
|
343
488
|
return {
|
|
489
|
+
currentDocuments: documents.filter((document) => !document.historical),
|
|
490
|
+
currentSnapshot: snapshotFor(
|
|
491
|
+
documents.filter((document) => !document.historical),
|
|
492
|
+
config
|
|
493
|
+
),
|
|
494
|
+
documents,
|
|
495
|
+
historicalDocuments: documents.filter((document) => document.historical),
|
|
344
496
|
root: projectRoot,
|
|
345
497
|
snapshot: snapshotFor(documents, config),
|
|
346
|
-
documents,
|
|
347
498
|
warnings,
|
|
348
499
|
};
|
|
349
|
-
}
|
|
500
|
+
};
|
|
350
501
|
|
|
351
|
-
|
|
502
|
+
const positiveInteger = function positiveInteger(
|
|
503
|
+
value: string | undefined,
|
|
504
|
+
label: string,
|
|
505
|
+
fallback?: number
|
|
506
|
+
) {
|
|
352
507
|
if (value === undefined) {
|
|
353
|
-
if (fallback !== undefined)
|
|
354
|
-
|
|
508
|
+
if (fallback !== undefined) {
|
|
509
|
+
return fallback;
|
|
510
|
+
}
|
|
511
|
+
return fail('INVALID_ARGUMENT', `${label} is required`);
|
|
512
|
+
}
|
|
513
|
+
if (!/^\d+$/u.test(value)) {
|
|
514
|
+
return fail('INVALID_ARGUMENT', `${label} must be a positive integer`);
|
|
355
515
|
}
|
|
356
|
-
if (!/^[0-9]+$/.test(value)) fail('INVALID_ARGUMENT', `${label} must be a positive integer`);
|
|
357
516
|
const number = Number(value);
|
|
358
|
-
if (!Number.isSafeInteger(number) || number < 1)
|
|
359
|
-
fail('INVALID_ARGUMENT', `${label} must be positive`);
|
|
517
|
+
if (!Number.isSafeInteger(number) || number < 1) {
|
|
518
|
+
return fail('INVALID_ARGUMENT', `${label} must be positive`);
|
|
519
|
+
}
|
|
360
520
|
return number;
|
|
361
|
-
}
|
|
521
|
+
};
|
|
362
522
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
523
|
+
const optionalPositiveInteger = function optionalPositiveInteger(
|
|
524
|
+
value: string | undefined,
|
|
525
|
+
label: string
|
|
526
|
+
) {
|
|
527
|
+
let result: number | undefined;
|
|
528
|
+
if (value !== undefined) {
|
|
529
|
+
result = positiveInteger(value, label);
|
|
530
|
+
}
|
|
531
|
+
return result;
|
|
532
|
+
};
|
|
367
533
|
|
|
368
|
-
function
|
|
369
|
-
let parsed: ReturnType<typeof parseArgs>;
|
|
534
|
+
const parseCommandArguments = function parseCommandArguments(commandArguments: string[]) {
|
|
370
535
|
try {
|
|
371
|
-
|
|
372
|
-
args,
|
|
536
|
+
return parseArgs({
|
|
373
537
|
allowPositionals: true,
|
|
374
|
-
|
|
538
|
+
args: commandArguments,
|
|
375
539
|
options: {
|
|
376
|
-
root: { type: 'string' },
|
|
377
|
-
'max-bytes': { type: 'string' },
|
|
378
|
-
limit: { type: 'string' },
|
|
379
540
|
cursor: { type: 'string' },
|
|
380
541
|
from: { type: 'string' },
|
|
542
|
+
limit: { type: 'string' },
|
|
543
|
+
'max-bytes': { type: 'string' },
|
|
544
|
+
root: { type: 'string' },
|
|
381
545
|
to: { type: 'string' },
|
|
382
546
|
},
|
|
547
|
+
strict: true,
|
|
383
548
|
});
|
|
384
549
|
} catch (error) {
|
|
385
|
-
fail(
|
|
550
|
+
return fail(
|
|
551
|
+
'INVALID_ARGUMENT',
|
|
552
|
+
Error.isError(error) ? error.message : 'Invalid command arguments'
|
|
553
|
+
);
|
|
386
554
|
}
|
|
387
|
-
|
|
388
|
-
}
|
|
555
|
+
};
|
|
389
556
|
|
|
390
|
-
|
|
557
|
+
const validatePositionals: (
|
|
391
558
|
command: string | undefined,
|
|
392
559
|
id: string | undefined,
|
|
393
|
-
extra: string | undefined
|
|
560
|
+
extra: string | undefined
|
|
561
|
+
) => asserts command is 'sources' | 'read' = function validatePositionals(
|
|
562
|
+
command: string | undefined,
|
|
563
|
+
id: string | undefined,
|
|
564
|
+
extra: string | undefined
|
|
394
565
|
): asserts command is 'sources' | 'read' {
|
|
395
|
-
if (command !== 'sources' && command !== 'read')
|
|
566
|
+
if (command !== 'sources' && command !== 'read') {
|
|
396
567
|
fail('INVALID_ARGUMENT', 'Usage: hivex sources | read <id> [options]');
|
|
397
|
-
|
|
568
|
+
}
|
|
569
|
+
if (command === 'sources' && (id !== undefined || extra !== undefined)) {
|
|
398
570
|
fail('INVALID_ARGUMENT', 'sources does not accept a source id');
|
|
399
|
-
|
|
400
|
-
if (command === 'read' &&
|
|
571
|
+
}
|
|
572
|
+
if (command === 'read' && id === undefined) {
|
|
573
|
+
fail('INVALID_ARGUMENT', 'read requires a source id');
|
|
574
|
+
}
|
|
575
|
+
if (command === 'read' && extra !== undefined) {
|
|
401
576
|
fail('INVALID_ARGUMENT', 'read accepts one source id');
|
|
402
|
-
}
|
|
577
|
+
}
|
|
578
|
+
};
|
|
403
579
|
|
|
404
|
-
function rangeOptions(command: string, values: ParsedValues) {
|
|
580
|
+
const rangeOptions = function rangeOptions(command: string, values: ParsedValues) {
|
|
405
581
|
if (command === 'sources') {
|
|
406
|
-
if (values.from !== undefined || values.to !== undefined)
|
|
582
|
+
if (values.from !== undefined || values.to !== undefined) {
|
|
407
583
|
fail('INVALID_ARGUMENT', '--from and --to are only valid for read');
|
|
408
|
-
|
|
584
|
+
}
|
|
585
|
+
return {};
|
|
409
586
|
}
|
|
410
|
-
if (values.limit !== undefined || values.cursor !== undefined)
|
|
587
|
+
if (values.limit !== undefined || values.cursor !== undefined) {
|
|
411
588
|
fail('INVALID_ARGUMENT', '--limit and --cursor are only valid for sources');
|
|
589
|
+
}
|
|
412
590
|
return {
|
|
413
591
|
from: optionalPositiveInteger(values.from, '--from'),
|
|
414
592
|
to: optionalPositiveInteger(values.to, '--to'),
|
|
415
593
|
};
|
|
416
|
-
}
|
|
594
|
+
};
|
|
417
595
|
|
|
418
|
-
function commandOptions(
|
|
596
|
+
const commandOptions = function commandOptions(commandArguments: string[]): {
|
|
419
597
|
command: string;
|
|
420
598
|
id: string | undefined;
|
|
421
599
|
options: CommandOptions;
|
|
422
600
|
} {
|
|
423
|
-
const parsed =
|
|
601
|
+
const parsed = parseCommandArguments(commandArguments);
|
|
424
602
|
const values = parsed.values as ParsedValues;
|
|
425
603
|
const [command, id, extra] = parsed.positionals;
|
|
426
604
|
validatePositionals(command, id, extra);
|
|
427
|
-
const maxBytes = positiveInteger(values['max-bytes'], '--max-bytes',
|
|
428
|
-
if (maxBytes >
|
|
429
|
-
fail('INVALID_ARGUMENT', `--max-bytes must be at most ${
|
|
605
|
+
const maxBytes = positiveInteger(values['max-bytes'], '--max-bytes', defaultMaxBytes);
|
|
606
|
+
if (maxBytes > maxOutputBytes) {
|
|
607
|
+
fail('INVALID_ARGUMENT', `--max-bytes must be at most ${maxOutputBytes}`);
|
|
608
|
+
}
|
|
430
609
|
return {
|
|
431
610
|
command,
|
|
432
611
|
id,
|
|
433
612
|
options: {
|
|
434
|
-
root: values.root ?? process.cwd(),
|
|
435
|
-
maxBytes,
|
|
436
|
-
limit: positiveInteger(values.limit, '--limit', 20),
|
|
437
|
-
cursor: values.cursor,
|
|
438
613
|
...rangeOptions(command, values),
|
|
614
|
+
cursor: values.cursor,
|
|
615
|
+
limit: positiveInteger(values.limit, '--limit', 20),
|
|
616
|
+
maxBytes,
|
|
617
|
+
root: values.root ?? process.cwd(),
|
|
439
618
|
},
|
|
440
619
|
};
|
|
441
|
-
}
|
|
620
|
+
};
|
|
442
621
|
|
|
443
|
-
function metadata(document: Document) {
|
|
444
|
-
const
|
|
622
|
+
const metadata = function metadata(document: Document) {
|
|
623
|
+
const result = { ...document };
|
|
624
|
+
Reflect.deleteProperty(result, 'text');
|
|
445
625
|
return result;
|
|
446
|
-
}
|
|
626
|
+
};
|
|
447
627
|
|
|
448
|
-
function linesFor(text: string) {
|
|
628
|
+
const linesFor = function linesFor(text: string) {
|
|
449
629
|
return { lines: rawMarkdownLines(text) };
|
|
450
|
-
}
|
|
630
|
+
};
|
|
451
631
|
|
|
452
|
-
function boundedLines(window: {
|
|
632
|
+
const boundedLines = function boundedLines(window: {
|
|
633
|
+
lines: string[];
|
|
634
|
+
start: number;
|
|
635
|
+
end: number;
|
|
636
|
+
maxBytes: number;
|
|
637
|
+
}) {
|
|
453
638
|
const { lines, start, end, maxBytes } = window;
|
|
454
639
|
let text = '';
|
|
455
640
|
let prefix = '';
|
|
456
641
|
let lineEnd = start - 1;
|
|
457
|
-
for (let line = start; line <= end; line
|
|
642
|
+
for (let line = start; line <= end; line += 1) {
|
|
458
643
|
const raw = lines[line - 1] ?? '';
|
|
459
644
|
const current = line === lines.length ? raw : lineContent(raw);
|
|
460
645
|
const next = prefix + current;
|
|
461
646
|
if (Buffer.byteLength(next) > maxBytes) {
|
|
462
|
-
if (lineEnd < start)
|
|
647
|
+
if (lineEnd < start) {
|
|
463
648
|
fail('OUTPUT_LIMIT', 'The first requested line exceeds --max-bytes', {
|
|
464
649
|
line,
|
|
465
650
|
maxBytes,
|
|
466
651
|
requiredBytes: Buffer.byteLength(next),
|
|
467
652
|
});
|
|
468
|
-
|
|
653
|
+
}
|
|
654
|
+
return { lineEnd, text };
|
|
469
655
|
}
|
|
470
656
|
text = next;
|
|
471
657
|
prefix += raw;
|
|
472
658
|
lineEnd = line;
|
|
473
659
|
}
|
|
474
|
-
return {
|
|
475
|
-
}
|
|
660
|
+
return { lineEnd, text };
|
|
661
|
+
};
|
|
476
662
|
|
|
477
|
-
function continuationFor(
|
|
478
|
-
lineEnd
|
|
479
|
-
|
|
480
|
-
requestedEnd
|
|
481
|
-
|
|
482
|
-
) {
|
|
483
|
-
if (lineEnd >= totalLines)
|
|
484
|
-
|
|
485
|
-
|
|
663
|
+
const continuationFor = function continuationFor({
|
|
664
|
+
lineEnd,
|
|
665
|
+
maxBytes,
|
|
666
|
+
requestedEnd,
|
|
667
|
+
totalLines,
|
|
668
|
+
}: ContinuationOptions) {
|
|
669
|
+
if (lineEnd >= totalLines) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
const reason = lineEnd < requestedEnd ? 'max-bytes' : 'range';
|
|
486
673
|
return {
|
|
487
674
|
from: lineEnd + 1,
|
|
488
|
-
to: totalLines,
|
|
489
|
-
reason,
|
|
490
675
|
maxBytes,
|
|
676
|
+
reason,
|
|
677
|
+
to: totalLines,
|
|
491
678
|
};
|
|
492
|
-
}
|
|
679
|
+
};
|
|
493
680
|
|
|
494
|
-
function readCommand(project: Project, id: string, options: CommandOptions) {
|
|
681
|
+
const readCommand = function readCommand(project: Project, id: string, options: CommandOptions) {
|
|
495
682
|
const source = project.documents.find((document) => document.id === id);
|
|
496
|
-
if (!source)
|
|
683
|
+
if (!source) {
|
|
684
|
+
return fail('SOURCE_NOT_FOUND', `Markdown source was not selected: ${id}`, {
|
|
685
|
+
id,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
497
688
|
const { lines } = linesFor(source.text);
|
|
498
689
|
const start = options.from ?? 1;
|
|
499
690
|
const requestedEnd = options.to ?? lines.length;
|
|
500
|
-
if (start > lines.length || requestedEnd > lines.length || start > requestedEnd)
|
|
691
|
+
if (start > lines.length || requestedEnd > lines.length || start > requestedEnd) {
|
|
501
692
|
fail('INVALID_RANGE', `Line range ${start}-${requestedEnd} is outside the source`, {
|
|
502
693
|
id,
|
|
503
694
|
lineCount: lines.length,
|
|
504
695
|
});
|
|
696
|
+
}
|
|
505
697
|
const bounded = boundedLines({
|
|
506
|
-
lines,
|
|
507
|
-
start,
|
|
508
698
|
end: requestedEnd,
|
|
699
|
+
lines,
|
|
509
700
|
maxBytes: options.maxBytes,
|
|
701
|
+
start,
|
|
510
702
|
});
|
|
511
|
-
const continuation = continuationFor(
|
|
512
|
-
bounded.lineEnd,
|
|
513
|
-
|
|
703
|
+
const continuation = continuationFor({
|
|
704
|
+
lineEnd: bounded.lineEnd,
|
|
705
|
+
maxBytes: options.maxBytes,
|
|
514
706
|
requestedEnd,
|
|
515
|
-
|
|
516
|
-
);
|
|
707
|
+
totalLines: lines.length,
|
|
708
|
+
});
|
|
517
709
|
return {
|
|
518
710
|
command: 'read',
|
|
519
|
-
|
|
711
|
+
continuation,
|
|
712
|
+
lineEnd: bounded.lineEnd,
|
|
713
|
+
lineStart: start,
|
|
714
|
+
origin,
|
|
520
715
|
snapshot: project.snapshot,
|
|
521
716
|
source: metadata(source),
|
|
522
717
|
text: bounded.text,
|
|
523
|
-
lineStart: start,
|
|
524
|
-
lineEnd: bounded.lineEnd,
|
|
525
|
-
continuation,
|
|
526
718
|
truncated: continuation !== null,
|
|
527
719
|
warnings: project.warnings,
|
|
528
720
|
};
|
|
529
|
-
}
|
|
721
|
+
};
|
|
530
722
|
|
|
531
|
-
function listSources(project: Project, options: CommandOptions) {
|
|
532
|
-
const cursor = options.cursor?.match(/^s1\.([a-
|
|
533
|
-
|
|
723
|
+
const listSources = function listSources(project: Project, options: CommandOptions) {
|
|
724
|
+
const cursor = options.cursor?.match(/^s1\.(?<snapshot>[a-f\d]{64})\.(?<start>\d+)$/u);
|
|
725
|
+
const cursorSnapshot = cursor?.groups?.snapshot;
|
|
726
|
+
if (options.cursor !== undefined && cursorSnapshot !== project.snapshot) {
|
|
534
727
|
fail('INVALID_CURSOR', 'Source continuation belongs to a different or invalid snapshot');
|
|
535
|
-
|
|
536
|
-
|
|
728
|
+
}
|
|
729
|
+
const start = Number(cursor?.groups?.start ?? 0);
|
|
730
|
+
if (
|
|
731
|
+
!Number.isSafeInteger(start) ||
|
|
732
|
+
start < 0 ||
|
|
733
|
+
(start > 0 && start >= project.documents.length)
|
|
734
|
+
) {
|
|
537
735
|
fail('INVALID_CURSOR', 'Source continuation is outside this snapshot');
|
|
736
|
+
}
|
|
538
737
|
const documents: ReturnType<typeof metadata>[] = [];
|
|
539
|
-
const response = ()
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
738
|
+
const response = function response() {
|
|
739
|
+
return {
|
|
740
|
+
command: 'sources',
|
|
741
|
+
continuation:
|
|
742
|
+
start + documents.length < project.documents.length
|
|
743
|
+
? `s1.${project.snapshot}.${start + documents.length}`
|
|
744
|
+
: null,
|
|
745
|
+
documents,
|
|
746
|
+
origin,
|
|
747
|
+
snapshot: project.snapshot,
|
|
748
|
+
totalDocuments: project.documents.length,
|
|
749
|
+
warnings: project.warnings,
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
const selectedDocuments = project.documents.slice(
|
|
552
753
|
start,
|
|
553
|
-
start + Math.min(options.limit,
|
|
554
|
-
)
|
|
754
|
+
start + Math.min(options.limit, maxDocuments)
|
|
755
|
+
);
|
|
756
|
+
for (const document of selectedDocuments) {
|
|
555
757
|
documents.push(metadata(document));
|
|
556
|
-
if (Buffer.byteLength(JSON.stringify(response()))
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
758
|
+
if (Buffer.byteLength(JSON.stringify(response())) > options.maxBytes) {
|
|
759
|
+
documents.pop();
|
|
760
|
+
if (!documents.length) {
|
|
761
|
+
fail(
|
|
762
|
+
'OUTPUT_LIMIT',
|
|
763
|
+
'The next source metadata does not fit; increase --max-bytes or narrow the selected sources'
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
564
768
|
}
|
|
565
|
-
if (Buffer.byteLength(JSON.stringify(response())) > options.maxBytes)
|
|
769
|
+
if (Buffer.byteLength(JSON.stringify(response())) > options.maxBytes) {
|
|
566
770
|
fail('OUTPUT_LIMIT', 'Source-list metadata exceeds --max-bytes');
|
|
771
|
+
}
|
|
567
772
|
return response();
|
|
568
|
-
}
|
|
773
|
+
};
|
|
569
774
|
|
|
570
|
-
export function documentCommand(
|
|
571
|
-
const { command, id, options } = commandOptions(
|
|
775
|
+
export const documentCommand = function documentCommand(commandArguments: string[]): unknown {
|
|
776
|
+
const { command, id, options } = commandOptions(commandArguments);
|
|
572
777
|
const project = loadProject(options.root);
|
|
573
|
-
if (command === 'sources')
|
|
778
|
+
if (command === 'sources') {
|
|
779
|
+
return listSources(project, options);
|
|
780
|
+
}
|
|
574
781
|
return readCommand(project, id ?? '', options);
|
|
575
|
-
}
|
|
782
|
+
};
|