@amalgm/shell 0.1.44 → 0.1.46
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/PURPOSE.md +16 -13
- package/dist/content-cache-host.d.ts +3 -0
- package/dist/content-cache-host.js +21 -3
- package/dist/content-cache-host.js.map +1 -1
- package/dist/detection/journal-codec.d.ts +3 -0
- package/dist/detection/journal-codec.js +34 -0
- package/dist/detection/journal-codec.js.map +1 -0
- package/dist/{detection-host.d.ts → detection/portable.d.ts} +19 -1
- package/dist/{detection-host.js → detection/portable.js} +32 -4
- package/dist/detection/portable.js.map +1 -0
- package/dist/detection/runtime.d.ts +67 -0
- package/dist/detection/runtime.js +919 -0
- package/dist/detection/runtime.js.map +1 -0
- package/dist/user-ground-host.d.ts +12 -2
- package/dist/user-ground-host.js +428 -169
- package/dist/user-ground-host.js.map +1 -1
- package/package.json +3 -3
- package/dist/detection-host.js.map +0 -1
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The named Detect lane.
|
|
3
|
+
*
|
|
4
|
+
* A watcher path that still names enrolled, non-repository ground does not
|
|
5
|
+
* justify a workspace walk. This runtime holds one SQLite connection,
|
|
6
|
+
* resolves indexed paths, widens only to a named new folder's subtree, and
|
|
7
|
+
* commits exact Live proposals plus local truth in one transaction. Evidence
|
|
8
|
+
* it cannot prove is declined before mutation and goes to reconciliation in
|
|
9
|
+
* the owning host.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
|
|
13
|
+
import { join, posix, resolve, sep } from "node:path";
|
|
14
|
+
import { canonicalVersion, checkContentManifest, classifyFile, createUserGroundEnrollmentPolicy, detectedChangeProposal, membershipHash, } from "@amalgm/live";
|
|
15
|
+
import Database from "better-sqlite3";
|
|
16
|
+
import { captureContentFile, sealCapturedArtifact } from "../content-cache-host.js";
|
|
17
|
+
import { captureRepository, hasGitMarker, inspectRepositoryTransportFile, } from "../git-repository-host.js";
|
|
18
|
+
import { exactTextReplay } from "./portable.js";
|
|
19
|
+
import { encodeMutationOperation } from "./journal-codec.js";
|
|
20
|
+
const ROW_SELECT = `
|
|
21
|
+
SELECT resource_id AS resourceId, root_uuid AS rootUUID,
|
|
22
|
+
uuid, type, parent_uuid AS parentUUID, name, status, version,
|
|
23
|
+
payload_version AS payloadVersion, transport_version AS transportVersion,
|
|
24
|
+
relative_path AS relativePath, absolute_path AS absolutePath,
|
|
25
|
+
device_number AS deviceNumber, inode,
|
|
26
|
+
byte_size AS byteSize, modified_time_ms AS modifiedTimeMs,
|
|
27
|
+
changed_time_ms AS changedTimeMs, filesystem_mode AS filesystemMode,
|
|
28
|
+
content_verified_at_ms AS contentVerifiedAtMs
|
|
29
|
+
FROM detection_notebook`;
|
|
30
|
+
const ROW_COLUMNS = [
|
|
31
|
+
"uuid", "resource_id", "root_uuid", "type", "parent_uuid", "name", "status", "version",
|
|
32
|
+
"payload_version", "transport_version", "relative_path", "absolute_path", "device_number",
|
|
33
|
+
"inode", "byte_size", "modified_time_ms", "changed_time_ms", "filesystem_mode",
|
|
34
|
+
"content_verified_at_ms",
|
|
35
|
+
].join(", ");
|
|
36
|
+
const rowValues = (row) => [
|
|
37
|
+
row.uuid, row.resourceId, row.rootUUID, row.type, row.parentUUID, row.name, row.status,
|
|
38
|
+
row.version, row.payloadVersion, row.transportVersion, row.relativePath, row.absolutePath,
|
|
39
|
+
row.deviceNumber, row.inode, row.byteSize, row.modifiedTimeMs, row.changedTimeMs,
|
|
40
|
+
row.filesystemMode, row.contentVerifiedAtMs,
|
|
41
|
+
];
|
|
42
|
+
const upsert = (database, table) => database.prepare(`
|
|
43
|
+
INSERT INTO ${table}(${ROW_COLUMNS})
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
45
|
+
ON CONFLICT(uuid) DO UPDATE SET
|
|
46
|
+
resource_id = excluded.resource_id,
|
|
47
|
+
root_uuid = excluded.root_uuid,
|
|
48
|
+
type = excluded.type,
|
|
49
|
+
parent_uuid = excluded.parent_uuid,
|
|
50
|
+
name = excluded.name,
|
|
51
|
+
status = excluded.status,
|
|
52
|
+
version = excluded.version,
|
|
53
|
+
payload_version = excluded.payload_version,
|
|
54
|
+
transport_version = excluded.transport_version,
|
|
55
|
+
relative_path = excluded.relative_path,
|
|
56
|
+
absolute_path = excluded.absolute_path,
|
|
57
|
+
device_number = excluded.device_number,
|
|
58
|
+
inode = excluded.inode,
|
|
59
|
+
byte_size = excluded.byte_size,
|
|
60
|
+
modified_time_ms = excluded.modified_time_ms,
|
|
61
|
+
changed_time_ms = excluded.changed_time_ms,
|
|
62
|
+
filesystem_mode = excluded.filesystem_mode,
|
|
63
|
+
content_verified_at_ms = excluded.content_verified_at_ms
|
|
64
|
+
`);
|
|
65
|
+
const portable = (row) => ({
|
|
66
|
+
uuid: row.uuid,
|
|
67
|
+
type: row.type,
|
|
68
|
+
parentUUID: row.parentUUID,
|
|
69
|
+
name: row.name,
|
|
70
|
+
status: row.status,
|
|
71
|
+
version: row.version,
|
|
72
|
+
payloadVersion: row.payloadVersion,
|
|
73
|
+
transportVersion: row.transportVersion,
|
|
74
|
+
});
|
|
75
|
+
const sha256Hex = (input) => createHash("sha256").update(input).digest("hex");
|
|
76
|
+
const sameFingerprint = (row, stats) => row.deviceNumber === stats.dev
|
|
77
|
+
&& row.inode === stats.ino
|
|
78
|
+
&& row.byteSize === stats.size
|
|
79
|
+
&& row.modifiedTimeMs === stats.mtimeMs
|
|
80
|
+
&& row.changedTimeMs === stats.ctimeMs
|
|
81
|
+
&& row.filesystemMode === stats.mode;
|
|
82
|
+
const journalJson = encodeMutationOperation;
|
|
83
|
+
/** One persistent, prepared Detect store. It owns no Watch or Send behavior. */
|
|
84
|
+
export class NamedDetectRuntime {
|
|
85
|
+
database;
|
|
86
|
+
cacheDir;
|
|
87
|
+
userRoot;
|
|
88
|
+
rootAtDirectory;
|
|
89
|
+
rowAtPath;
|
|
90
|
+
rowAtUuid;
|
|
91
|
+
rowsUnderPath;
|
|
92
|
+
rowsAtPhysicalIdentity;
|
|
93
|
+
notebookChildren;
|
|
94
|
+
materializedByUuid;
|
|
95
|
+
materializedUnderPath;
|
|
96
|
+
writeEntity;
|
|
97
|
+
writeNotebook;
|
|
98
|
+
appendMutation;
|
|
99
|
+
removeEntity;
|
|
100
|
+
userPolicy;
|
|
101
|
+
constructor(databaseFile, cacheDir, userRoot) {
|
|
102
|
+
this.database = new Database(databaseFile);
|
|
103
|
+
this.database.pragma("journal_mode = WAL");
|
|
104
|
+
this.database.pragma("synchronous = FULL");
|
|
105
|
+
this.cacheDir = cacheDir;
|
|
106
|
+
this.userRoot = resolve(userRoot);
|
|
107
|
+
this.userPolicy = this.readUserPolicy();
|
|
108
|
+
this.rootAtDirectory = this.database.prepare(`${ROW_SELECT} WHERE absolute_path = ? AND relative_path = '' AND status = 'active' LIMIT 1`);
|
|
109
|
+
this.rowAtPath = this.database.prepare(`${ROW_SELECT} WHERE root_uuid = ? AND relative_path = ? AND status = 'active' LIMIT 1`);
|
|
110
|
+
this.rowAtUuid = this.database.prepare(`${ROW_SELECT} WHERE uuid = ? LIMIT 1`);
|
|
111
|
+
this.rowsUnderPath = this.database.prepare(`${ROW_SELECT}
|
|
112
|
+
WHERE root_uuid = ? AND status = 'active'
|
|
113
|
+
AND (? = '' OR relative_path = ?
|
|
114
|
+
OR substr(relative_path, 1, length(?) + 1) = ? || '/')
|
|
115
|
+
ORDER BY relative_path`);
|
|
116
|
+
this.rowsAtPhysicalIdentity = this.database.prepare(`${ROW_SELECT} WHERE device_number = ? AND inode = ? AND status = 'active' ORDER BY uuid`);
|
|
117
|
+
this.notebookChildren = this.database.prepare(`
|
|
118
|
+
SELECT uuid, name FROM detection_notebook
|
|
119
|
+
WHERE parent_uuid = ? AND status = 'active' ORDER BY name, uuid
|
|
120
|
+
`);
|
|
121
|
+
this.materializedByUuid = this.database.prepare("SELECT 1 AS present FROM entities WHERE uuid = ?");
|
|
122
|
+
this.materializedUnderPath = this.database.prepare(`
|
|
123
|
+
SELECT uuid FROM entities
|
|
124
|
+
WHERE root_uuid = ?
|
|
125
|
+
AND (relative_path = ? OR substr(relative_path, 1, length(?) + 1) = ? || '/')
|
|
126
|
+
`);
|
|
127
|
+
this.writeEntity = upsert(this.database, "entities");
|
|
128
|
+
this.writeNotebook = upsert(this.database, "detection_notebook");
|
|
129
|
+
this.appendMutation = this.database.prepare(`
|
|
130
|
+
INSERT INTO mutation_journal(
|
|
131
|
+
mutation_id, resource_id, operation_kind, operation_json, created_at
|
|
132
|
+
) VALUES (?, ?, 'detected.change', ?, ?)
|
|
133
|
+
`);
|
|
134
|
+
this.removeEntity = this.database.prepare("DELETE FROM entities WHERE uuid = ?");
|
|
135
|
+
}
|
|
136
|
+
close() {
|
|
137
|
+
this.database.close();
|
|
138
|
+
}
|
|
139
|
+
refreshEnrollmentPolicy() {
|
|
140
|
+
this.userPolicy = this.readUserPolicy();
|
|
141
|
+
}
|
|
142
|
+
async detect(observations) {
|
|
143
|
+
if (observations.some(({ suspicion }) => suspicion.paths === null)) {
|
|
144
|
+
return { handled: false, evidence: [] };
|
|
145
|
+
}
|
|
146
|
+
const repository = await this.detectRepository(observations);
|
|
147
|
+
if (repository)
|
|
148
|
+
return repository;
|
|
149
|
+
const captured = [];
|
|
150
|
+
const materializedRemovals = new Set();
|
|
151
|
+
const membershipParents = new Set();
|
|
152
|
+
const evidence = [];
|
|
153
|
+
const prepared = [];
|
|
154
|
+
const moveSources = [];
|
|
155
|
+
let repositoryBoundary = false;
|
|
156
|
+
for (const observation of observations) {
|
|
157
|
+
const paths = observation.suspicion.paths;
|
|
158
|
+
if (paths.length === 0)
|
|
159
|
+
continue;
|
|
160
|
+
if (paths.some((path) => path === ".amalgmignore"
|
|
161
|
+
|| path === ".git" || path.startsWith(".git/") || path.includes("/.git/")
|
|
162
|
+
|| path.endsWith("/.git"))) {
|
|
163
|
+
return { handled: false, evidence: [] };
|
|
164
|
+
}
|
|
165
|
+
const root = this.rootAtDirectory.get(observation.directory);
|
|
166
|
+
if (!root)
|
|
167
|
+
return { handled: false, evidence: [] };
|
|
168
|
+
const started = performance.now();
|
|
169
|
+
const scopedPaths = paths.filter((path) => !paths.some((candidate) => candidate !== path && path.startsWith(`${candidate}/`)));
|
|
170
|
+
const policy = resolve(observation.directory) === this.userRoot
|
|
171
|
+
? this.userPolicy
|
|
172
|
+
: () => true;
|
|
173
|
+
const entries = scopedPaths.filter(policy).map((path) => {
|
|
174
|
+
const exact = this.rowAtPath.get(root.rootUUID, path);
|
|
175
|
+
const absolutePath = join(observation.directory, ...path.split("/"));
|
|
176
|
+
const stats = tryLstat(absolutePath);
|
|
177
|
+
if (!stats && exact) {
|
|
178
|
+
if (this.enclosedByRepository(exact)) {
|
|
179
|
+
repositoryBoundary = true;
|
|
180
|
+
}
|
|
181
|
+
moveSources.push(absolutePath);
|
|
182
|
+
}
|
|
183
|
+
return { path, absolutePath, exact, stats };
|
|
184
|
+
});
|
|
185
|
+
if (repositoryBoundary)
|
|
186
|
+
return { handled: false, evidence: [] };
|
|
187
|
+
prepared.push({ observation, root, paths, policy, started, entries });
|
|
188
|
+
}
|
|
189
|
+
for (const item of prepared) {
|
|
190
|
+
const { observation, root, paths, policy, started, entries } = item;
|
|
191
|
+
let metadataReads = entries.length;
|
|
192
|
+
let contentReads = 0;
|
|
193
|
+
let journalBytes = 0;
|
|
194
|
+
const beforeCount = captured.length;
|
|
195
|
+
for (const { path, absolutePath, exact, stats } of entries) {
|
|
196
|
+
if (!stats) {
|
|
197
|
+
if (!exact)
|
|
198
|
+
continue;
|
|
199
|
+
const removals = ["workspace", "folder"].includes(exact.type)
|
|
200
|
+
? this.materializedUnderPath.all(root.rootUUID, path, path, path)
|
|
201
|
+
: [{ uuid: exact.uuid }];
|
|
202
|
+
for (const row of removals)
|
|
203
|
+
materializedRemovals.add(row.uuid);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const portableParent = posix.dirname(path);
|
|
207
|
+
const parentPath = portableParent === "." ? "" : portableParent;
|
|
208
|
+
const parent = this.rowAtPath.get(root.rootUUID, parentPath);
|
|
209
|
+
if (!parent || !["workspace", "folder"].includes(parent.type)
|
|
210
|
+
|| this.enclosedByRepository(parent)) {
|
|
211
|
+
return { handled: false, evidence: [] };
|
|
212
|
+
}
|
|
213
|
+
let scope;
|
|
214
|
+
if (stats.isFile()) {
|
|
215
|
+
if (exact && !["file.text", "file.binary"].includes(exact.type)) {
|
|
216
|
+
return { handled: false, evidence: [] };
|
|
217
|
+
}
|
|
218
|
+
const file = await this.captureFile({
|
|
219
|
+
root, path, absolutePath, stats, parent, exact, moveSources,
|
|
220
|
+
});
|
|
221
|
+
scope = {
|
|
222
|
+
items: file.item ? [file.item] : [],
|
|
223
|
+
metadataReads: 1,
|
|
224
|
+
contentReads: file.contentReads,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
else if (stats.isSymbolicLink()) {
|
|
228
|
+
if (exact?.type === "reference"
|
|
229
|
+
|| (resolve(observation.directory) === this.userRoot
|
|
230
|
+
&& (path === "workspaces" || path.startsWith("workspaces/")))) {
|
|
231
|
+
return { handled: false, evidence: [] };
|
|
232
|
+
}
|
|
233
|
+
const link = this.captureLink({
|
|
234
|
+
root, path, absolutePath, stats, parent, exact, moveSources,
|
|
235
|
+
});
|
|
236
|
+
if (!link)
|
|
237
|
+
return { handled: false, evidence: [] };
|
|
238
|
+
scope = {
|
|
239
|
+
items: link.item ? [link.item] : [],
|
|
240
|
+
metadataReads: 1,
|
|
241
|
+
contentReads: link.contentReads,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
else if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
245
|
+
const directory = await this.captureDirectory({
|
|
246
|
+
root,
|
|
247
|
+
path,
|
|
248
|
+
absolutePath,
|
|
249
|
+
stats,
|
|
250
|
+
parent,
|
|
251
|
+
policy,
|
|
252
|
+
moveSources,
|
|
253
|
+
});
|
|
254
|
+
if (!directory)
|
|
255
|
+
return { handled: false, evidence: [] };
|
|
256
|
+
scope = directory;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
return { handled: false, evidence: [] };
|
|
260
|
+
}
|
|
261
|
+
metadataReads += scope.metadataReads - 1;
|
|
262
|
+
contentReads += scope.contentReads;
|
|
263
|
+
for (const item of scope.items) {
|
|
264
|
+
if (item.proposal)
|
|
265
|
+
journalBytes += Buffer.byteLength(journalJson({ proposal: item.proposal }));
|
|
266
|
+
captured.push(item);
|
|
267
|
+
if (item.base === null
|
|
268
|
+
|| item.base.parentUUID !== item.result.parentUUID
|
|
269
|
+
|| item.base.name !== item.result.name) {
|
|
270
|
+
if (item.base?.parentUUID)
|
|
271
|
+
membershipParents.add(item.base.parentUUID);
|
|
272
|
+
if (item.result.parentUUID)
|
|
273
|
+
membershipParents.add(item.result.parentUUID);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
evidence.push({
|
|
278
|
+
directory: observation.directory,
|
|
279
|
+
paths: paths.length,
|
|
280
|
+
metadataReads,
|
|
281
|
+
contentReads,
|
|
282
|
+
gitInspections: 0,
|
|
283
|
+
repositoryCaptures: 0,
|
|
284
|
+
plannedRows: captured.length - beforeCount,
|
|
285
|
+
records: captured.slice(beforeCount).filter(({ proposal }) => proposal !== null).length,
|
|
286
|
+
journalBytes,
|
|
287
|
+
durationMs: performance.now() - started,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
this.database.transaction(() => {
|
|
291
|
+
for (const uuid of materializedRemovals)
|
|
292
|
+
this.removeEntity.run(uuid);
|
|
293
|
+
for (const item of captured) {
|
|
294
|
+
this.writeEntity.run(...rowValues(item.result));
|
|
295
|
+
this.writeNotebook.run(...rowValues(item.result));
|
|
296
|
+
if (!item.proposal)
|
|
297
|
+
continue;
|
|
298
|
+
const mutationId = randomUUID();
|
|
299
|
+
this.appendMutation.run(mutationId, item.proposal.resourceId, journalJson({ proposal: item.proposal }), new Date().toISOString());
|
|
300
|
+
}
|
|
301
|
+
for (const parentUUID of membershipParents) {
|
|
302
|
+
const base = this.rowAtUuid.get(parentUUID);
|
|
303
|
+
if (!base || !["workspace", "folder"].includes(base.type))
|
|
304
|
+
continue;
|
|
305
|
+
const children = this.notebookChildren.all(parentUUID);
|
|
306
|
+
const payloadVersion = membershipHash(children, sha256Hex);
|
|
307
|
+
const version = canonicalVersion({
|
|
308
|
+
type: base.type,
|
|
309
|
+
parentUUID: base.parentUUID,
|
|
310
|
+
name: base.name,
|
|
311
|
+
status: base.status,
|
|
312
|
+
payloadVersion,
|
|
313
|
+
}, sha256Hex);
|
|
314
|
+
if (version === base.version)
|
|
315
|
+
continue;
|
|
316
|
+
const result = { ...base, payloadVersion, version };
|
|
317
|
+
this.writeEntity.run(...rowValues(result));
|
|
318
|
+
this.writeNotebook.run(...rowValues(result));
|
|
319
|
+
const proposal = detectedChangeProposal({
|
|
320
|
+
base: portable(base),
|
|
321
|
+
result: portable(result),
|
|
322
|
+
replay: { kind: "structure" },
|
|
323
|
+
});
|
|
324
|
+
if (!proposal)
|
|
325
|
+
continue;
|
|
326
|
+
this.appendMutation.run(randomUUID(), proposal.resourceId, journalJson({ proposal }), new Date().toISOString());
|
|
327
|
+
}
|
|
328
|
+
})();
|
|
329
|
+
return { handled: true, evidence };
|
|
330
|
+
}
|
|
331
|
+
/** A known leaf edit inside one repository is still named evidence. It
|
|
332
|
+
* updates that leaf's local identity proof, then records one nearest-repo
|
|
333
|
+
* Card + Checkpoint transition. Ambiguous structure and rail changes widen
|
|
334
|
+
* to reconciliation before this method mutates SQLite. */
|
|
335
|
+
async detectRepository(observations) {
|
|
336
|
+
const active = observations.filter(({ suspicion }) => suspicion.paths.length > 0);
|
|
337
|
+
if (active.length === 0)
|
|
338
|
+
return null;
|
|
339
|
+
let repository = null;
|
|
340
|
+
let directory = "";
|
|
341
|
+
const leafRows = new Map();
|
|
342
|
+
let metadataReads = 0;
|
|
343
|
+
let contentReads = 0;
|
|
344
|
+
const started = performance.now();
|
|
345
|
+
for (const observation of active) {
|
|
346
|
+
const root = this.rootAtDirectory.get(observation.directory);
|
|
347
|
+
if (!root)
|
|
348
|
+
return null;
|
|
349
|
+
const paths = observation.suspicion.paths
|
|
350
|
+
.filter((path) => !observation.suspicion.paths?.some((candidate) => candidate !== path && path.startsWith(`${candidate}/`)));
|
|
351
|
+
for (const path of paths) {
|
|
352
|
+
const owner = this.repositoryAtPath(root.rootUUID, path);
|
|
353
|
+
if (!owner || (repository && owner.uuid !== repository.uuid))
|
|
354
|
+
return null;
|
|
355
|
+
repository = owner;
|
|
356
|
+
directory = observation.directory;
|
|
357
|
+
const marker = path.split("/").indexOf(".git");
|
|
358
|
+
if (marker >= 0) {
|
|
359
|
+
// An established repository may report Card metadata. Index changes
|
|
360
|
+
// can also imply worktree identity changes, so they widen.
|
|
361
|
+
const insideMarker = path.split("/").slice(marker + 1).join("/");
|
|
362
|
+
if (!hasGitMarker(owner.absolutePath)
|
|
363
|
+
|| insideMarker === "index" || insideMarker.startsWith("index/"))
|
|
364
|
+
return null;
|
|
365
|
+
metadataReads += 1;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const exact = this.rowAtPath.get(root.rootUUID, path);
|
|
369
|
+
const stats = tryLstat(join(observation.directory, ...path.split("/")));
|
|
370
|
+
if (!exact || !stats || exact.uuid === owner.uuid
|
|
371
|
+
|| this.repositoryAtPath(root.rootUUID, exact.relativePath)?.uuid !== owner.uuid)
|
|
372
|
+
return null;
|
|
373
|
+
metadataReads += 1;
|
|
374
|
+
if (stats.isFile() && ["file.text", "file.binary"].includes(exact.type)) {
|
|
375
|
+
const captured = await captureContentFile(exact.absolutePath, stats, null);
|
|
376
|
+
const type = classifyFile({ binary: captured.binary });
|
|
377
|
+
const result = {
|
|
378
|
+
...exact,
|
|
379
|
+
type,
|
|
380
|
+
version: canonicalVersion({
|
|
381
|
+
type,
|
|
382
|
+
parentUUID: exact.parentUUID,
|
|
383
|
+
name: exact.name,
|
|
384
|
+
status: exact.status,
|
|
385
|
+
payloadVersion: captured.contentHash,
|
|
386
|
+
}, sha256Hex),
|
|
387
|
+
payloadVersion: captured.contentHash,
|
|
388
|
+
transportVersion: null,
|
|
389
|
+
deviceNumber: stats.dev,
|
|
390
|
+
inode: stats.ino,
|
|
391
|
+
byteSize: stats.size,
|
|
392
|
+
modifiedTimeMs: stats.mtimeMs,
|
|
393
|
+
changedTimeMs: stats.ctimeMs,
|
|
394
|
+
filesystemMode: stats.mode,
|
|
395
|
+
contentVerifiedAtMs: Date.now(),
|
|
396
|
+
};
|
|
397
|
+
leafRows.set(result.uuid, result);
|
|
398
|
+
contentReads += 1;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (stats.isSymbolicLink() && exact.type === "link") {
|
|
402
|
+
const target = readlinkSync(exact.absolutePath);
|
|
403
|
+
const payloadVersion = sha256Hex(Buffer.from(target, "utf8"));
|
|
404
|
+
const result = {
|
|
405
|
+
...exact,
|
|
406
|
+
version: canonicalVersion({
|
|
407
|
+
type: "link",
|
|
408
|
+
parentUUID: exact.parentUUID,
|
|
409
|
+
name: exact.name,
|
|
410
|
+
status: exact.status,
|
|
411
|
+
payloadVersion,
|
|
412
|
+
}, sha256Hex),
|
|
413
|
+
payloadVersion,
|
|
414
|
+
deviceNumber: stats.dev,
|
|
415
|
+
inode: stats.ino,
|
|
416
|
+
byteSize: stats.size,
|
|
417
|
+
modifiedTimeMs: stats.mtimeMs,
|
|
418
|
+
changedTimeMs: stats.ctimeMs,
|
|
419
|
+
filesystemMode: stats.mode,
|
|
420
|
+
contentVerifiedAtMs: Date.now(),
|
|
421
|
+
};
|
|
422
|
+
leafRows.set(result.uuid, result);
|
|
423
|
+
contentReads += 1;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (!repository || !directory)
|
|
430
|
+
return null;
|
|
431
|
+
const priorRows = this.rowsUnderPath.all(repository.rootUUID, repository.relativePath, repository.relativePath, repository.relativePath, repository.relativePath);
|
|
432
|
+
const currentRows = priorRows.map((row) => leafRows.get(row.uuid) ?? row);
|
|
433
|
+
const previousIdentity = repositoryIdentity(repository, priorRows);
|
|
434
|
+
const identity = repositoryIdentity(repository, currentRows);
|
|
435
|
+
let prior = null;
|
|
436
|
+
let priorLayout = null;
|
|
437
|
+
if (repository.payloadVersion && repository.transportVersion) {
|
|
438
|
+
const priorFile = join(this.cacheDir, `${repository.transportVersion}.bin`);
|
|
439
|
+
if (existsSync(priorFile)) {
|
|
440
|
+
try {
|
|
441
|
+
const layout = inspectRepositoryTransportFile(priorFile);
|
|
442
|
+
if (layout.stateId === repository.payloadVersion) {
|
|
443
|
+
priorLayout = layout;
|
|
444
|
+
prior = {
|
|
445
|
+
stateId: repository.payloadVersion,
|
|
446
|
+
transportVersion: repository.transportVersion,
|
|
447
|
+
card: layout.card,
|
|
448
|
+
identityHash: layout.identityHash,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
prior = null;
|
|
454
|
+
priorLayout = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const captured = captureRepository(repository.absolutePath, identity, prior, prior ? previousIdentity : null);
|
|
459
|
+
if (captured.bytes) {
|
|
460
|
+
sealCapturedArtifact(this.cacheDir, {
|
|
461
|
+
entityId: repository.uuid,
|
|
462
|
+
entityType: "repo.git",
|
|
463
|
+
kind: "git",
|
|
464
|
+
contentHash: captured.transportVersion,
|
|
465
|
+
}, captured.bytes);
|
|
466
|
+
}
|
|
467
|
+
const layout = inspectRepositoryTransportFile(join(this.cacheDir, `${captured.transportVersion}.bin`));
|
|
468
|
+
const stats = lstatSync(repository.absolutePath);
|
|
469
|
+
const result = {
|
|
470
|
+
...repository,
|
|
471
|
+
version: canonicalVersion({
|
|
472
|
+
type: "repo.git",
|
|
473
|
+
parentUUID: repository.parentUUID,
|
|
474
|
+
name: repository.name,
|
|
475
|
+
status: repository.status,
|
|
476
|
+
payloadVersion: captured.stateId,
|
|
477
|
+
}, sha256Hex),
|
|
478
|
+
payloadVersion: captured.stateId,
|
|
479
|
+
transportVersion: captured.transportVersion,
|
|
480
|
+
deviceNumber: stats.dev,
|
|
481
|
+
inode: stats.ino,
|
|
482
|
+
byteSize: stats.size,
|
|
483
|
+
modifiedTimeMs: stats.mtimeMs,
|
|
484
|
+
changedTimeMs: stats.ctimeMs,
|
|
485
|
+
filesystemMode: stats.mode,
|
|
486
|
+
};
|
|
487
|
+
const proposal = detectedChangeProposal({
|
|
488
|
+
base: portable(repository),
|
|
489
|
+
result: portable(result),
|
|
490
|
+
replay: {
|
|
491
|
+
kind: "repo.git",
|
|
492
|
+
stateId: layout.stateId,
|
|
493
|
+
cardId: layout.cardId,
|
|
494
|
+
checkpointId: layout.checkpointId,
|
|
495
|
+
cardChanged: priorLayout?.cardId !== layout.cardId,
|
|
496
|
+
checkpointChanged: priorLayout?.checkpointId !== layout.checkpointId,
|
|
497
|
+
parentTransportVersion: layout.parentTransportVersion,
|
|
498
|
+
transportVersion: captured.transportVersion,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
const journalBytes = proposal
|
|
502
|
+
? Buffer.byteLength(journalJson({ proposal }))
|
|
503
|
+
: 0;
|
|
504
|
+
this.database.transaction(() => {
|
|
505
|
+
for (const row of leafRows.values()) {
|
|
506
|
+
this.writeEntity.run(...rowValues(row));
|
|
507
|
+
this.writeNotebook.run(...rowValues(row));
|
|
508
|
+
}
|
|
509
|
+
this.writeEntity.run(...rowValues(result));
|
|
510
|
+
this.writeNotebook.run(...rowValues(result));
|
|
511
|
+
if (proposal) {
|
|
512
|
+
this.appendMutation.run(randomUUID(), proposal.resourceId, journalJson({ proposal }), new Date().toISOString());
|
|
513
|
+
}
|
|
514
|
+
})();
|
|
515
|
+
return {
|
|
516
|
+
handled: true,
|
|
517
|
+
evidence: [{
|
|
518
|
+
directory,
|
|
519
|
+
paths: active.reduce((count, item) => count + item.suspicion.paths.length, 0),
|
|
520
|
+
metadataReads,
|
|
521
|
+
contentReads,
|
|
522
|
+
gitInspections: 1,
|
|
523
|
+
repositoryCaptures: 1,
|
|
524
|
+
plannedRows: 1,
|
|
525
|
+
records: proposal ? 1 : 0,
|
|
526
|
+
journalBytes,
|
|
527
|
+
durationMs: performance.now() - started,
|
|
528
|
+
}],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
repositoryAtPath(rootUUID, path) {
|
|
532
|
+
const segments = path.split("/");
|
|
533
|
+
const marker = segments.indexOf(".git");
|
|
534
|
+
if (marker >= 0) {
|
|
535
|
+
const candidate = segments.slice(0, marker).join("/");
|
|
536
|
+
const row = this.rowAtPath.get(rootUUID, candidate);
|
|
537
|
+
return row?.type === "repo.git" ? row : null;
|
|
538
|
+
}
|
|
539
|
+
let candidate = path;
|
|
540
|
+
while (true) {
|
|
541
|
+
const row = this.rowAtPath.get(rootUUID, candidate);
|
|
542
|
+
if (row?.type === "repo.git")
|
|
543
|
+
return row;
|
|
544
|
+
if (candidate === "")
|
|
545
|
+
return null;
|
|
546
|
+
const parent = posix.dirname(candidate);
|
|
547
|
+
candidate = parent === "." ? "" : parent;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async captureFile(input) {
|
|
551
|
+
const { root, path, absolutePath, stats, parent, exact, moveSources } = input;
|
|
552
|
+
const base = exact ?? this.uniqueMovedBase(stats.dev, stats.ino, absolutePath, moveSources);
|
|
553
|
+
const fingerprintTrusted = base !== null
|
|
554
|
+
&& base.relativePath === path
|
|
555
|
+
&& this.materializedByUuid.get(base.uuid) !== undefined
|
|
556
|
+
&& sameFingerprint(base, stats)
|
|
557
|
+
&& base.contentVerifiedAtMs !== null
|
|
558
|
+
&& stats.mtimeMs < base.contentVerifiedAtMs;
|
|
559
|
+
if (fingerprintTrusted)
|
|
560
|
+
return { item: null, contentReads: 0 };
|
|
561
|
+
const canReuseMovedContent = base !== null
|
|
562
|
+
&& base.relativePath !== path
|
|
563
|
+
&& base.deviceNumber === stats.dev
|
|
564
|
+
&& base.inode === stats.ino
|
|
565
|
+
&& base.byteSize === stats.size
|
|
566
|
+
&& base.modifiedTimeMs === stats.mtimeMs
|
|
567
|
+
&& base.filesystemMode === stats.mode
|
|
568
|
+
&& base.payloadVersion !== null;
|
|
569
|
+
const content = canReuseMovedContent ? null : await captureContentFile(absolutePath, stats, this.cacheDir);
|
|
570
|
+
const type = content ? classifyFile({ binary: content.binary }) : base?.type;
|
|
571
|
+
const payloadVersion = content?.contentHash ?? base?.payloadVersion ?? null;
|
|
572
|
+
if (!type || !["file.text", "file.binary"].includes(type) || !payloadVersion) {
|
|
573
|
+
throw new Error(`named Detect could not capture ${path}`);
|
|
574
|
+
}
|
|
575
|
+
const result = {
|
|
576
|
+
resourceId: root.resourceId,
|
|
577
|
+
rootUUID: root.rootUUID,
|
|
578
|
+
uuid: base?.uuid ?? randomUUID(),
|
|
579
|
+
type,
|
|
580
|
+
parentUUID: parent.uuid,
|
|
581
|
+
name: posix.basename(path),
|
|
582
|
+
status: "active",
|
|
583
|
+
version: canonicalVersion({
|
|
584
|
+
type,
|
|
585
|
+
parentUUID: parent.uuid,
|
|
586
|
+
name: posix.basename(path),
|
|
587
|
+
status: "active",
|
|
588
|
+
payloadVersion,
|
|
589
|
+
}, sha256Hex),
|
|
590
|
+
payloadVersion,
|
|
591
|
+
transportVersion: null,
|
|
592
|
+
relativePath: path,
|
|
593
|
+
absolutePath,
|
|
594
|
+
deviceNumber: stats.dev,
|
|
595
|
+
inode: stats.ino,
|
|
596
|
+
byteSize: stats.size,
|
|
597
|
+
modifiedTimeMs: stats.mtimeMs,
|
|
598
|
+
changedTimeMs: stats.ctimeMs,
|
|
599
|
+
filesystemMode: stats.mode,
|
|
600
|
+
contentVerifiedAtMs: content ? Date.now() : base?.contentVerifiedAtMs ?? null,
|
|
601
|
+
};
|
|
602
|
+
const replay = base !== null
|
|
603
|
+
&& base.type === result.type
|
|
604
|
+
&& base.payloadVersion === result.payloadVersion
|
|
605
|
+
&& base.transportVersion === result.transportVersion
|
|
606
|
+
? { kind: "structure" }
|
|
607
|
+
: this.replayFor(base, result);
|
|
608
|
+
return {
|
|
609
|
+
item: {
|
|
610
|
+
base,
|
|
611
|
+
result,
|
|
612
|
+
proposal: detectedChangeProposal({
|
|
613
|
+
base: base ? portable(base) : null,
|
|
614
|
+
result: portable(result),
|
|
615
|
+
replay,
|
|
616
|
+
}),
|
|
617
|
+
},
|
|
618
|
+
contentReads: content ? 1 : 0,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
async captureDirectory(input) {
|
|
622
|
+
if (existsSync(join(input.absolutePath, ".git")))
|
|
623
|
+
return null;
|
|
624
|
+
const exact = this.rowAtPath.get(input.root.rootUUID, input.path);
|
|
625
|
+
if (exact && !["workspace", "folder"].includes(exact.type))
|
|
626
|
+
return null;
|
|
627
|
+
const base = exact ?? this.uniqueMovedBase(input.stats.dev, input.stats.ino, input.absolutePath, input.moveSources);
|
|
628
|
+
if (base && !["workspace", "folder"].includes(base.type))
|
|
629
|
+
return null;
|
|
630
|
+
const uuid = base?.uuid ?? randomUUID();
|
|
631
|
+
const childItems = [];
|
|
632
|
+
let metadataReads = 1;
|
|
633
|
+
let contentReads = 0;
|
|
634
|
+
const children = [];
|
|
635
|
+
for (const child of readdirSync(input.absolutePath, { withFileTypes: true })
|
|
636
|
+
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
637
|
+
const path = `${input.path}/${child.name}`;
|
|
638
|
+
if (!input.policy(path))
|
|
639
|
+
continue;
|
|
640
|
+
const absolutePath = join(input.absolutePath, child.name);
|
|
641
|
+
const stats = lstatSync(absolutePath);
|
|
642
|
+
metadataReads += 1;
|
|
643
|
+
const parentRow = {
|
|
644
|
+
resourceId: input.root.resourceId,
|
|
645
|
+
rootUUID: input.root.rootUUID,
|
|
646
|
+
uuid,
|
|
647
|
+
type: "folder",
|
|
648
|
+
parentUUID: input.parent.uuid,
|
|
649
|
+
name: posix.basename(input.path),
|
|
650
|
+
status: "active",
|
|
651
|
+
version: "0".repeat(64),
|
|
652
|
+
payloadVersion: null,
|
|
653
|
+
transportVersion: null,
|
|
654
|
+
relativePath: input.path,
|
|
655
|
+
absolutePath: input.absolutePath,
|
|
656
|
+
deviceNumber: input.stats.dev,
|
|
657
|
+
inode: input.stats.ino,
|
|
658
|
+
byteSize: input.stats.size,
|
|
659
|
+
modifiedTimeMs: input.stats.mtimeMs,
|
|
660
|
+
changedTimeMs: input.stats.ctimeMs,
|
|
661
|
+
filesystemMode: input.stats.mode,
|
|
662
|
+
contentVerifiedAtMs: null,
|
|
663
|
+
};
|
|
664
|
+
if (stats.isSymbolicLink()) {
|
|
665
|
+
const link = this.captureLink({
|
|
666
|
+
root: input.root,
|
|
667
|
+
path,
|
|
668
|
+
absolutePath,
|
|
669
|
+
stats,
|
|
670
|
+
parent: parentRow,
|
|
671
|
+
exact: this.rowAtPath.get(input.root.rootUUID, path),
|
|
672
|
+
moveSources: input.moveSources,
|
|
673
|
+
});
|
|
674
|
+
if (!link)
|
|
675
|
+
return null;
|
|
676
|
+
const identity = link.item?.result
|
|
677
|
+
?? this.rowAtPath.get(input.root.rootUUID, path);
|
|
678
|
+
if (!identity)
|
|
679
|
+
throw new Error(`captured link ${path} has no identity`);
|
|
680
|
+
children.push({ uuid: identity.uuid, name: child.name });
|
|
681
|
+
if (link.item)
|
|
682
|
+
childItems.push(link.item);
|
|
683
|
+
contentReads += link.contentReads;
|
|
684
|
+
}
|
|
685
|
+
else if (stats.isDirectory()) {
|
|
686
|
+
const nested = await this.captureDirectory({
|
|
687
|
+
root: input.root,
|
|
688
|
+
path,
|
|
689
|
+
absolutePath,
|
|
690
|
+
stats,
|
|
691
|
+
parent: parentRow,
|
|
692
|
+
policy: input.policy,
|
|
693
|
+
moveSources: input.moveSources,
|
|
694
|
+
});
|
|
695
|
+
if (!nested)
|
|
696
|
+
return null;
|
|
697
|
+
const nestedRoot = nested.items[0];
|
|
698
|
+
if (!nestedRoot)
|
|
699
|
+
throw new Error(`captured directory ${path} has no root`);
|
|
700
|
+
children.push({ uuid: nestedRoot.result.uuid, name: child.name });
|
|
701
|
+
childItems.push(...nested.items);
|
|
702
|
+
metadataReads += nested.metadataReads - 1;
|
|
703
|
+
contentReads += nested.contentReads;
|
|
704
|
+
}
|
|
705
|
+
else if (stats.isFile()) {
|
|
706
|
+
const file = await this.captureFile({
|
|
707
|
+
root: input.root,
|
|
708
|
+
path,
|
|
709
|
+
absolutePath,
|
|
710
|
+
stats,
|
|
711
|
+
parent: parentRow,
|
|
712
|
+
exact: this.rowAtPath.get(input.root.rootUUID, path),
|
|
713
|
+
moveSources: input.moveSources,
|
|
714
|
+
});
|
|
715
|
+
const identity = file.item?.result
|
|
716
|
+
?? this.rowAtPath.get(input.root.rootUUID, path);
|
|
717
|
+
if (!identity)
|
|
718
|
+
throw new Error(`captured file ${path} has no identity`);
|
|
719
|
+
children.push({ uuid: identity.uuid, name: child.name });
|
|
720
|
+
if (file.item)
|
|
721
|
+
childItems.push(file.item);
|
|
722
|
+
contentReads += file.contentReads;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const result = {
|
|
726
|
+
resourceId: input.root.resourceId,
|
|
727
|
+
rootUUID: input.root.rootUUID,
|
|
728
|
+
uuid,
|
|
729
|
+
type: "folder",
|
|
730
|
+
parentUUID: input.parent.uuid,
|
|
731
|
+
name: posix.basename(input.path),
|
|
732
|
+
status: "active",
|
|
733
|
+
version: canonicalVersion({
|
|
734
|
+
type: "folder",
|
|
735
|
+
parentUUID: input.parent.uuid,
|
|
736
|
+
name: posix.basename(input.path),
|
|
737
|
+
status: "active",
|
|
738
|
+
payloadVersion: membershipHash(children, sha256Hex),
|
|
739
|
+
}, sha256Hex),
|
|
740
|
+
payloadVersion: null,
|
|
741
|
+
transportVersion: null,
|
|
742
|
+
relativePath: input.path,
|
|
743
|
+
absolutePath: input.absolutePath,
|
|
744
|
+
deviceNumber: input.stats.dev,
|
|
745
|
+
inode: input.stats.ino,
|
|
746
|
+
byteSize: input.stats.size,
|
|
747
|
+
modifiedTimeMs: input.stats.mtimeMs,
|
|
748
|
+
changedTimeMs: input.stats.ctimeMs,
|
|
749
|
+
filesystemMode: input.stats.mode,
|
|
750
|
+
contentVerifiedAtMs: null,
|
|
751
|
+
};
|
|
752
|
+
const proposal = detectedChangeProposal({
|
|
753
|
+
base: base ? portable(base) : null,
|
|
754
|
+
result: portable(result),
|
|
755
|
+
replay: { kind: "structure" },
|
|
756
|
+
});
|
|
757
|
+
return {
|
|
758
|
+
items: [{ base, result, proposal }, ...childItems],
|
|
759
|
+
metadataReads,
|
|
760
|
+
contentReads,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
captureLink(input) {
|
|
764
|
+
const { root, path, absolutePath, stats, parent, exact, moveSources } = input;
|
|
765
|
+
const base = exact ?? this.uniqueMovedBase(stats.dev, stats.ino, absolutePath, moveSources);
|
|
766
|
+
if (base && base.type !== "link")
|
|
767
|
+
return null;
|
|
768
|
+
const fingerprintTrusted = base !== null
|
|
769
|
+
&& base.relativePath === path
|
|
770
|
+
&& this.materializedByUuid.get(base.uuid) !== undefined
|
|
771
|
+
&& sameFingerprint(base, stats)
|
|
772
|
+
&& base.contentVerifiedAtMs !== null
|
|
773
|
+
&& stats.mtimeMs < base.contentVerifiedAtMs;
|
|
774
|
+
if (fingerprintTrusted)
|
|
775
|
+
return { item: null, contentReads: 0 };
|
|
776
|
+
const canReuseMovedTarget = base !== null
|
|
777
|
+
&& base.relativePath !== path
|
|
778
|
+
&& base.deviceNumber === stats.dev
|
|
779
|
+
&& base.inode === stats.ino
|
|
780
|
+
&& base.byteSize === stats.size
|
|
781
|
+
&& base.modifiedTimeMs === stats.mtimeMs
|
|
782
|
+
&& base.filesystemMode === stats.mode
|
|
783
|
+
&& base.payloadVersion !== null;
|
|
784
|
+
const target = canReuseMovedTarget ? null : readlinkSync(absolutePath);
|
|
785
|
+
const payloadVersion = target === null
|
|
786
|
+
? base?.payloadVersion ?? null
|
|
787
|
+
: sha256Hex(Buffer.from(target, "utf8"));
|
|
788
|
+
if (!payloadVersion)
|
|
789
|
+
return null;
|
|
790
|
+
const result = {
|
|
791
|
+
resourceId: root.resourceId,
|
|
792
|
+
rootUUID: root.rootUUID,
|
|
793
|
+
uuid: base?.uuid ?? randomUUID(),
|
|
794
|
+
type: "link",
|
|
795
|
+
parentUUID: parent.uuid,
|
|
796
|
+
name: posix.basename(path),
|
|
797
|
+
status: "active",
|
|
798
|
+
version: canonicalVersion({
|
|
799
|
+
type: "link",
|
|
800
|
+
parentUUID: parent.uuid,
|
|
801
|
+
name: posix.basename(path),
|
|
802
|
+
status: "active",
|
|
803
|
+
payloadVersion,
|
|
804
|
+
}, sha256Hex),
|
|
805
|
+
payloadVersion,
|
|
806
|
+
transportVersion: null,
|
|
807
|
+
relativePath: path,
|
|
808
|
+
absolutePath,
|
|
809
|
+
deviceNumber: stats.dev,
|
|
810
|
+
inode: stats.ino,
|
|
811
|
+
byteSize: stats.size,
|
|
812
|
+
modifiedTimeMs: stats.mtimeMs,
|
|
813
|
+
changedTimeMs: stats.ctimeMs,
|
|
814
|
+
filesystemMode: stats.mode,
|
|
815
|
+
contentVerifiedAtMs: target === null ? base?.contentVerifiedAtMs ?? null : Date.now(),
|
|
816
|
+
};
|
|
817
|
+
const replay = base !== null && base.payloadVersion === result.payloadVersion
|
|
818
|
+
? { kind: "structure" }
|
|
819
|
+
: target === null ? { kind: "structure" } : {
|
|
820
|
+
kind: "link",
|
|
821
|
+
resultPayloadVersion: payloadVersion,
|
|
822
|
+
target,
|
|
823
|
+
};
|
|
824
|
+
return {
|
|
825
|
+
item: {
|
|
826
|
+
base,
|
|
827
|
+
result,
|
|
828
|
+
proposal: detectedChangeProposal({
|
|
829
|
+
base: base ? portable(base) : null,
|
|
830
|
+
result: portable(result),
|
|
831
|
+
replay,
|
|
832
|
+
}),
|
|
833
|
+
},
|
|
834
|
+
contentReads: target === null ? 0 : 1,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
enclosedByRepository(row) {
|
|
838
|
+
let cursor = row;
|
|
839
|
+
const seen = new Set();
|
|
840
|
+
while (cursor.parentUUID !== null) {
|
|
841
|
+
if (seen.has(cursor.uuid))
|
|
842
|
+
throw new Error("Detect notebook has a parent cycle");
|
|
843
|
+
seen.add(cursor.uuid);
|
|
844
|
+
const parent = this.rowAtUuid.get(cursor.parentUUID);
|
|
845
|
+
if (!parent)
|
|
846
|
+
throw new Error(`Detect notebook has no parent ${cursor.parentUUID}`);
|
|
847
|
+
if (parent.type === "repo.git")
|
|
848
|
+
return true;
|
|
849
|
+
cursor = parent;
|
|
850
|
+
}
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
readUserPolicy() {
|
|
854
|
+
const ignore = join(this.userRoot, ".amalgmignore");
|
|
855
|
+
return createUserGroundEnrollmentPolicy(existsSync(ignore) ? readFileSync(ignore, "utf8") : "");
|
|
856
|
+
}
|
|
857
|
+
uniqueMovedBase(deviceNumber, inode, absolutePath, moveSources) {
|
|
858
|
+
const candidates = this.rowsAtPhysicalIdentity.all(deviceNumber, inode)
|
|
859
|
+
.filter((row) => row.absolutePath !== absolutePath
|
|
860
|
+
&& moveSources.some((source) => row.absolutePath === source || row.absolutePath.startsWith(`${source}${sep}`))
|
|
861
|
+
&& !existsSync(row.absolutePath));
|
|
862
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
863
|
+
}
|
|
864
|
+
replayFor(base, result) {
|
|
865
|
+
if (result.type === "file.text") {
|
|
866
|
+
const resultBytes = readFileSync(join(this.cacheDir, `${result.payloadVersion}.bin`));
|
|
867
|
+
const baseFile = base?.payloadVersion ? join(this.cacheDir, `${base.payloadVersion}.bin`) : null;
|
|
868
|
+
const baseBytes = baseFile && existsSync(baseFile) ? readFileSync(baseFile) : null;
|
|
869
|
+
return exactTextReplay({
|
|
870
|
+
entityId: result.uuid,
|
|
871
|
+
basePayloadVersion: base?.payloadVersion ?? null,
|
|
872
|
+
baseBytes,
|
|
873
|
+
resultPayloadVersion: result.payloadVersion,
|
|
874
|
+
resultBytes,
|
|
875
|
+
sha256Hex,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
const manifestFile = join(this.cacheDir, "manifests", `${result.payloadVersion}.json`);
|
|
879
|
+
const checked = checkContentManifest(JSON.parse(readFileSync(manifestFile, "utf8")), result.payloadVersion);
|
|
880
|
+
if (!checked.ok)
|
|
881
|
+
throw new Error(checked.error);
|
|
882
|
+
return {
|
|
883
|
+
kind: "file.binary",
|
|
884
|
+
resultPayloadVersion: result.payloadVersion,
|
|
885
|
+
manifest: checked.value,
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function tryLstat(path) {
|
|
890
|
+
try {
|
|
891
|
+
return lstatSync(path);
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
const code = error.code;
|
|
895
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
896
|
+
return null;
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function repositoryIdentity(repository, rows) {
|
|
901
|
+
const repositories = rows.filter((row) => row.type === "repo.git");
|
|
902
|
+
const ownerOf = (row) => repositories
|
|
903
|
+
.filter((candidate) => candidate.uuid !== row.uuid
|
|
904
|
+
&& (candidate.relativePath === ""
|
|
905
|
+
|| row.relativePath.startsWith(`${candidate.relativePath}/`)))
|
|
906
|
+
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
907
|
+
return rows
|
|
908
|
+
.filter((row) => row.uuid !== repository.uuid && ownerOf(row)?.uuid === repository.uuid)
|
|
909
|
+
.map((row) => ({
|
|
910
|
+
path: repository.relativePath
|
|
911
|
+
? row.relativePath.slice(repository.relativePath.length + 1)
|
|
912
|
+
: row.relativePath,
|
|
913
|
+
uuid: row.uuid,
|
|
914
|
+
type: row.type,
|
|
915
|
+
payloadVersion: row.payloadVersion,
|
|
916
|
+
}))
|
|
917
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
918
|
+
}
|
|
919
|
+
//# sourceMappingURL=runtime.js.map
|