@hraness/wordcell 0.20.0-bootstrap.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +749 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +57 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +41 -0
- package/dist/index-054mb7d3.js +1974 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-180zbkkt.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-23z4zxgg.js +119 -0
- package/dist/index-2gv8y733.js +432 -0
- package/dist/index-3ehtt59c.js +199 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6jcz0m1c.js +712 -0
- package/dist/index-6sw24nvv.js +1189 -0
- package/dist/index-8a08v6x8.js +56 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-bcknqxrq.js +1943 -0
- package/dist/index-bs0yf4pp.js +711 -0
- package/dist/index-by0pbant.js +827 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dfag79p7.js +370 -0
- package/dist/index-e4fagfjz.js +44 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-ekpwvbra.js +1070 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-h170byqw.js +371 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-m7et4rz4.js +966 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n05s3wsb.js +2728 -0
- package/dist/index-nd6nynv2.js +1162 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-rdd5f7jj.js +70 -0
- package/dist/index-sy0zety7.js +3630 -0
- package/dist/index-tkpxp48t.js +1166 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-why54q5p.js +462 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-x6m57nfc.js +1041 -0
- package/dist/index.js +784 -0
- package/dist/kb-alias.js +55 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +44 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +421 -0
- package/skills/wordcell/AGENTS.md +22 -0
- package/skills/wordcell/SKILL.md +91 -0
- package/skills/wordcell/agents/openai.yaml +4 -0
- package/skills/wordcell/references/companion-skills.md +96 -0
- package/skills/wordcell/references/customize.md +123 -0
- package/skills/wordcell/references/pdf-review.md +51 -0
- package/skills/wordcell/references/percolate.md +176 -0
- package/skills/wordcell/references/plan-structure.md +105 -0
- package/skills/wordcell/references/plan.md +115 -0
- package/skills/wordcell/references/query.md +252 -0
- package/skills/wordcell/references/refresh.md +133 -0
- package/skills/wordcell/references/save-pdf.md +125 -0
- package/skills/wordcell/references/save-url.md +178 -0
- package/skills/wordcell/references/url-authentication.md +75 -0
- package/skills/wordcell/references/url-platforms.md +25 -0
- package/skills/wordcell/templates/companion-skill.template.md +57 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring-model.ts +798 -0
- package/src/authoring-platform.ts +549 -0
- package/src/authoring-program.ts +278 -0
- package/src/authoring-runtime.ts +54 -0
- package/src/authoring.ts +75 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli-program.ts +3662 -0
- package/src/cli.ts +6 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1494 -0
- package/src/index.ts +21 -0
- package/src/init.ts +123 -0
- package/src/kb-alias.ts +14 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/oh-adoption.ts +444 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +1948 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow-model.ts +444 -0
- package/src/workflow-platform.ts +74 -0
- package/src/workflow-program.ts +183 -0
- package/src/workflow-runtime.ts +51 -0
- package/src/workflow.ts +23 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { acquireNoteLock } from "./note-lock.js";
|
|
2
|
+
import { rename } from "node:fs/promises";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { link, lstat, mkdir, open, readdir, realpath, rmdir, unlink } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { type NoteLock } from "./note-lock.js";
|
|
8
|
+
import { MAX_NOTE_BYTES, MAX_PARENT_DIRECTORY_ENTRIES, MAX_RECOVERY_LOCATIONS_PER_NOTE, type NoteRevision, type AuthoringDependencies, NoteRecoveryRequiredError, type Vault, type NoteSnapshot, isErrno, sha256, revisionFor, inside, pathFor, type RecoveryLocation, type DirectoryIdentity, recoveryRelativePath } from "./authoring-model.js";
|
|
9
|
+
|
|
10
|
+
export async function resolveVault(rootInput: string): Promise<Vault> {
|
|
11
|
+
const root = await realpath(resolve(rootInput));
|
|
12
|
+
const metadata = await lstat(root);
|
|
13
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
14
|
+
throw new Error("the vault root must be a real directory");
|
|
15
|
+
}
|
|
16
|
+
if (dirname(root) === root) {
|
|
17
|
+
throw new Error("refusing to author notes in a filesystem root");
|
|
18
|
+
}
|
|
19
|
+
return { root };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function assertExactDirectoryEntry(
|
|
23
|
+
directory: string,
|
|
24
|
+
name: string,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const entries = await readdir(directory);
|
|
27
|
+
if (!entries.includes(name)) {
|
|
28
|
+
const error = new Error(`vault path component is not exact: ${name}`) as Error & {
|
|
29
|
+
code?: string;
|
|
30
|
+
};
|
|
31
|
+
error.code = "ENOENT";
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function assertSafeParent(vault: Vault, path: string): Promise<void> {
|
|
37
|
+
if (!inside(vault.root, path)) {
|
|
38
|
+
throw new Error("the note path must remain inside the vault");
|
|
39
|
+
}
|
|
40
|
+
const parent = dirname(path);
|
|
41
|
+
const segments = relative(vault.root, parent).split(sep).filter(Boolean);
|
|
42
|
+
let current = vault.root;
|
|
43
|
+
for (const segment of segments) {
|
|
44
|
+
await assertExactDirectoryEntry(current, segment);
|
|
45
|
+
current = join(current, segment);
|
|
46
|
+
const metadata = await lstat(current);
|
|
47
|
+
if (metadata.isSymbolicLink()) {
|
|
48
|
+
throw new Error("the note path must not traverse a symbolic link");
|
|
49
|
+
}
|
|
50
|
+
if (!metadata.isDirectory()) {
|
|
51
|
+
throw new Error("every note parent must be a directory");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const canonicalParent = await realpath(parent);
|
|
55
|
+
if (canonicalParent !== parent || !inside(vault.root, join(canonicalParent, basename(path)))) {
|
|
56
|
+
throw new Error("the note parent resolves outside the vault");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function readSnapshotAtPath(
|
|
61
|
+
vault: Vault,
|
|
62
|
+
path: string,
|
|
63
|
+
relativePath: string,
|
|
64
|
+
): Promise<NoteSnapshot> {
|
|
65
|
+
await assertSafeParent(vault, path);
|
|
66
|
+
await assertExactDirectoryEntry(dirname(path), basename(path));
|
|
67
|
+
|
|
68
|
+
const beforeOpen = await lstat(path, { bigint: true });
|
|
69
|
+
if (!beforeOpen.isFile() || beforeOpen.isSymbolicLink()) {
|
|
70
|
+
throw new Error("the note target must be a regular file");
|
|
71
|
+
}
|
|
72
|
+
if (beforeOpen.nlink !== 1n) {
|
|
73
|
+
throw new Error("the note target must not be hard-linked");
|
|
74
|
+
}
|
|
75
|
+
if (beforeOpen.size > BigInt(MAX_NOTE_BYTES)) {
|
|
76
|
+
throw new Error("the note is too large for bounded authoring");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
80
|
+
try {
|
|
81
|
+
const opened = await handle.stat({ bigint: true });
|
|
82
|
+
if (
|
|
83
|
+
!opened.isFile()
|
|
84
|
+
|| opened.nlink !== 1n
|
|
85
|
+
|| opened.dev !== beforeOpen.dev
|
|
86
|
+
|| opened.ino !== beforeOpen.ino
|
|
87
|
+
|| opened.size !== beforeOpen.size
|
|
88
|
+
|| opened.size > BigInt(MAX_NOTE_BYTES)
|
|
89
|
+
) {
|
|
90
|
+
throw new Error("the note target changed while it was opened");
|
|
91
|
+
}
|
|
92
|
+
const bytes = new Uint8Array(Number(opened.size));
|
|
93
|
+
let offset = 0;
|
|
94
|
+
while (offset < bytes.byteLength) {
|
|
95
|
+
const result = await handle.read(bytes, offset, bytes.byteLength - offset, offset);
|
|
96
|
+
if (result.bytesRead === 0) {
|
|
97
|
+
throw new Error("the note target changed while it was read");
|
|
98
|
+
}
|
|
99
|
+
offset += result.bytesRead;
|
|
100
|
+
}
|
|
101
|
+
const overflow = new Uint8Array(1);
|
|
102
|
+
if ((await handle.read(overflow, 0, 1, Number(opened.size))).bytesRead !== 0) {
|
|
103
|
+
throw new Error("the note target grew while it was read");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const finished = await handle.stat({ bigint: true });
|
|
107
|
+
const finalPath = await lstat(path, { bigint: true });
|
|
108
|
+
if (
|
|
109
|
+
!finalPath.isFile()
|
|
110
|
+
|| finalPath.isSymbolicLink()
|
|
111
|
+
|| finalPath.nlink !== 1n
|
|
112
|
+
|| finalPath.dev !== opened.dev
|
|
113
|
+
|| finalPath.ino !== opened.ino
|
|
114
|
+
|| finalPath.size !== opened.size
|
|
115
|
+
|| finished.size !== opened.size
|
|
116
|
+
|| finished.mtimeNs !== opened.mtimeNs
|
|
117
|
+
|| finished.ctimeNs !== opened.ctimeNs
|
|
118
|
+
) {
|
|
119
|
+
throw new Error("the note target changed while it was read");
|
|
120
|
+
}
|
|
121
|
+
const canonicalPath = await realpath(path);
|
|
122
|
+
if (canonicalPath !== path || !inside(vault.root, canonicalPath)) {
|
|
123
|
+
throw new Error("the note target resolves outside the vault");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let content: string;
|
|
127
|
+
try {
|
|
128
|
+
content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error("the note target is not valid UTF-8", { cause: error });
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
path,
|
|
134
|
+
relativePath,
|
|
135
|
+
content,
|
|
136
|
+
revision: revisionFor(bytes),
|
|
137
|
+
device: opened.dev,
|
|
138
|
+
inode: opened.ino,
|
|
139
|
+
size: opened.size,
|
|
140
|
+
modifiedAtNs: opened.mtimeNs,
|
|
141
|
+
changedAtNs: opened.ctimeNs,
|
|
142
|
+
mode: Number(opened.mode & 0o777n),
|
|
143
|
+
};
|
|
144
|
+
} finally {
|
|
145
|
+
await handle.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function readSnapshot(
|
|
150
|
+
vault: Vault,
|
|
151
|
+
id: string,
|
|
152
|
+
): Promise<NoteSnapshot> {
|
|
153
|
+
const { path, relativePath } = pathFor(vault, id);
|
|
154
|
+
return readSnapshotAtPath(vault, path, relativePath);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function readOptionalSnapshot(
|
|
158
|
+
vault: Vault,
|
|
159
|
+
id: string,
|
|
160
|
+
): Promise<NoteSnapshot | null> {
|
|
161
|
+
try {
|
|
162
|
+
return await readSnapshot(vault, id);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (isErrno(error, "ENOENT")) return null;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function dependenciesFor(
|
|
170
|
+
overrides: Partial<AuthoringDependencies> | undefined,
|
|
171
|
+
): AuthoringDependencies {
|
|
172
|
+
return {
|
|
173
|
+
documentId: overrides?.documentId ?? randomUUID,
|
|
174
|
+
token: overrides?.token ?? randomUUID,
|
|
175
|
+
...(overrides?.beforeInstall === undefined
|
|
176
|
+
? {}
|
|
177
|
+
: { beforeInstall: overrides.beforeInstall }),
|
|
178
|
+
...(overrides?.beforeCommit === undefined
|
|
179
|
+
? {}
|
|
180
|
+
: { beforeCommit: overrides.beforeCommit }),
|
|
181
|
+
...(overrides?.afterSourceQuarantined === undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { afterSourceQuarantined: overrides.afterSourceQuarantined }),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function cleanupTemporary(
|
|
188
|
+
temporaryPath: string,
|
|
189
|
+
identity: { readonly device: bigint; readonly inode: bigint } | null,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
if (identity === null) return;
|
|
192
|
+
try {
|
|
193
|
+
const current = await lstat(temporaryPath, { bigint: true });
|
|
194
|
+
if (current.dev === identity.device && current.ino === identity.inode) {
|
|
195
|
+
await unlink(temporaryPath);
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!isErrno(error, "ENOENT")) throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function fsyncDirectory(
|
|
203
|
+
path: string,
|
|
204
|
+
openDirectory: typeof open = open,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const handle = await openDirectory(path, constants.O_RDONLY);
|
|
207
|
+
try {
|
|
208
|
+
await handle.sync();
|
|
209
|
+
} finally {
|
|
210
|
+
await handle.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function discoveredRecoveryLocations(
|
|
215
|
+
vault: Vault,
|
|
216
|
+
notePath: string,
|
|
217
|
+
): Promise<{
|
|
218
|
+
readonly recoverable: readonly RecoveryLocation[];
|
|
219
|
+
readonly empty: readonly RecoveryLocation[];
|
|
220
|
+
}> {
|
|
221
|
+
const directory = dirname(notePath);
|
|
222
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
223
|
+
if (entries.length > MAX_PARENT_DIRECTORY_ENTRIES) {
|
|
224
|
+
throw new Error("the note parent has too many entries for bounded recovery");
|
|
225
|
+
}
|
|
226
|
+
const prefix = `.${basename(notePath)}.`;
|
|
227
|
+
const suffix = ".recovery";
|
|
228
|
+
const matching = entries
|
|
229
|
+
.filter(({ name }) => name.startsWith(prefix) && name.endsWith(suffix))
|
|
230
|
+
.toSorted((left, right) => left.name.localeCompare(right.name));
|
|
231
|
+
if (matching.length > MAX_RECOVERY_LOCATIONS_PER_NOTE) {
|
|
232
|
+
const firstPath = join(directory, matching[0]?.name ?? "");
|
|
233
|
+
throw new NoteRecoveryRequiredError(
|
|
234
|
+
recoveryRelativePath(vault, notePath),
|
|
235
|
+
recoveryRelativePath(vault, firstPath),
|
|
236
|
+
new Error("too many interrupted authoring transactions require manual recovery"),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const recoverable: RecoveryLocation[] = [];
|
|
240
|
+
const empty: RecoveryLocation[] = [];
|
|
241
|
+
for (const entry of matching) {
|
|
242
|
+
const nonce = entry.name.slice(prefix.length, -suffix.length);
|
|
243
|
+
const recoveryDirectory = join(directory, entry.name);
|
|
244
|
+
const recoveryDirectoryRelative = recoveryRelativePath(vault, recoveryDirectory);
|
|
245
|
+
if (
|
|
246
|
+
!/^\d+\.[0-9a-f]{32}$/u.test(nonce)
|
|
247
|
+
|| !entry.isDirectory()
|
|
248
|
+
|| entry.isSymbolicLink()
|
|
249
|
+
) {
|
|
250
|
+
throw new NoteRecoveryRequiredError(
|
|
251
|
+
recoveryRelativePath(vault, notePath),
|
|
252
|
+
recoveryDirectoryRelative,
|
|
253
|
+
new Error("an unrecognized authoring recovery artifact is present"),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const metadata = await lstat(recoveryDirectory, { bigint: true });
|
|
257
|
+
if (
|
|
258
|
+
!metadata.isDirectory()
|
|
259
|
+
|| metadata.isSymbolicLink()
|
|
260
|
+
|| await realpath(recoveryDirectory) !== recoveryDirectory
|
|
261
|
+
) {
|
|
262
|
+
throw new NoteRecoveryRequiredError(
|
|
263
|
+
recoveryRelativePath(vault, notePath),
|
|
264
|
+
recoveryDirectoryRelative,
|
|
265
|
+
new Error("an authoring recovery directory changed identity"),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const children = await readdir(recoveryDirectory);
|
|
269
|
+
if (children.length === 0) {
|
|
270
|
+
empty.push({
|
|
271
|
+
directory: recoveryDirectory,
|
|
272
|
+
path: join(recoveryDirectory, basename(notePath)),
|
|
273
|
+
relativePath: recoveryRelativePath(
|
|
274
|
+
vault,
|
|
275
|
+
join(recoveryDirectory, basename(notePath)),
|
|
276
|
+
),
|
|
277
|
+
device: metadata.dev,
|
|
278
|
+
inode: metadata.ino,
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (children.length !== 1 || children[0] !== basename(notePath)) {
|
|
283
|
+
throw new NoteRecoveryRequiredError(
|
|
284
|
+
recoveryRelativePath(vault, notePath),
|
|
285
|
+
recoveryDirectoryRelative,
|
|
286
|
+
new Error("an authoring recovery directory has unexpected contents"),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const recoveryPath = join(recoveryDirectory, basename(notePath));
|
|
290
|
+
try {
|
|
291
|
+
await readSnapshotAtPath(
|
|
292
|
+
vault,
|
|
293
|
+
recoveryPath,
|
|
294
|
+
recoveryRelativePath(vault, recoveryPath),
|
|
295
|
+
);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw new NoteRecoveryRequiredError(
|
|
298
|
+
recoveryRelativePath(vault, notePath),
|
|
299
|
+
recoveryRelativePath(vault, recoveryPath),
|
|
300
|
+
error,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
recoverable.push({
|
|
304
|
+
directory: recoveryDirectory,
|
|
305
|
+
path: recoveryPath,
|
|
306
|
+
relativePath: recoveryRelativePath(vault, recoveryPath),
|
|
307
|
+
device: metadata.dev,
|
|
308
|
+
inode: metadata.ino,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return { recoverable, empty };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function directoryIdentity(
|
|
315
|
+
vault: Vault,
|
|
316
|
+
notePath: string,
|
|
317
|
+
): Promise<DirectoryIdentity> {
|
|
318
|
+
await assertSafeParent(vault, notePath);
|
|
319
|
+
const metadata = await lstat(dirname(notePath), { bigint: true });
|
|
320
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
321
|
+
throw new Error("the note parent must remain a real directory");
|
|
322
|
+
}
|
|
323
|
+
return { device: metadata.dev, inode: metadata.ino };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function assertSameDirectory(
|
|
327
|
+
vault: Vault,
|
|
328
|
+
notePath: string,
|
|
329
|
+
expected: DirectoryIdentity,
|
|
330
|
+
): Promise<void> {
|
|
331
|
+
const current = await directoryIdentity(vault, notePath);
|
|
332
|
+
if (current.device !== expected.device || current.inode !== expected.inode) {
|
|
333
|
+
throw new Error("the note parent changed during authoring");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function createRecoveryLocation(
|
|
338
|
+
vault: Vault,
|
|
339
|
+
path: string,
|
|
340
|
+
dependencies: AuthoringDependencies,
|
|
341
|
+
): Promise<RecoveryLocation> {
|
|
342
|
+
const directory = dirname(path);
|
|
343
|
+
const recoveryDirectory = join(
|
|
344
|
+
directory,
|
|
345
|
+
`.${basename(path)}.${process.pid}.${sha256(dependencies.token()).slice(0, 32)}.recovery`,
|
|
346
|
+
);
|
|
347
|
+
await mkdir(recoveryDirectory, { mode: 0o700 });
|
|
348
|
+
const metadata = await lstat(recoveryDirectory, { bigint: true });
|
|
349
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
350
|
+
throw new Error("the recovery location is not a private directory");
|
|
351
|
+
}
|
|
352
|
+
await fsyncDirectory(directory);
|
|
353
|
+
const recoveryPath = join(recoveryDirectory, basename(path));
|
|
354
|
+
return {
|
|
355
|
+
directory: recoveryDirectory,
|
|
356
|
+
path: recoveryPath,
|
|
357
|
+
relativePath: relative(vault.root, recoveryPath).split(sep).join("/"),
|
|
358
|
+
device: metadata.dev,
|
|
359
|
+
inode: metadata.ino,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function assertRecoveryLocation(recovery: RecoveryLocation): Promise<void> {
|
|
364
|
+
const metadata = await lstat(recovery.directory, { bigint: true });
|
|
365
|
+
if (
|
|
366
|
+
!metadata.isDirectory()
|
|
367
|
+
|| metadata.isSymbolicLink()
|
|
368
|
+
|| metadata.dev !== recovery.device
|
|
369
|
+
|| metadata.ino !== recovery.inode
|
|
370
|
+
|| await realpath(recovery.directory) !== recovery.directory
|
|
371
|
+
) {
|
|
372
|
+
throw new Error("the recovery location changed during authoring");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function removeRecoveryDirectory(
|
|
377
|
+
recovery: RecoveryLocation,
|
|
378
|
+
parentDirectory: string,
|
|
379
|
+
): Promise<void> {
|
|
380
|
+
await rmdir(recovery.directory);
|
|
381
|
+
await fsyncDirectory(parentDirectory);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function restoreQuarantinedSource(
|
|
385
|
+
vault: Vault,
|
|
386
|
+
recovery: RecoveryLocation,
|
|
387
|
+
path: string,
|
|
388
|
+
expectedDirectory: DirectoryIdentity,
|
|
389
|
+
): Promise<boolean> {
|
|
390
|
+
await assertSameDirectory(vault, path, expectedDirectory);
|
|
391
|
+
await assertRecoveryLocation(recovery);
|
|
392
|
+
try {
|
|
393
|
+
// link(2) is the portable no-clobber primitive: unlike rename, it returns
|
|
394
|
+
// EEXIST rather than replacing a writer that recreated the source path.
|
|
395
|
+
await link(recovery.path, path);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
await unlink(recovery.path);
|
|
401
|
+
await fsyncDirectory(recovery.directory);
|
|
402
|
+
await removeRecoveryDirectory(recovery, dirname(path));
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function assertNoInterruptedRecovery(
|
|
407
|
+
vault: Vault,
|
|
408
|
+
id: string,
|
|
409
|
+
): Promise<void> {
|
|
410
|
+
const { path, relativePath } = pathFor(vault, id);
|
|
411
|
+
await assertSafeParent(vault, path);
|
|
412
|
+
const artifacts = await discoveredRecoveryLocations(vault, path);
|
|
413
|
+
const first = artifacts.recoverable[0] ?? artifacts.empty[0];
|
|
414
|
+
if (first !== undefined) {
|
|
415
|
+
throw new NoteRecoveryRequiredError(
|
|
416
|
+
relativePath,
|
|
417
|
+
first.relativePath,
|
|
418
|
+
new Error("an interrupted authoring transaction requires a writer to recover it"),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function recoverInterruptedAuthoring(
|
|
424
|
+
vault: Vault,
|
|
425
|
+
id: string,
|
|
426
|
+
lock: NoteLock,
|
|
427
|
+
): Promise<void> {
|
|
428
|
+
const { path, relativePath } = pathFor(vault, id);
|
|
429
|
+
await lock.assertOwned();
|
|
430
|
+
await assertSafeParent(vault, path);
|
|
431
|
+
const artifacts = await discoveredRecoveryLocations(vault, path);
|
|
432
|
+
for (const emptyRecovery of artifacts.empty) {
|
|
433
|
+
await assertRecoveryLocation(emptyRecovery);
|
|
434
|
+
await removeRecoveryDirectory(emptyRecovery, dirname(path));
|
|
435
|
+
}
|
|
436
|
+
const first = artifacts.recoverable[0];
|
|
437
|
+
if (first === undefined) return;
|
|
438
|
+
if (artifacts.recoverable.length !== 1) {
|
|
439
|
+
throw new NoteRecoveryRequiredError(
|
|
440
|
+
relativePath,
|
|
441
|
+
first.relativePath,
|
|
442
|
+
new Error("multiple interrupted authoring transactions require manual recovery"),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const current = await readOptionalSnapshot(vault, id);
|
|
447
|
+
if (current !== null) {
|
|
448
|
+
throw new NoteRecoveryRequiredError(
|
|
449
|
+
relativePath,
|
|
450
|
+
first.relativePath,
|
|
451
|
+
new Error("both the canonical note and displaced bytes exist"),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const expectedDirectory = await directoryIdentity(vault, path);
|
|
455
|
+
if (!await restoreQuarantinedSource(vault, first, path, expectedDirectory)) {
|
|
456
|
+
throw new NoteRecoveryRequiredError(
|
|
457
|
+
relativePath,
|
|
458
|
+
first.relativePath,
|
|
459
|
+
new Error("the canonical note was recreated during interrupted recovery"),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
await lock.assertOwned();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function installTemporaryWithoutClobber(
|
|
466
|
+
temporaryPath: string,
|
|
467
|
+
path: string,
|
|
468
|
+
): Promise<boolean> {
|
|
469
|
+
try {
|
|
470
|
+
// The temporary file has already been fsync'd. Linking gives the final
|
|
471
|
+
// name to that exact inode only if the name is still absent.
|
|
472
|
+
await link(temporaryPath, path);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (isErrno(error, "EEXIST")) return false;
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
await unlink(temporaryPath);
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export async function currentRevisionOrNull(
|
|
482
|
+
vault: Vault,
|
|
483
|
+
id: string,
|
|
484
|
+
): Promise<NoteRevision | null> {
|
|
485
|
+
try {
|
|
486
|
+
return (await readOptionalSnapshot(vault, id))?.revision ?? null;
|
|
487
|
+
} catch {
|
|
488
|
+
// A raced directory, link, or non-UTF-8 file is still a conflict. Do not
|
|
489
|
+
// inspect or mutate it further merely to improve an error field.
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Preserve Promise.all's first native failure, retaining both admitted handles. */
|
|
495
|
+
export async function joinDirectorySyncs(
|
|
496
|
+
paths: readonly [string, string],
|
|
497
|
+
sync: (path: string) => Promise<void>,
|
|
498
|
+
): Promise<void> {
|
|
499
|
+
const pending = paths.map((path) => {
|
|
500
|
+
// An injected native port may throw before returning its Promise. Observe
|
|
501
|
+
// that admission failure without abandoning another admitted operation.
|
|
502
|
+
try { return sync(path); }
|
|
503
|
+
catch (error: unknown) { return Promise.reject(error); }
|
|
504
|
+
});
|
|
505
|
+
try {
|
|
506
|
+
await Promise.all(pending);
|
|
507
|
+
} catch (error: unknown) {
|
|
508
|
+
await Promise.allSettled(pending);
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Internal physical boundary. It never owns a complete note-publication transaction. */
|
|
514
|
+
export const nativeAuthoringPlatform = {
|
|
515
|
+
resolveVault,
|
|
516
|
+
acquireNoteLock,
|
|
517
|
+
dependenciesFor,
|
|
518
|
+
readSnapshot,
|
|
519
|
+
readOptionalSnapshot,
|
|
520
|
+
readSnapshotAtPath,
|
|
521
|
+
recoverInterruptedAuthoring,
|
|
522
|
+
assertSafeParent,
|
|
523
|
+
directoryIdentity,
|
|
524
|
+
assertSameDirectory,
|
|
525
|
+
createRecoveryLocation,
|
|
526
|
+
assertRecoveryLocation,
|
|
527
|
+
removeRecoveryDirectory,
|
|
528
|
+
restoreQuarantinedSource,
|
|
529
|
+
installTemporaryWithoutClobber,
|
|
530
|
+
currentRevisionOrNull,
|
|
531
|
+
cleanupTemporary,
|
|
532
|
+
fsyncDirectory,
|
|
533
|
+
rename,
|
|
534
|
+
unlink,
|
|
535
|
+
directoryPath: dirname,
|
|
536
|
+
temporaryPath(path: string, dependencies: AuthoringDependencies): string {
|
|
537
|
+
return join(dirname(path), `.${basename(path)}.${process.pid}.${sha256(dependencies.token()).slice(0, 32)}.tmp`);
|
|
538
|
+
},
|
|
539
|
+
openTemporary(path: string, mode: number) {
|
|
540
|
+
return open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode);
|
|
541
|
+
},
|
|
542
|
+
statTemporary(path: string) {
|
|
543
|
+
return lstat(path, { bigint: true });
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
export type AuthoringPlatform = typeof nativeAuthoringPlatform;
|
|
548
|
+
|
|
549
|
+
export type TemporaryNoteHandle = Awaited<ReturnType<typeof open>>;
|