@semiont/content 0.5.29 → 0.5.31
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/dist/index.d.ts +134 -151
- package/dist/index.js +128 -103
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,87 @@
|
|
|
1
1
|
// src/working-tree-store.ts
|
|
2
2
|
import { promises as fs, createReadStream, createWriteStream } from "fs";
|
|
3
|
-
import { execFileSync } from "child_process";
|
|
4
3
|
import { createHash, randomUUID } from "crypto";
|
|
5
4
|
import { Readable } from "stream";
|
|
6
5
|
import { pipeline } from "stream/promises";
|
|
7
6
|
import path from "path";
|
|
7
|
+
|
|
8
|
+
// src/git-staging.ts
|
|
9
|
+
import { execFile } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
import { recordGitCommand } from "@semiont/observability";
|
|
12
|
+
var run = promisify(execFile);
|
|
13
|
+
var DEFAULT_FLUSH_MS = 250;
|
|
14
|
+
var DEFAULT_MAX_WAIT_MS = 2e3;
|
|
15
|
+
function createStager(cwd, options = {}) {
|
|
16
|
+
const flushMs = options.flushMs ?? DEFAULT_FLUSH_MS;
|
|
17
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
|
|
18
|
+
const queued = /* @__PURE__ */ new Set();
|
|
19
|
+
let timer;
|
|
20
|
+
let oldestAt;
|
|
21
|
+
let inFlight = Promise.resolve();
|
|
22
|
+
let disposed = false;
|
|
23
|
+
const git = async (args) => {
|
|
24
|
+
const started = performance.now();
|
|
25
|
+
try {
|
|
26
|
+
await run("git", args, { cwd });
|
|
27
|
+
} finally {
|
|
28
|
+
recordGitCommand(args[0] ?? "git", performance.now() - started);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const serialize = (work) => {
|
|
32
|
+
inFlight = inFlight.then(work, work);
|
|
33
|
+
return inFlight;
|
|
34
|
+
};
|
|
35
|
+
const clearTimer = () => {
|
|
36
|
+
if (timer) {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = void 0;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const drain = () => {
|
|
42
|
+
clearTimer();
|
|
43
|
+
if (queued.size === 0) return inFlight;
|
|
44
|
+
const batch = [...queued];
|
|
45
|
+
queued.clear();
|
|
46
|
+
oldestAt = void 0;
|
|
47
|
+
return serialize(() => git(["add", ...batch]));
|
|
48
|
+
};
|
|
49
|
+
const arm = () => {
|
|
50
|
+
clearTimer();
|
|
51
|
+
if (disposed || queued.size === 0) return;
|
|
52
|
+
const sinceOldest = oldestAt === void 0 ? 0 : Date.now() - oldestAt;
|
|
53
|
+
const wait = Math.max(0, Math.min(flushMs, maxWaitMs - sinceOldest));
|
|
54
|
+
timer = setTimeout(() => {
|
|
55
|
+
void drain();
|
|
56
|
+
}, wait);
|
|
57
|
+
timer.unref?.();
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
add(path4) {
|
|
61
|
+
if (disposed) return;
|
|
62
|
+
if (queued.size === 0) oldestAt = Date.now();
|
|
63
|
+
queued.add(path4);
|
|
64
|
+
arm();
|
|
65
|
+
},
|
|
66
|
+
run(args) {
|
|
67
|
+
return drain().then(() => serialize(() => git(args)));
|
|
68
|
+
},
|
|
69
|
+
flush() {
|
|
70
|
+
return drain();
|
|
71
|
+
},
|
|
72
|
+
pending() {
|
|
73
|
+
return queued.size;
|
|
74
|
+
},
|
|
75
|
+
async dispose() {
|
|
76
|
+
await drain();
|
|
77
|
+
disposed = true;
|
|
78
|
+
clearTimer();
|
|
79
|
+
await inFlight;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/working-tree-store.ts
|
|
8
85
|
function hashingTap() {
|
|
9
86
|
const hash = createHash("sha256");
|
|
10
87
|
let byteSize = 0;
|
|
@@ -25,33 +102,40 @@ var WorkingTreeStore = class {
|
|
|
25
102
|
projectRoot;
|
|
26
103
|
gitSync;
|
|
27
104
|
logger;
|
|
28
|
-
|
|
105
|
+
_stager;
|
|
106
|
+
staging;
|
|
107
|
+
/** `staging` is policy — how stale the index may get is the caller's call. */
|
|
108
|
+
constructor(project, logger, staging = {}) {
|
|
29
109
|
this.projectRoot = project.root;
|
|
30
110
|
this.gitSync = project.gitSync;
|
|
31
111
|
this.logger = logger;
|
|
112
|
+
this.staging = staging;
|
|
113
|
+
}
|
|
114
|
+
/** Created on first use — importers of this package may never stage. */
|
|
115
|
+
stager() {
|
|
116
|
+
if (!this._stager) this._stager = createStager(this.projectRoot, this.staging);
|
|
117
|
+
return this._stager;
|
|
118
|
+
}
|
|
119
|
+
/** Stage everything pending now — for a caller that wants the index current. */
|
|
120
|
+
flushStaging() {
|
|
121
|
+
return this._stager ? this._stager.flush() : Promise.resolve();
|
|
122
|
+
}
|
|
123
|
+
/** Drain and stop. A stopped process must leave nothing unstaged. */
|
|
124
|
+
async dispose() {
|
|
125
|
+
if (this._stager) await this._stager.dispose();
|
|
32
126
|
}
|
|
33
127
|
shouldRunGit(noGit) {
|
|
34
128
|
return this.gitSync && !noGit;
|
|
35
129
|
}
|
|
36
130
|
/**
|
|
37
|
-
* Write
|
|
38
|
-
*
|
|
39
|
-
* API/GUI/AI path: caller provides bytes — as a Buffer it already holds, or
|
|
40
|
-
* as a stream (the Archivist's write endpoint hands the request body
|
|
41
|
-
* straight through, SINGLE-KB-MOUNT P2/D7: memory stays bounded by the
|
|
42
|
-
* chunk, never the representation).
|
|
131
|
+
* Write bytes to the path storageUri names, whole or streamed.
|
|
43
132
|
*
|
|
44
|
-
* Atomic
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* overwritten survives) and no temp file behind, so the Stower's `register`
|
|
49
|
-
* can never find partial bytes an event names.
|
|
133
|
+
* Atomic: bytes land in a temp file and are renamed into place only once
|
|
134
|
+
* complete and once `expectedChecksum`, when given, agrees. A mismatch or a
|
|
135
|
+
* torn stream leaves the target untouched, so `register` can never find
|
|
136
|
+
* partial bytes an event names.
|
|
50
137
|
*
|
|
51
|
-
* @param content - Raw bytes to write, whole or streamed
|
|
52
|
-
* @param storageUri - file:// URI (e.g. "file://docs/overview.md")
|
|
53
138
|
* @throws ChecksumMismatchError when expectedChecksum disagrees with the body
|
|
54
|
-
* @returns Stored resource metadata
|
|
55
139
|
*/
|
|
56
140
|
async store(content, storageUri, options) {
|
|
57
141
|
const filePath = this.resolveUri(storageUri);
|
|
@@ -78,7 +162,7 @@ var WorkingTreeStore = class {
|
|
|
78
162
|
}
|
|
79
163
|
await fs.rename(tempPath, filePath);
|
|
80
164
|
if (this.shouldRunGit(options?.noGit)) {
|
|
81
|
-
|
|
165
|
+
this.stager().add(filePath);
|
|
82
166
|
}
|
|
83
167
|
this.logger?.info("Resource stored", { storageUri, checksum, byteSize });
|
|
84
168
|
return {
|
|
@@ -93,17 +177,9 @@ var WorkingTreeStore = class {
|
|
|
93
177
|
}
|
|
94
178
|
}
|
|
95
179
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* The file is already on disk; this hashes it by streaming to confirm what
|
|
99
|
-
* it is, then stages it. If expectedChecksum is provided, throws
|
|
100
|
-
* ChecksumMismatchError on mismatch.
|
|
180
|
+
* Adopt a file already on disk: stream it to hash it, then stage it.
|
|
101
181
|
*
|
|
102
|
-
* @
|
|
103
|
-
* @param expectedChecksum - Optional SHA-256 to verify against
|
|
104
|
-
* @returns Stored resource metadata
|
|
105
|
-
* @throws ChecksumMismatchError if expectedChecksum is provided and does not match
|
|
106
|
-
* @throws Error if file does not exist
|
|
182
|
+
* @throws ChecksumMismatchError if expectedChecksum is given and disagrees
|
|
107
183
|
*/
|
|
108
184
|
async register(storageUri, expectedChecksum, options) {
|
|
109
185
|
const filePath = this.resolveUri(storageUri);
|
|
@@ -117,7 +193,7 @@ var WorkingTreeStore = class {
|
|
|
117
193
|
throw new ChecksumMismatchError(storageUri, expectedChecksum, checksum);
|
|
118
194
|
}
|
|
119
195
|
if (this.shouldRunGit(options?.noGit)) {
|
|
120
|
-
|
|
196
|
+
this.stager().add(filePath);
|
|
121
197
|
}
|
|
122
198
|
const byteSize = tap.byteSize;
|
|
123
199
|
this.logger?.info("Resource registered", { storageUri, checksum, byteSize });
|
|
@@ -129,22 +205,9 @@ var WorkingTreeStore = class {
|
|
|
129
205
|
};
|
|
130
206
|
}
|
|
131
207
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* @returns Raw bytes
|
|
136
|
-
*/
|
|
137
|
-
/**
|
|
138
|
-
* The same bytes as `retrieve`, streamed — for the byte paths that must not
|
|
139
|
-
* hold a whole representation in memory (SINGLE-KB-MOUNT D7: the Archivist
|
|
140
|
-
* serves content for every reader now, so its memory cannot be bounded by
|
|
141
|
-
* the largest file anyone asks for).
|
|
142
|
-
*
|
|
143
|
-
* Lazy by construction: the stream is created here but nothing is read
|
|
144
|
-
* until the caller iterates, so a missing file surfaces as an `error` event
|
|
145
|
-
* on the stream rather than a rejected promise. Callers that need the
|
|
146
|
-
* distinction up front should resolve the descriptor first — which is what
|
|
147
|
-
* `resolveRepresentation` does.
|
|
208
|
+
* The same bytes as `retrieve`, streamed. Lazy: a missing file surfaces as
|
|
209
|
+
* an `error` event on the stream, not a rejected promise — callers needing
|
|
210
|
+
* that up front should resolve the descriptor first.
|
|
148
211
|
*/
|
|
149
212
|
retrieveStream(storageUri) {
|
|
150
213
|
return createReadStream(this.resolveUri(storageUri));
|
|
@@ -160,42 +223,20 @@ var WorkingTreeStore = class {
|
|
|
160
223
|
throw error;
|
|
161
224
|
}
|
|
162
225
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Move a file from one URI to another.
|
|
165
|
-
*
|
|
166
|
-
* If .git/ exists in the project root and noGit is not set, runs `git mv`.
|
|
167
|
-
* Otherwise (no .git/ or noGit: true), runs fs.rename.
|
|
168
|
-
*
|
|
169
|
-
* @param fromUri - Current file:// URI
|
|
170
|
-
* @param toUri - New file:// URI
|
|
171
|
-
* @param options.noGit - Skip git mv even if .git/ is present
|
|
172
|
-
*/
|
|
226
|
+
/** `git mv` when the project syncs git, `fs.rename` otherwise. */
|
|
173
227
|
async move(fromUri, toUri, options) {
|
|
174
228
|
const fromPath = this.resolveUri(fromUri);
|
|
175
229
|
const toPath = this.resolveUri(toUri);
|
|
176
230
|
this.logger?.debug("Moving resource", { fromUri, toUri });
|
|
177
231
|
await fs.mkdir(path.dirname(toPath), { recursive: true });
|
|
178
232
|
if (this.shouldRunGit(options?.noGit)) {
|
|
179
|
-
|
|
233
|
+
await this.stager().run(["mv", fromPath, toPath]);
|
|
180
234
|
} else {
|
|
181
235
|
await fs.rename(fromPath, toPath);
|
|
182
236
|
}
|
|
183
237
|
this.logger?.info("Resource moved", { fromUri, toUri });
|
|
184
238
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Remove a file from the working tree.
|
|
187
|
-
*
|
|
188
|
-
* If .git/ exists and noGit is not set:
|
|
189
|
-
* - keepFile false (default): runs `git rm` (removes from index and disk)
|
|
190
|
-
* - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)
|
|
191
|
-
* If no .git/ or noGit: true:
|
|
192
|
-
* - keepFile false: runs fs.unlink
|
|
193
|
-
* - keepFile true: no-op on filesystem
|
|
194
|
-
*
|
|
195
|
-
* @param storageUri - file:// URI
|
|
196
|
-
* @param options.noGit - Skip git rm even if .git/ is present
|
|
197
|
-
* @param options.keepFile - Remove from git index only; leave file on disk
|
|
198
|
-
*/
|
|
239
|
+
/** @param options.keepFile - Drop from the index only; leave the file on disk. */
|
|
199
240
|
async remove(storageUri, options) {
|
|
200
241
|
const filePath = this.resolveUri(storageUri);
|
|
201
242
|
const keepFile = options?.keepFile ?? false;
|
|
@@ -203,7 +244,7 @@ var WorkingTreeStore = class {
|
|
|
203
244
|
const useGit = this.shouldRunGit(options?.noGit);
|
|
204
245
|
if (useGit) {
|
|
205
246
|
const gitArgs = keepFile ? ["rm", "--cached", filePath] : ["rm", filePath];
|
|
206
|
-
|
|
247
|
+
await this.stager().run(gitArgs);
|
|
207
248
|
this.logger?.info("Resource removed", { storageUri, keepFile, git: true });
|
|
208
249
|
return;
|
|
209
250
|
}
|
|
@@ -222,14 +263,6 @@ var WorkingTreeStore = class {
|
|
|
222
263
|
throw error;
|
|
223
264
|
}
|
|
224
265
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Convert a file:// URI to an absolute filesystem path.
|
|
227
|
-
*
|
|
228
|
-
* "file://docs/overview.md" → "{projectRoot}/docs/overview.md"
|
|
229
|
-
*
|
|
230
|
-
* @param storageUri - file:// URI
|
|
231
|
-
* @returns Absolute path
|
|
232
|
-
*/
|
|
233
266
|
resolveUri(storageUri) {
|
|
234
267
|
if (!storageUri.startsWith("file://")) {
|
|
235
268
|
throw new Error(`Invalid storage URI (must start with file://): ${storageUri}`);
|
|
@@ -265,8 +298,8 @@ function verifyChecksum(content, checksum) {
|
|
|
265
298
|
return calculateChecksum(content) === checksum;
|
|
266
299
|
}
|
|
267
300
|
|
|
268
|
-
// src/
|
|
269
|
-
import {
|
|
301
|
+
// src/text-extractor.ts
|
|
302
|
+
import { yieldsGeometryOf } from "@semiont/core";
|
|
270
303
|
|
|
271
304
|
// src/pdf-extractor.ts
|
|
272
305
|
import { isObject as isObject4 } from "@semiont/core";
|
|
@@ -815,18 +848,17 @@ function shapeTables(layer) {
|
|
|
815
848
|
}
|
|
816
849
|
var pdfExtractor = {
|
|
817
850
|
// Every non-declined PDF extraction carries positioned runs — native text
|
|
818
|
-
// layers and OCR both anchor by page geometry.
|
|
819
|
-
|
|
851
|
+
// layers and OCR both anchor by page geometry. That fact is declared in core
|
|
852
|
+
// ('pdf-text-layer' → true) rather than here; this comment records the
|
|
853
|
+
// behavior the census gate holds core's answer to.
|
|
820
854
|
async extract(content, _mediaType, cache) {
|
|
821
|
-
const hit = await cache
|
|
855
|
+
const hit = await cache.store.read(cache.key);
|
|
822
856
|
if (hit) return hit;
|
|
823
857
|
const outcome = await extractPdf(content);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
} catch {
|
|
829
|
-
}
|
|
858
|
+
try {
|
|
859
|
+
if (outcome.kind === "declined") await cache.store.write(cache.key, outcome);
|
|
860
|
+
else if (outcome.items) await cache.store.write(cache.key, { ...outcome, items: outcome.items });
|
|
861
|
+
} catch {
|
|
830
862
|
}
|
|
831
863
|
return outcome;
|
|
832
864
|
}
|
|
@@ -881,18 +913,10 @@ async function extractPdf(content) {
|
|
|
881
913
|
};
|
|
882
914
|
}
|
|
883
915
|
|
|
884
|
-
// src/
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
return { kind: "extracted", text: decodeRepresentation(content, mediaType), method: "text-passthrough" };
|
|
889
|
-
}
|
|
890
|
-
};
|
|
891
|
-
var EXTRACTORS = {
|
|
892
|
-
"decode": passthroughExtractor,
|
|
893
|
-
"pdf-text-layer": pdfExtractor,
|
|
894
|
-
"none": null
|
|
895
|
-
};
|
|
916
|
+
// src/text-extractor.ts
|
|
917
|
+
function derivingExtractorFor(mediaType) {
|
|
918
|
+
return yieldsGeometryOf(mediaType) ? pdfExtractor : null;
|
|
919
|
+
}
|
|
896
920
|
|
|
897
921
|
// src/anchored-text-store.ts
|
|
898
922
|
import fs2 from "fs";
|
|
@@ -1094,13 +1118,14 @@ function archivistContentReads(config) {
|
|
|
1094
1118
|
}
|
|
1095
1119
|
export {
|
|
1096
1120
|
ChecksumMismatchError,
|
|
1097
|
-
EXTRACTORS,
|
|
1098
1121
|
MAX_PDF_BYTES,
|
|
1099
1122
|
RepresentationMissing,
|
|
1100
1123
|
WorkingTreeStore,
|
|
1101
1124
|
archivistContentReads,
|
|
1102
1125
|
calculateChecksum,
|
|
1103
1126
|
createAnchoredTextStore,
|
|
1127
|
+
createStager,
|
|
1128
|
+
derivingExtractorFor,
|
|
1104
1129
|
extractPdfTextLayer,
|
|
1105
1130
|
verifyChecksum,
|
|
1106
1131
|
withinByteBudget
|