pi-posthorse 0.10.5 → 0.10.6
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/package.json +1 -1
- package/src/note-discovery.ts +57 -34
- package/src/note-guards.ts +6 -5
- package/src/note-provenance.ts +6 -3
- package/src/note-snapshots.ts +29 -9
- package/src/notes.ts +10 -9
package/package.json
CHANGED
package/src/note-discovery.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { join, relative } from "node:path";
|
|
|
3
3
|
import { isRecord } from "./contracts.ts";
|
|
4
4
|
import { sessionNoteTouches } from "./note-evidence.ts";
|
|
5
5
|
import { notesUnder, type NoteFile } from "./note-inventory.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
fileDigest,
|
|
8
|
+
fileIdentity,
|
|
9
|
+
mapBatch,
|
|
10
|
+
readManifest,
|
|
11
|
+
sameIdentity,
|
|
12
|
+
} from "./note-snapshots.ts";
|
|
7
13
|
import {
|
|
8
14
|
sameScope,
|
|
9
15
|
scopeDirectory,
|
|
@@ -47,12 +53,13 @@ async function projectScopes(
|
|
|
47
53
|
}
|
|
48
54
|
throw error;
|
|
49
55
|
});
|
|
50
|
-
|
|
56
|
+
const candidates = entries.filter((entry) => !entry.name.startsWith(".staging-"));
|
|
57
|
+
const manifests = await mapBatch(candidates, async (entry) =>
|
|
58
|
+
entry.isDirectory() ? readManifest(join(directory, entry.name, "notes")) : undefined,
|
|
59
|
+
);
|
|
60
|
+
for (const [index, entry] of candidates.entries()) {
|
|
51
61
|
const path = join(directory, entry.name);
|
|
52
|
-
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
const manifest = entry.isDirectory() ? await readManifest(join(path, "notes")) : undefined;
|
|
62
|
+
const manifest = manifests[index];
|
|
56
63
|
if (manifest === undefined || manifest.root !== session.binding.root) {
|
|
57
64
|
notice(`Not indexed: unmanaged or incomplete notes data at ${path}.`);
|
|
58
65
|
} else {
|
|
@@ -199,9 +206,19 @@ export class NoteDiscovery {
|
|
|
199
206
|
Number(sameScope(b.address, session.binding)) -
|
|
200
207
|
Number(sameScope(a.address, session.binding)),
|
|
201
208
|
);
|
|
202
|
-
|
|
209
|
+
const results = await mapBatch(group, async (file) => {
|
|
210
|
+
const fileNotices: string[] = [];
|
|
211
|
+
const digest = await this.#digest(file, session, {
|
|
212
|
+
...options,
|
|
213
|
+
notice: (text) => {
|
|
214
|
+
fileNotices.push(text);
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
return { file, digest, fileNotices };
|
|
218
|
+
});
|
|
219
|
+
for (const { file, digest, fileNotices } of results) {
|
|
220
|
+
fileNotices.forEach(options.notice);
|
|
203
221
|
// Only same-lineage, same-size candidates need content comparison.
|
|
204
|
-
const digest = await this.#digest(file, session, options);
|
|
205
222
|
const previous = digest === undefined ? undefined : matches.get(digest);
|
|
206
223
|
if (previous === undefined || digest === undefined) {
|
|
207
224
|
matches.set(digest ?? noteKey(file.address), file);
|
|
@@ -229,22 +246,17 @@ export class NoteDiscovery {
|
|
|
229
246
|
directories: new Set(session.scopes.map(scopeDirectory)),
|
|
230
247
|
});
|
|
231
248
|
const paths = new Set(touches.map((touch) => touch.notePath));
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
path
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (file !== undefined) {
|
|
244
|
-
files.push(file);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return files;
|
|
249
|
+
const candidates = [...paths].filter(
|
|
250
|
+
(path) =>
|
|
251
|
+
!session.owns(path) &&
|
|
252
|
+
(options.path === undefined ||
|
|
253
|
+
path === options.path ||
|
|
254
|
+
path.startsWith(`${options.path}/`)),
|
|
255
|
+
);
|
|
256
|
+
const resolved = await mapBatch(candidates, (path) =>
|
|
257
|
+
readReference(session, path, options.signal),
|
|
258
|
+
);
|
|
259
|
+
return resolved.filter((file): file is DiscoveredNote => file !== undefined);
|
|
248
260
|
}
|
|
249
261
|
async list(
|
|
250
262
|
session: SessionNotes,
|
|
@@ -258,21 +270,32 @@ export class NoteDiscovery {
|
|
|
258
270
|
const notice = (text: string) => {
|
|
259
271
|
notices.push(text);
|
|
260
272
|
};
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
273
|
+
const requestedScope = options.scope;
|
|
274
|
+
const [scopes, references] = await Promise.all([
|
|
275
|
+
requestedScope === undefined
|
|
276
|
+
? projectScopes(session, notice)
|
|
277
|
+
: (async () => [
|
|
265
278
|
{
|
|
266
|
-
scope: await session.selectScope(
|
|
267
|
-
manifest: await readManifest(
|
|
279
|
+
scope: await session.selectScope(requestedScope),
|
|
280
|
+
manifest: await readManifest(requestedScope),
|
|
268
281
|
},
|
|
269
|
-
]
|
|
282
|
+
])(),
|
|
283
|
+
this.#references(session, options),
|
|
284
|
+
]);
|
|
285
|
+
const scoped = await mapBatch(scopes, async (scope) => {
|
|
286
|
+
const scopeNotices: string[] = [];
|
|
287
|
+
const scopeFiles = await scopedFiles(scope, options, (text) => {
|
|
288
|
+
scopeNotices.push(text);
|
|
289
|
+
});
|
|
290
|
+
return { scopeFiles, scopeNotices };
|
|
291
|
+
});
|
|
270
292
|
const files: DiscoveredNote[] = [];
|
|
271
|
-
for (const
|
|
293
|
+
for (const { scopeFiles, scopeNotices } of scoped) {
|
|
272
294
|
// Preserve native listing order while walking each scope independently.
|
|
273
|
-
|
|
295
|
+
notices.push(...scopeNotices);
|
|
296
|
+
files.push(...scopeFiles);
|
|
274
297
|
}
|
|
275
|
-
for (const reference of
|
|
298
|
+
for (const reference of references) {
|
|
276
299
|
if (!files.some((file) => noteKey(file.address) === noteKey(reference.address))) {
|
|
277
300
|
files.push(reference);
|
|
278
301
|
}
|
package/src/note-guards.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type NoteAddress,
|
|
13
13
|
type NoteTouchRecord,
|
|
14
14
|
} from "./note-scopes.ts";
|
|
15
|
+
import { mapBatch } from "./note-snapshots.ts";
|
|
15
16
|
import { noteOperations, verifiedNoteOperations, type NoteOperation } from "./note-tool-effects.ts";
|
|
16
17
|
import type { NoteCall, NotesStore, SessionNotes } from "./note-store.ts";
|
|
17
18
|
|
|
@@ -115,12 +116,12 @@ async function recordedFiles(
|
|
|
115
116
|
captured: CapturedCall,
|
|
116
117
|
): Promise<NoteTouchRecord["files"]> {
|
|
117
118
|
const files: NoteTouchRecord["files"][number][] = [];
|
|
118
|
-
|
|
119
|
+
const addresses = await mapBatch(operations, (operation) =>
|
|
120
|
+
noteAddress(toolPath(captured.tool, operation.path, captured.cwd), captured.session),
|
|
121
|
+
);
|
|
122
|
+
for (const [index, operation] of operations.entries()) {
|
|
119
123
|
// Each result belongs to the scope captured before execution, even after /tree.
|
|
120
|
-
const address =
|
|
121
|
-
toolPath(captured.tool, operation.path, captured.cwd),
|
|
122
|
-
captured.session,
|
|
123
|
-
);
|
|
124
|
+
const address = addresses[index];
|
|
124
125
|
if (address === undefined) {
|
|
125
126
|
continue;
|
|
126
127
|
}
|
package/src/note-provenance.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { isRecord, type EntryLike } from "./contracts.ts";
|
|
|
5
5
|
import { sessionLines } from "./session-lines.ts";
|
|
6
6
|
import { parseSessionLine } from "./session-files.ts";
|
|
7
7
|
import { sessionNoteTouches } from "./note-evidence.ts";
|
|
8
|
-
import { readManifest, snapshot, type SnapshotNote } from "./note-snapshots.ts";
|
|
8
|
+
import { mapBatch, readManifest, snapshot, type SnapshotNote } from "./note-snapshots.ts";
|
|
9
9
|
import {
|
|
10
10
|
addressOf,
|
|
11
11
|
bindingsOf,
|
|
@@ -227,9 +227,12 @@ async function forkBinding(
|
|
|
227
227
|
const entries = [...manager.getEntries(), ...source.entries];
|
|
228
228
|
const files: SnapshotNote[] = [];
|
|
229
229
|
const absent: string[] = [];
|
|
230
|
-
|
|
230
|
+
const addresses = await mapBatch(candidates, (candidate) =>
|
|
231
|
+
resolveReference(scope ?? candidate.source, candidate.path, entries),
|
|
232
|
+
);
|
|
233
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
231
234
|
// Resolve against the immediate parent's files, not a historical ancestor's version.
|
|
232
|
-
const address =
|
|
235
|
+
const address = addresses[index];
|
|
233
236
|
if (address !== undefined) {
|
|
234
237
|
files.push({
|
|
235
238
|
path: candidate.path,
|
package/src/note-snapshots.ts
CHANGED
|
@@ -89,18 +89,39 @@ export type SnapshotNote = {
|
|
|
89
89
|
readonly lineage: NoteAddress;
|
|
90
90
|
readonly anchors: readonly string[];
|
|
91
91
|
};
|
|
92
|
+
const NOTE_IO_BATCH = 16;
|
|
93
|
+
export async function mapBatch<Input, Output>(
|
|
94
|
+
items: readonly Input[],
|
|
95
|
+
fn: (item: Input) => Promise<Output>,
|
|
96
|
+
): Promise<Output[]> {
|
|
97
|
+
const results: Output[] = [];
|
|
98
|
+
for (let start = 0; start < items.length; start += NOTE_IO_BATCH) {
|
|
99
|
+
const settled = await Promise.allSettled(
|
|
100
|
+
items.slice(start, start + NOTE_IO_BATCH).map((item) => fn(item)),
|
|
101
|
+
);
|
|
102
|
+
for (const item of settled) {
|
|
103
|
+
if (item.status === "rejected") {
|
|
104
|
+
throw item.reason instanceof Error ? item.reason : new Error(String(item.reason));
|
|
105
|
+
}
|
|
106
|
+
results.push(item.value);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
92
111
|
async function copyNote(note: SnapshotNote, destination: string): Promise<NoteSeed> {
|
|
93
112
|
const source = notePath(note.source, note.source.path);
|
|
94
|
-
const before = await
|
|
95
|
-
|
|
113
|
+
const [before] = await Promise.all([
|
|
114
|
+
stat(source),
|
|
115
|
+
mkdir(dirname(destination), { recursive: true }),
|
|
116
|
+
]);
|
|
96
117
|
await copyFile(source, destination, constants.COPYFILE_EXCL);
|
|
97
118
|
const after = await stat(source);
|
|
98
119
|
if (!sameIdentity(before, after)) {
|
|
99
120
|
throw new Error(`Source note changed during snapshot: ${source}. Retry initialization.`);
|
|
100
121
|
}
|
|
101
122
|
await utimes(destination, before.atimeMs / 1000, before.mtimeMs / 1000);
|
|
102
|
-
const digest = await fileDigest(destination);
|
|
103
|
-
return { ...note, digest, identity: fileIdentity(
|
|
123
|
+
const [digest, destInfo] = await Promise.all([fileDigest(destination), stat(destination)]);
|
|
124
|
+
return { ...note, digest, identity: fileIdentity(destInfo) };
|
|
104
125
|
}
|
|
105
126
|
/** Publication is the ownership boundary. An existing unrelated directory is never overwritten. */
|
|
106
127
|
export async function snapshot(
|
|
@@ -129,11 +150,10 @@ export async function snapshot(
|
|
|
129
150
|
await mkdir(dirname(container), { recursive: true });
|
|
130
151
|
const stage = await mkdtemp(join(dirname(container), ".staging-"));
|
|
131
152
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
153
|
+
// Keep each file's identity and digest paired with the bytes actually copied.
|
|
154
|
+
const seeds = await mapBatch(notes, (note) =>
|
|
155
|
+
copyNote(note, notePath({ root: stage, scope: "notes" }, note.path)),
|
|
156
|
+
);
|
|
137
157
|
const complete: ForkBinding = { ...binding, seeds };
|
|
138
158
|
await writeFile(join(stage, "manifest.json"), JSON.stringify(complete), { flag: "wx" });
|
|
139
159
|
await rename(stage, container);
|
package/src/notes.ts
CHANGED
|
@@ -55,16 +55,17 @@ async function listNotes(
|
|
|
55
55
|
): Promise<TextResult> {
|
|
56
56
|
const { host, pages, signal } = access;
|
|
57
57
|
const folder = params.path;
|
|
58
|
-
const inventory = await
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
const [inventory, selectedScope] = await Promise.all([
|
|
59
|
+
access.discovery.list(session, {
|
|
60
|
+
path: folder,
|
|
61
|
+
scope: params.scope,
|
|
62
|
+
signal,
|
|
63
|
+
}),
|
|
64
|
+
(async () =>
|
|
65
|
+
params.scope === undefined ? undefined : session.selectScope(params.scope))(),
|
|
66
|
+
]);
|
|
63
67
|
const { files } = inventory;
|
|
64
|
-
const dir =
|
|
65
|
-
params.scope === undefined
|
|
66
|
-
? session.directory
|
|
67
|
-
: scopeDirectory(await session.selectScope(params.scope));
|
|
68
|
+
const dir = selectedScope === undefined ? session.directory : scopeDirectory(selectedScope);
|
|
68
69
|
const rows: string[] = [];
|
|
69
70
|
for (let start = 0; start < files.length; start += FORMAT_BATCH) {
|
|
70
71
|
// Pi gets an event-loop turn before each batch of rows.
|