@h1v35/hivex 0.2.0 → 0.2.2
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 +55 -163
- package/docs/CONTEXT.md +20 -36
- package/docs/README.md +6 -12
- 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 +16 -43
- package/docs/guidelines/engineering.md +74 -0
- package/docs/procedures/self-hosted-runner.md +7 -0
- package/package.json +32 -11
- package/skills/hivex/SKILL.md +28 -92
- package/skills/hivex/references/markdown.md +12 -42
- package/src/cli/diagnostic.ts +21 -11
- package/src/cli.ts +46 -36
- package/src/documents.ts +502 -320
- 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 -268
- package/src/knowledge-serialization.ts +239 -0
- package/src/knowledge-snapshot.ts +100 -77
- package/src/knowledge-store.ts +634 -453
- package/src/knowledge.ts +1001 -758
- 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 +82 -43
- package/src/source-relocation.ts +222 -0
- package/docs/engineering.md +0 -174
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Field order is part of the persisted v1 cache format. Keep model inputs stable
|
|
2
|
+
// when source formatting changes; unknown future fields remain part of the input.
|
|
3
|
+
const fieldOrder = {
|
|
4
|
+
citation: ['document', 'lineStart', 'lineEnd', 'version', 'historical', 'text'],
|
|
5
|
+
code: ['path', 'side', 'lineStart', 'lineEnd', 'version', 'text'],
|
|
6
|
+
context: [
|
|
7
|
+
'command',
|
|
8
|
+
'snapshot',
|
|
9
|
+
'documents',
|
|
10
|
+
'unavailableDocuments',
|
|
11
|
+
'unexpandedDecisions',
|
|
12
|
+
'decisions',
|
|
13
|
+
'relationships',
|
|
14
|
+
'pendingDocuments',
|
|
15
|
+
'warnings',
|
|
16
|
+
],
|
|
17
|
+
decision: [
|
|
18
|
+
'id',
|
|
19
|
+
'document',
|
|
20
|
+
'text',
|
|
21
|
+
'kind',
|
|
22
|
+
'status',
|
|
23
|
+
'conditions',
|
|
24
|
+
'exceptions',
|
|
25
|
+
'reason',
|
|
26
|
+
'lineStart',
|
|
27
|
+
'lineEnd',
|
|
28
|
+
'version',
|
|
29
|
+
'batch',
|
|
30
|
+
'localId',
|
|
31
|
+
'quality',
|
|
32
|
+
],
|
|
33
|
+
document: ['id', 'title', 'status', 'historical', 'version', 'lineCount', 'lines'],
|
|
34
|
+
extraction: [
|
|
35
|
+
'operation',
|
|
36
|
+
'targets',
|
|
37
|
+
'repairReason',
|
|
38
|
+
'units',
|
|
39
|
+
'documents',
|
|
40
|
+
'existing',
|
|
41
|
+
'previousRelationships',
|
|
42
|
+
'scope',
|
|
43
|
+
'extraction',
|
|
44
|
+
],
|
|
45
|
+
file: ['path', 'before', 'after'],
|
|
46
|
+
finding: ['target', 'reason'],
|
|
47
|
+
implementation: ['baseCommit', 'diff', 'files', 'warnings', 'fingerprint'],
|
|
48
|
+
packet: [
|
|
49
|
+
'operation',
|
|
50
|
+
'implementation',
|
|
51
|
+
'task',
|
|
52
|
+
'context',
|
|
53
|
+
'documents',
|
|
54
|
+
'omittedUnits',
|
|
55
|
+
'warnings',
|
|
56
|
+
],
|
|
57
|
+
queriedDecision: [
|
|
58
|
+
'historical',
|
|
59
|
+
'id',
|
|
60
|
+
'document',
|
|
61
|
+
'version',
|
|
62
|
+
'text',
|
|
63
|
+
'kind',
|
|
64
|
+
'status',
|
|
65
|
+
'quality',
|
|
66
|
+
'conditions',
|
|
67
|
+
'exceptions',
|
|
68
|
+
'reason',
|
|
69
|
+
'evidence',
|
|
70
|
+
],
|
|
71
|
+
relationship: ['id', 'from', 'to', 'type', 'reason', 'evidence', 'batch', 'localId', 'quality'],
|
|
72
|
+
reviewFinding: ['assessment', 'explanation', 'documents', 'code'],
|
|
73
|
+
unit: ['id', 'document', 'hash', 'lineStart', 'lineEnd', 'text'],
|
|
74
|
+
version: ['version', 'lines'],
|
|
75
|
+
warning: ['path', 'message'],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const isRecord = function isRecord(value: unknown): value is Record<string, unknown> {
|
|
79
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
80
|
+
};
|
|
81
|
+
const hasFields = function hasFields(value: Record<string, unknown>, fields: string[]) {
|
|
82
|
+
return fields.every((field) => Object.hasOwn(value, field));
|
|
83
|
+
};
|
|
84
|
+
const orderRecord = function orderRecord(value: Record<string, unknown>, order: string[]) {
|
|
85
|
+
const existing = Object.keys(value);
|
|
86
|
+
const names = new Set(Iterator.concat(order, existing));
|
|
87
|
+
return Object.fromEntries(
|
|
88
|
+
names
|
|
89
|
+
.values()
|
|
90
|
+
.filter((name) => existing.includes(name))
|
|
91
|
+
.map<[string, unknown]>((name) => [name, value[name]])
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const hasOrderPrefix = function hasOrderPrefix(value: Record<string, unknown>, fields: string[]) {
|
|
96
|
+
const keys = Object.keys(value);
|
|
97
|
+
return fields.every((field, index) => keys[index] === field);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// New graph records must retain the original live provenance order between
|
|
101
|
+
// rounds. Decoding a stored graph has its own schema order.
|
|
102
|
+
export const withLiveProvenance = function withLiveProvenance<T extends object>(
|
|
103
|
+
value: T,
|
|
104
|
+
kind: 'decision' | 'relationship'
|
|
105
|
+
): T {
|
|
106
|
+
const record = { ...value };
|
|
107
|
+
const fields =
|
|
108
|
+
kind === 'decision'
|
|
109
|
+
? [...fieldOrder.decision.slice(0, 10), 'localId', 'version', 'batch', 'quality']
|
|
110
|
+
: [...fieldOrder.relationship.slice(0, 6), 'localId', 'batch', 'quality'];
|
|
111
|
+
for (const field of fields) {
|
|
112
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, field);
|
|
113
|
+
if (descriptor !== undefined) {
|
|
114
|
+
Reflect.deleteProperty(record, field);
|
|
115
|
+
Object.defineProperty(record, field, descriptor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return record;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const sourceOrder = function sourceOrder(value: Record<string, unknown>) {
|
|
122
|
+
if (hasFields(value, ['kind', 'id'])) {
|
|
123
|
+
if (Object.hasOwn(value, 'historical')) {
|
|
124
|
+
return fieldOrder.queriedDecision;
|
|
125
|
+
}
|
|
126
|
+
// Retained packets may contain live graph records, whose provenance fields
|
|
127
|
+
// were appended in a different order from records decoded by the store.
|
|
128
|
+
return hasOrderPrefix(value, fieldOrder.decision.slice(0, 10)) ? null : fieldOrder.decision;
|
|
129
|
+
}
|
|
130
|
+
if (hasFields(value, ['from', 'to', 'type'])) {
|
|
131
|
+
return hasOrderPrefix(value, fieldOrder.relationship.slice(0, 6))
|
|
132
|
+
? null
|
|
133
|
+
: fieldOrder.relationship;
|
|
134
|
+
}
|
|
135
|
+
if (hasFields(value, ['id', 'title'])) {
|
|
136
|
+
return fieldOrder.document;
|
|
137
|
+
}
|
|
138
|
+
if (hasFields(value, ['document', 'lineStart', 'lineEnd'])) {
|
|
139
|
+
return Object.hasOwn(value, 'id') ? fieldOrder.unit : fieldOrder.citation;
|
|
140
|
+
}
|
|
141
|
+
if (hasFields(value, ['path', 'side'])) {
|
|
142
|
+
return fieldOrder.code;
|
|
143
|
+
}
|
|
144
|
+
if (hasFields(value, ['version', 'lines'])) {
|
|
145
|
+
return fieldOrder.version;
|
|
146
|
+
}
|
|
147
|
+
if (hasFields(value, ['path', 'message'])) {
|
|
148
|
+
return fieldOrder.warning;
|
|
149
|
+
}
|
|
150
|
+
// No source shape matched. Null above preserves a recognized record's live order.
|
|
151
|
+
return [];
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const packetOrder = function packetOrder(key: string, value: Record<string, unknown>) {
|
|
155
|
+
if (key === '' && typeof value.operation === 'string') {
|
|
156
|
+
const isExtraction = value.operation === 'extract' || value.operation === 'check';
|
|
157
|
+
return isExtraction ? fieldOrder.extraction : fieldOrder.packet;
|
|
158
|
+
}
|
|
159
|
+
if (key === 'context' && hasFields(value, ['command', 'snapshot'])) {
|
|
160
|
+
return fieldOrder.context;
|
|
161
|
+
}
|
|
162
|
+
if (hasFields(value, ['baseCommit', 'files'])) {
|
|
163
|
+
return fieldOrder.implementation;
|
|
164
|
+
}
|
|
165
|
+
if (hasFields(value, ['path', 'before', 'after'])) {
|
|
166
|
+
return fieldOrder.file;
|
|
167
|
+
}
|
|
168
|
+
const order = sourceOrder(value);
|
|
169
|
+
if (order === null || order.length > 0) {
|
|
170
|
+
return order;
|
|
171
|
+
}
|
|
172
|
+
return hasFields(value, ['target', 'reason']) ? fieldOrder.finding : null;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const stringifyKnowledge = function stringifyKnowledge(packet: unknown) {
|
|
176
|
+
return JSON.stringify(packet, (key: string, value: unknown) => {
|
|
177
|
+
if (!isRecord(value)) {
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
const order = packetOrder(key, value);
|
|
181
|
+
return order === null ? value : orderRecord(value, order);
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const schemaOrder = function schemaOrder(properties: Record<string, unknown>) {
|
|
186
|
+
if (hasFields(properties, ['kind', 'id'])) {
|
|
187
|
+
return fieldOrder.decision;
|
|
188
|
+
}
|
|
189
|
+
if (hasFields(properties, ['from', 'to', 'type'])) {
|
|
190
|
+
return fieldOrder.relationship;
|
|
191
|
+
}
|
|
192
|
+
if (hasFields(properties, ['document', 'lineStart', 'lineEnd'])) {
|
|
193
|
+
return fieldOrder.citation;
|
|
194
|
+
}
|
|
195
|
+
if (hasFields(properties, ['path', 'side'])) {
|
|
196
|
+
return fieldOrder.code;
|
|
197
|
+
}
|
|
198
|
+
if (hasFields(properties, ['assessment', 'explanation'])) {
|
|
199
|
+
return fieldOrder.reviewFinding;
|
|
200
|
+
}
|
|
201
|
+
if (hasFields(properties, ['target', 'reason'])) {
|
|
202
|
+
return fieldOrder.finding;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const schemaNode = function schemaNode(value: unknown): unknown {
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
return value.map(schemaNode);
|
|
210
|
+
}
|
|
211
|
+
if (!isRecord(value)) {
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
const result = Object.fromEntries(
|
|
215
|
+
Object.entries(value).map(([key, node]) => [key, schemaNode(node)])
|
|
216
|
+
);
|
|
217
|
+
if (!isRecord(result.properties)) {
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
const order = schemaOrder(result.properties);
|
|
221
|
+
if (order === null) {
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
result.properties = orderRecord(result.properties, order);
|
|
225
|
+
if (Array.isArray(result.required)) {
|
|
226
|
+
const { required } = result;
|
|
227
|
+
const names = new Set(Iterator.concat(order, required));
|
|
228
|
+
result.required = [...names].filter((name) => required.includes(name));
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const schemaForKnowledge = function schemaForKnowledge(schema: Record<string, unknown>) {
|
|
234
|
+
const result = schemaNode(schema);
|
|
235
|
+
if (!isRecord(result)) {
|
|
236
|
+
throw new Error('Expected a JSON schema object');
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
};
|
|
@@ -1,140 +1,163 @@
|
|
|
1
1
|
import { lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { compareSerializedStrings } from './ordering.ts';
|
|
4
5
|
import { HivexError } from './errors.ts';
|
|
5
6
|
import { isMarkdownPath } from './markdown.ts';
|
|
6
|
-
import { emptyGraph, graphSchema
|
|
7
|
+
import { emptyGraph, graphSchema } from './knowledge-model.ts';
|
|
8
|
+
import type { Graph } from './knowledge-model.ts';
|
|
7
9
|
|
|
8
|
-
const
|
|
10
|
+
const maxBytes = 64 * 1024 * 1024;
|
|
9
11
|
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
!
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
18
|
-
}
|
|
12
|
+
const isPortableDocument = function isPortableDocument(id: string) {
|
|
13
|
+
const isProjectRelative = !id.startsWith('/') && !id.includes('\\') && !id.includes('\0');
|
|
14
|
+
const hasSafeParts = id
|
|
15
|
+
.split('/')
|
|
16
|
+
.every((part) => !['..', '.', '.git', '.hivex', 'node_modules', ''].includes(part));
|
|
17
|
+
return isMarkdownPath(id) && isProjectRelative && hasSafeParts;
|
|
18
|
+
};
|
|
19
19
|
|
|
20
|
-
function validateGraph(graph: Graph) {
|
|
20
|
+
const validateGraph = function validateGraph(graph: Graph) {
|
|
21
21
|
const decisions = new Set(graph.decisions.map((entry) => entry.id));
|
|
22
22
|
const relationships = new Set(graph.relationships.map((entry) => entry.id));
|
|
23
|
-
const
|
|
23
|
+
const hasInvalidDecision = graph.decisions.some((entry) => {
|
|
24
|
+
if (entry.quality !== 'checked') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return entry.lineStart > entry.lineEnd;
|
|
28
|
+
});
|
|
29
|
+
const hasInvalidRelationship = graph.relationships.some((edge) => {
|
|
30
|
+
if (!decisions.has(edge.from) || !decisions.has(edge.to)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (edge.evidence.length === 0) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return edge.evidence.some((entry) => entry.lineStart > entry.lineEnd);
|
|
37
|
+
});
|
|
38
|
+
const isInvalid =
|
|
24
39
|
decisions.size !== graph.decisions.length ||
|
|
25
40
|
relationships.size !== graph.relationships.length ||
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
) ||
|
|
29
|
-
graph.relationships.some(
|
|
30
|
-
(edge) =>
|
|
31
|
-
!decisions.has(edge.from) ||
|
|
32
|
-
!decisions.has(edge.to) ||
|
|
33
|
-
!edge.evidence.length ||
|
|
34
|
-
edge.evidence.some((entry) => entry.lineStart > entry.lineEnd),
|
|
35
|
-
);
|
|
41
|
+
hasInvalidDecision ||
|
|
42
|
+
hasInvalidRelationship;
|
|
36
43
|
const references = [
|
|
37
44
|
...Object.keys(graph.documents),
|
|
38
45
|
...Object.values(graph.units).map((unit) => unit.document),
|
|
39
46
|
...graph.decisions.map((entry) => entry.document),
|
|
40
47
|
...graph.relationships.flatMap((edge) => edge.evidence.map((entry) => entry.document)),
|
|
41
|
-
...graph.warnings.flatMap((warning) =>
|
|
42
|
-
typeof warning === 'string'
|
|
43
|
-
|
|
48
|
+
...graph.warnings.flatMap((warning) => {
|
|
49
|
+
if (typeof warning === 'string') {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return warning.scope.map((entry) => entry.document);
|
|
53
|
+
}),
|
|
44
54
|
];
|
|
45
|
-
if (
|
|
55
|
+
if (isInvalid || references.some((id) => !isPortableDocument(id))) {
|
|
46
56
|
throw new HivexError({
|
|
47
57
|
code: 'INVALID_SNAPSHOT',
|
|
48
58
|
message:
|
|
49
59
|
'Knowledge snapshot has invalid identities, relationships or project-relative sources.',
|
|
50
60
|
});
|
|
61
|
+
}
|
|
51
62
|
return graph;
|
|
52
|
-
}
|
|
63
|
+
};
|
|
53
64
|
|
|
54
|
-
function snapshotPath(root: string) {
|
|
55
|
-
const directory = join(root, '.hivex');
|
|
56
|
-
const
|
|
57
|
-
for (const candidate of [directory,
|
|
65
|
+
const snapshotPath = function snapshotPath(root: string) {
|
|
66
|
+
const directory = path.join(root, '.hivex');
|
|
67
|
+
const snapshotFile = path.join(directory, 'graph.json');
|
|
68
|
+
for (const candidate of [directory, snapshotFile]) {
|
|
58
69
|
const stat = lstatSync(candidate, { throwIfNoEntry: false });
|
|
59
70
|
if (
|
|
60
|
-
stat?.isSymbolicLink() ||
|
|
71
|
+
stat?.isSymbolicLink() === true ||
|
|
61
72
|
(stat && !(candidate === directory ? stat.isDirectory() : stat.isFile()))
|
|
62
|
-
)
|
|
73
|
+
) {
|
|
63
74
|
throw new HivexError({
|
|
64
75
|
code: 'INVALID_SNAPSHOT',
|
|
65
76
|
message: 'Knowledge snapshot must use regular project-local files.',
|
|
66
77
|
});
|
|
78
|
+
}
|
|
67
79
|
}
|
|
68
|
-
return
|
|
69
|
-
}
|
|
80
|
+
return snapshotFile;
|
|
81
|
+
};
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
const parseSnapshot = function parseSnapshot(bytes: Uint8Array) {
|
|
84
|
+
if (bytes.length > maxBytes) {
|
|
85
|
+
throw new Error('Snapshot exceeds size limit');
|
|
86
|
+
}
|
|
87
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
88
|
+
const data: unknown = JSON.parse(decoder.decode(bytes));
|
|
89
|
+
return validateGraph(graphSchema.strict().parse(data));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const readKnowledgeSnapshot = function readKnowledgeSnapshot(root: string): Graph | null {
|
|
93
|
+
const snapshotFile = snapshotPath(root);
|
|
94
|
+
const stat = lstatSync(snapshotFile, { throwIfNoEntry: false });
|
|
95
|
+
if (stat === undefined) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (stat.size > maxBytes) {
|
|
76
99
|
throw new HivexError({
|
|
77
100
|
code: 'INVALID_SNAPSHOT',
|
|
78
101
|
message: 'Knowledge snapshot exceeds 64 MiB.',
|
|
79
102
|
});
|
|
103
|
+
}
|
|
80
104
|
try {
|
|
81
|
-
|
|
82
|
-
if (bytes.length > MAX_BYTES) throw new Error('Snapshot exceeds size limit');
|
|
83
|
-
const data: unknown = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
84
|
-
return validateGraph(graphSchema.strict().parse(data));
|
|
105
|
+
return parseSnapshot(readFileSync(snapshotFile));
|
|
85
106
|
} catch {
|
|
86
107
|
throw new HivexError({
|
|
87
108
|
code: 'INVALID_SNAPSHOT',
|
|
88
109
|
message: 'Knowledge snapshot is not a supported graph JSON document.',
|
|
89
110
|
});
|
|
90
111
|
}
|
|
91
|
-
}
|
|
112
|
+
};
|
|
92
113
|
|
|
93
|
-
function ordered(value: unknown): unknown {
|
|
94
|
-
if (Array.isArray(value))
|
|
95
|
-
|
|
114
|
+
const ordered = function ordered(value: unknown): unknown {
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
return value.map(ordered);
|
|
117
|
+
}
|
|
118
|
+
if (value !== null && typeof value === 'object') {
|
|
96
119
|
return Object.fromEntries(
|
|
97
120
|
Object.entries(value)
|
|
98
|
-
.
|
|
99
|
-
.map(([key, entry]) => [key, ordered(entry)])
|
|
121
|
+
.toSorted(([a], [b]) => compareSerializedStrings(a, b))
|
|
122
|
+
.map(([key, entry]) => [key, ordered(entry)])
|
|
100
123
|
);
|
|
124
|
+
}
|
|
101
125
|
return value;
|
|
102
|
-
}
|
|
126
|
+
};
|
|
103
127
|
|
|
104
|
-
function byId(a: { id: string }, b: { id: string }) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
128
|
+
const byId = function byId(a: { id: string }, b: { id: string }) {
|
|
129
|
+
return compareSerializedStrings(a.id, b.id);
|
|
130
|
+
};
|
|
108
131
|
|
|
109
|
-
export function writeKnowledgeSnapshot(root: string, value: Graph) {
|
|
110
|
-
const
|
|
132
|
+
export const writeKnowledgeSnapshot = function writeKnowledgeSnapshot(root: string, value: Graph) {
|
|
133
|
+
const snapshotFile = snapshotPath(root);
|
|
111
134
|
const graph = validateGraph(graphSchema.parse(value));
|
|
112
|
-
const text =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (Buffer.byteLength(text) > MAX_BYTES)
|
|
135
|
+
const text = `${JSON.stringify(
|
|
136
|
+
ordered({
|
|
137
|
+
...graph,
|
|
138
|
+
decisions: graph.decisions.toSorted(byId),
|
|
139
|
+
relationships: graph.relationships.toSorted(byId),
|
|
140
|
+
}),
|
|
141
|
+
null,
|
|
142
|
+
2
|
|
143
|
+
)}\n`;
|
|
144
|
+
if (Buffer.byteLength(text) > maxBytes) {
|
|
123
145
|
throw new HivexError({
|
|
124
146
|
code: 'INVALID_SNAPSHOT',
|
|
125
147
|
message: 'Knowledge snapshot exceeds 64 MiB.',
|
|
126
148
|
});
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
}
|
|
150
|
+
mkdirSync(path.join(root, '.hivex'), { mode: 0o700, recursive: true });
|
|
151
|
+
const temporary = path.join(root, '.hivex', `graph-${randomUUID()}.tmp`);
|
|
129
152
|
try {
|
|
130
153
|
writeFileSync(temporary, text, { flag: 'wx', mode: 0o600 });
|
|
131
|
-
renameSync(temporary,
|
|
154
|
+
renameSync(temporary, snapshotFile);
|
|
132
155
|
} finally {
|
|
133
156
|
rmSync(temporary, { force: true });
|
|
134
157
|
}
|
|
135
|
-
return
|
|
136
|
-
}
|
|
158
|
+
return snapshotFile;
|
|
159
|
+
};
|
|
137
160
|
|
|
138
|
-
export function sharedKnowledge(root: string) {
|
|
161
|
+
export const sharedKnowledge = function sharedKnowledge(root: string) {
|
|
139
162
|
return readKnowledgeSnapshot(root) ?? emptyGraph();
|
|
140
|
-
}
|
|
163
|
+
};
|