@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 CHANGED
@@ -1,155 +1,102 @@
1
1
  import { Readable } from 'stream';
2
2
  import { SemiontProject, ArchivistAddressConfig } from '@semiont/core/node';
3
- import { Logger, ExtractionOutcome, PdfTextItem, TextExtraction, IContentTransport, AnchoredText } from '@semiont/core';
3
+ import { Logger, StoredResource, ExtractionOutcome, PdfTextItem, IContentTransport, AnchoredText } from '@semiont/core';
4
4
 
5
5
  /**
6
- * WorkingTreeStore - Manages files in the project working tree
7
- *
8
- * Unlike the old content-addressed RepresentationStore, this store treats
9
- * the working tree (project root) as the source of truth for file content.
10
- * Resources are identified by their file:// URI, which is stable across
11
- * content changes and moves (tracked by events).
12
- *
13
- * Two write paths:
14
- * - store(content, storageUri): Write bytes to disk (API/GUI/AI path).
15
- * Used when the file does not yet exist and the caller provides content.
16
- * - register(storageUri, expectedChecksum?): Adopt a file already on disk and
17
- * return its metadata. The CLI path (the file arrived by other means) and
18
- * the event-apply path (the Stower staging bytes an event names) both use
19
- * it. Streams the file to hash it — never holds it. If expectedChecksum is
20
- * provided, throws on mismatch.
21
- *
22
- * Storage layout:
23
- * {projectRoot}/{path-from-uri}
24
- *
25
- * For example, storageUri "file://docs/overview.md" resolves to
26
- * {projectRoot}/docs/overview.md
27
- */
28
-
29
- /**
30
- * Result of store() or register()
6
+ * Deferred, deduped `git add`. The index is for humans who commit by hand, so
7
+ * it must be current within seconds, not after every change.
8
+ *
9
+ * Serialized per repo git's index is single-writer, and concurrent `git add`
10
+ * fails on `index.lock` rather than retrying. Created on first use, never at
11
+ * import: consumers of this package may never stage anything.
31
12
  */
32
- interface StoredResource {
33
- storageUri: string;
34
- checksum: string;
35
- byteSize: number;
36
- created: string;
13
+ interface StagerOptions {
14
+ /** Quiet period after the last change before staging. */
15
+ flushMs?: number;
16
+ /** Ceiling on staleness: stage this long after the OLDEST pending path even
17
+ * if changes keep arriving. Without it a continuous append stream resets
18
+ * the debounce forever and the index never updates. */
19
+ maxWaitMs?: number;
20
+ }
21
+ interface Stager {
22
+ /** Queue a path. Returns immediately; deduped against what is pending. */
23
+ add(path: string): void;
24
+ /** Run an order-sensitive command (`mv`, `rm`): pending adds flush first,
25
+ * then this runs alone — it must not overtake the adds it depends on. */
26
+ run(args: string[]): Promise<void>;
27
+ /** Stage everything pending now. */
28
+ flush(): Promise<void>;
29
+ /** Paths queued and not yet staged. */
30
+ pending(): number;
31
+ /** Drain and stop. A stopped process must leave nothing unstaged. */
32
+ dispose(): Promise<void>;
37
33
  }
34
+ declare function createStager(cwd: string, options?: StagerOptions): Stager;
35
+
38
36
  /**
39
- * Manages files in the project working tree
37
+ * Files in the project working tree, addressed by file:// URI —
38
+ * "file://docs/overview.md" is {projectRoot}/docs/overview.md.
39
+ *
40
+ * `store` writes bytes the caller supplies; `register` adopts a file already
41
+ * on disk. Both stream to hash, neither holds a representation in memory.
40
42
  */
43
+
41
44
  declare class WorkingTreeStore {
42
45
  private projectRoot;
43
46
  private gitSync;
44
47
  private logger?;
45
- constructor(project: SemiontProject, logger?: Logger);
48
+ private _stager?;
49
+ private readonly staging;
50
+ /** `staging` is policy — how stale the index may get is the caller's call. */
51
+ constructor(project: SemiontProject, logger?: Logger, staging?: StagerOptions);
52
+ /** Created on first use — importers of this package may never stage. */
53
+ private stager;
54
+ /** Stage everything pending now — for a caller that wants the index current. */
55
+ flushStaging(): Promise<void>;
56
+ /** Drain and stop. A stopped process must leave nothing unstaged. */
57
+ dispose(): Promise<void>;
46
58
  private shouldRunGit;
47
59
  /**
48
- * Write content to disk at the location indicated by storageUri.
49
- *
50
- * API/GUI/AI path: caller provides bytes — as a Buffer it already holds, or
51
- * as a stream (the Archivist's write endpoint hands the request body
52
- * straight through, SINGLE-KB-MOUNT P2/D7: memory stays bounded by the
53
- * chunk, never the representation).
60
+ * Write bytes to the path storageUri names, whole or streamed.
54
61
  *
55
- * Atomic either way: bytes stream into a temp file beside the target and
56
- * are renamed into place only once complete and only once
57
- * `expectedChecksum`, when given, agrees with what actually arrived. A
58
- * mismatch or a torn stream leaves the target untouched (a version being
59
- * overwritten survives) and no temp file behind, so the Stower's `register`
60
- * can never find partial bytes an event names.
62
+ * Atomic: bytes land in a temp file and are renamed into place only once
63
+ * complete and once `expectedChecksum`, when given, agrees. A mismatch or a
64
+ * torn stream leaves the target untouched, so `register` can never find
65
+ * partial bytes an event names.
61
66
  *
62
- * @param content - Raw bytes to write, whole or streamed
63
- * @param storageUri - file:// URI (e.g. "file://docs/overview.md")
64
67
  * @throws ChecksumMismatchError when expectedChecksum disagrees with the body
65
- * @returns Stored resource metadata
66
68
  */
67
69
  store(content: Buffer | Readable, storageUri: string, options?: {
68
70
  noGit?: boolean;
69
71
  expectedChecksum?: string;
70
72
  }): Promise<StoredResource>;
71
73
  /**
72
- * Read an existing file and return its metadata.
74
+ * Adopt a file already on disk: stream it to hash it, then stage it.
73
75
  *
74
- * The file is already on disk; this hashes it by streaming to confirm what
75
- * it is, then stages it. If expectedChecksum is provided, throws
76
- * ChecksumMismatchError on mismatch.
77
- *
78
- * @param storageUri - file:// URI (e.g. "file://docs/overview.md")
79
- * @param expectedChecksum - Optional SHA-256 to verify against
80
- * @returns Stored resource metadata
81
- * @throws ChecksumMismatchError if expectedChecksum is provided and does not match
82
- * @throws Error if file does not exist
76
+ * @throws ChecksumMismatchError if expectedChecksum is given and disagrees
83
77
  */
84
78
  register(storageUri: string, expectedChecksum?: string, options?: {
85
79
  noGit?: boolean;
86
80
  }): Promise<StoredResource>;
87
81
  /**
88
- * Read file content by URI.
89
- *
90
- * @param storageUri - file:// URI
91
- * @returns Raw bytes
92
- */
93
- /**
94
- * The same bytes as `retrieve`, streamed — for the byte paths that must not
95
- * hold a whole representation in memory (SINGLE-KB-MOUNT D7: the Archivist
96
- * serves content for every reader now, so its memory cannot be bounded by
97
- * the largest file anyone asks for).
98
- *
99
- * Lazy by construction: the stream is created here but nothing is read
100
- * until the caller iterates, so a missing file surfaces as an `error` event
101
- * on the stream rather than a rejected promise. Callers that need the
102
- * distinction up front should resolve the descriptor first — which is what
103
- * `resolveRepresentation` does.
82
+ * The same bytes as `retrieve`, streamed. Lazy: a missing file surfaces as
83
+ * an `error` event on the stream, not a rejected promise — callers needing
84
+ * that up front should resolve the descriptor first.
104
85
  */
105
86
  retrieveStream(storageUri: string): Readable;
106
87
  retrieve(storageUri: string): Promise<Buffer>;
107
- /**
108
- * Move a file from one URI to another.
109
- *
110
- * If .git/ exists in the project root and noGit is not set, runs `git mv`.
111
- * Otherwise (no .git/ or noGit: true), runs fs.rename.
112
- *
113
- * @param fromUri - Current file:// URI
114
- * @param toUri - New file:// URI
115
- * @param options.noGit - Skip git mv even if .git/ is present
116
- */
88
+ /** `git mv` when the project syncs git, `fs.rename` otherwise. */
117
89
  move(fromUri: string, toUri: string, options?: {
118
90
  noGit?: boolean;
119
91
  }): Promise<void>;
120
- /**
121
- * Remove a file from the working tree.
122
- *
123
- * If .git/ exists and noGit is not set:
124
- * - keepFile false (default): runs `git rm` (removes from index and disk)
125
- * - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)
126
- * If no .git/ or noGit: true:
127
- * - keepFile false: runs fs.unlink
128
- * - keepFile true: no-op on filesystem
129
- *
130
- * @param storageUri - file:// URI
131
- * @param options.noGit - Skip git rm even if .git/ is present
132
- * @param options.keepFile - Remove from git index only; leave file on disk
133
- */
92
+ /** @param options.keepFile - Drop from the index only; leave the file on disk. */
134
93
  remove(storageUri: string, options?: {
135
94
  noGit?: boolean;
136
95
  keepFile?: boolean;
137
96
  }): Promise<void>;
138
- /**
139
- * Convert a file:// URI to an absolute filesystem path.
140
- *
141
- * "file://docs/overview.md" → "{projectRoot}/docs/overview.md"
142
- *
143
- * @param storageUri - file:// URI
144
- * @returns Absolute path
145
- */
146
97
  resolveUri(storageUri: string): string;
147
98
  }
148
- /**
149
- * Thrown when a registered file's checksum does not match the expected value.
150
- * This indicates the file on disk differs from what was recorded (e.g. modified
151
- * after staging, or wrong file path provided).
152
- */
99
+ /** The file on disk is not the file the checksum names. */
153
100
  declare class ChecksumMismatchError extends Error {
154
101
  readonly storageUri: string;
155
102
  readonly expected: string;
@@ -308,19 +255,23 @@ interface AnchoredTextStore {
308
255
  declare function createAnchoredTextStore(dir: string, logger?: Logger): AnchoredTextStore;
309
256
 
310
257
  /**
311
- * ContentExtractorstrategy-keyed text extraction for embedding.
312
- *
313
- * The registry is keyed by `TextExtraction` from `@semiont/core` the
314
- * media-type registry's dispatch vocabularynever by a second media-type
315
- * list (SMELTER-MEDIA-TYPES.md, Design §1): there is exactly one media-type
316
- * table in the system, and this registry consumes it. The Smelter resolves
317
- * `textExtractionOf(contentType)` and looks the extractor up by strategy; a
318
- * `null` slot means decline (settle skipped, reason 'no-extractor').
319
- *
320
- * Extraction is ephemeral: `extract` runs at read time, its output feeds the
321
- * chunker, and is discarded no stored derived representation. Annotations
322
- * anchor to native geometry (`items`), never to extracted-text offsets, so
323
- * re-extraction can never break an anchor.
258
+ * TextExtractorDERIVING text from bytes that carry none of their own.
259
+ *
260
+ * Scope note (READ-VS-EXTRACT P2/P3): this file used to hold a registry covering
261
+ * both ways a resource yields text decoding (charset-aware `Buffer string`)
262
+ * and deriving (parse a PDF, OCR it when there is no text layer). Those shared a
263
+ * name and almost nothing else: microseconds vs. minutes,
264
+ * total determinism vs. none across engine versions, no canonical artifact vs.
265
+ * exactly one, and anyone-with-bytes vs. the Smelter alone. The registry made
266
+ * them interchangeable at every call site.
267
+ *
268
+ * Decoding left: it is `decodeRepresentation` in `@semiont/core`, called
269
+ * directly. What remains here is the deriving half, reached through
270
+ * `derivingExtractorFor` and callable only with the store that persists its
271
+ * output.
272
+ *
273
+ * Anchoring is unaffected: annotations anchor to native geometry (`items`),
274
+ * never to extracted-text offsets, so re-derivation can never break an anchor.
324
275
  */
325
276
 
326
277
  interface ExtractedText {
@@ -375,7 +326,7 @@ interface ExtractionDecline {
375
326
  declined: 'no-text-layer' | 'encrypted' | 'corrupt' | 'too-large';
376
327
  }
377
328
  /**
378
- * Where a strategy may reuse an earlier recognition, and under what key.
329
+ * Where a derivation may reuse an earlier recognition, and under what key.
379
330
  *
380
331
  * The caller supplies the key, and derives it from the bytes it actually
381
332
  * holds — `calculateChecksum` over the same Buffer it passes to `extract()` —
@@ -386,40 +337,72 @@ interface ExtractionDecline {
386
337
  * (PERSIST-ANCHORS P1b); readers mirror it (P1c). One SHA-256 over bytes
387
338
  * already in memory is noise against the engine pass a hit avoids.
388
339
  *
389
- * Optional throughout: a caller that passes nothing extracts uncached and is
390
- * unaffected. The seam is `extract()` itself (PERSIST-ANCHORS D1/P2b): a hit
391
- * returns the FINISHED outcome classification, geometry, provenance, or a
392
- * named decline so neither the native parse nor the engine runs. Every
393
- * geometry-yielding extraction produces an entry, native documents included;
394
- * the 'decode' strategy ignores the cache (no geometry, nothing expensive).
340
+ * REQUIRED, and that is the ownership rule (READ-VS-EXTRACT P2). It carries an
341
+ * `AnchoredTextStore`, and only the Smelter holds one so deriving is reachable
342
+ * exactly to the process that can persist what it derived. The restriction is a
343
+ * capability the caller must already hold, not a convention it must remember:
344
+ * a would-be second producer cannot construct the argument, so it cannot compile.
345
+ *
346
+ * The seam is `extract()` itself (PERSIST-ANCHORS D1/P2b): a hit returns the
347
+ * FINISHED outcome — classification, geometry, provenance, or a named decline —
348
+ * so neither the native parse nor the engine runs.
395
349
  */
396
350
  interface ExtractionCache {
397
351
  key: string;
398
352
  store: AnchoredTextStore;
399
353
  }
400
- interface ContentExtractor {
401
- /**
402
- * Whether this strategy's extractions carry positioned runs (`items`) — the
403
- * geometry an anchored-text artifact is made of. Declared, not probed:
404
- * the reconcile planner must know "should an artifact exist?" without
405
- * running the extractor (PERSIST-ANCHORS P0, the third drift class), and
406
- * the declaration keeps the planner's gate and the live fetch's behavior
407
- * twins by construction. Text strategies anchor by character offset and
408
- * declare false.
409
- */
410
- yieldsGeometry: boolean;
354
+ /**
355
+ * Deriving text from bytes that carry none of their own.
356
+ *
357
+ * Named `ContentExtractor` until READ-VS-EXTRACT P3: the `Content` prefix named
358
+ * the INPUT, when what distinguishes this type is that it produces TEXT — by
359
+ * deriving, which since P3 is the only thing "extraction" means here. WHERE a
360
+ * media type's text comes from at all is core's `TextSource`, which spans both
361
+ * routes and is therefore not called extraction.
362
+ *
363
+ * Whether a text source yields positioned runs lives in `@semiont/core`'s
364
+ * `yieldsGeometryOf`, NOT here (P1). It is a property of the source, and that
365
+ * vocabulary is core's — declaring it per-implementation made it two facts that
366
+ * could disagree, and forced consumers asking about a media type to resolve an
367
+ * implementation to find out. `text-extractor.test.ts` gates core's answer
368
+ * against what these extractors actually produce.
369
+ */
370
+ interface TextExtractor {
411
371
  /**
412
- * Extract embeddable/annotatable text, or decline with the class reason
413
- * (scanned-without-OCR, encrypted, corrupt). The caller skips embedding
414
- * and settles skipped with that reason.
372
+ * Derive text WITH geometry from bytes that carry no text of their own, or
373
+ * decline with the class reason (scanned-without-OCR, encrypted, corrupt).
374
+ * The caller skips embedding and settles skipped with that reason.
375
+ *
376
+ * Expensive, non-deterministic across engine versions, and the sole producer
377
+ * of a canonical artifact — which is why `cache` is required rather than
378
+ * optional (see `ExtractionCache`).
415
379
  */
416
- extract(content: Buffer, mediaType: string, cache?: ExtractionCache): Promise<ExtractedText | ExtractionDecline>;
380
+ extract(content: Buffer, mediaType: string, cache: ExtractionCache): Promise<ExtractedText | ExtractionDecline>;
417
381
  }
418
382
  /**
419
- * Strategy extractor. A `null` slot is a decline: the strategy names a
420
- * capability nothing currently provides ('none' permanently).
383
+ * The deriving extractor for a media type, or `null` when its text needs no
384
+ * deriving.
385
+ *
386
+ * **This replaced a `Record<TextSource, TextExtractor | null>` keyed by
387
+ * strategy (READ-VS-EXTRACT P2), and the deletion is the point.** That map held
388
+ * one real extractor, a `null`, and — under 'decode' — a one-line wrapper around
389
+ * core's `decodeRepresentation`, which five sites in `@semiont/make-meaning`
390
+ * already called directly. Resolving "give me an extractor for this media type"
391
+ * therefore returned, half the time, a trivial function dressed as the same
392
+ * capability as OCR: identical at the call site, wildly different in cost,
393
+ * determinism, and who is allowed to run it. That symmetry is what let a
394
+ * detection worker OCR scanned PDFs for four months without anyone reading it as
395
+ * a category error (#739).
396
+ *
397
+ * Decoding is now a direct `decodeRepresentation()` call at the two sites that
398
+ * need it. There is no registry to resolve, so there is no way to reach OCR by
399
+ * asking a generic question — and a caller that gets a non-null answer here still
400
+ * cannot run it without an `AnchoredTextStore`.
401
+ *
402
+ * Keyed by P1's `yieldsGeometryOf`, so this and the Smelter's publish gate cannot
403
+ * disagree about which media types have a canonical artifact.
421
404
  */
422
- declare const EXTRACTORS: Record<TextExtraction, ContentExtractor | null>;
405
+ declare function derivingExtractorFor(mediaType: string): TextExtractor | null;
423
406
 
424
407
  /**
425
408
  * PDF extractor — the 'pdf-text-layer' strategy (SMELTER-MEDIA-TYPES).
@@ -583,5 +566,5 @@ interface PdfTextLayer extends AnchoredText {
583
566
 
584
567
  declare function extractPdfTextLayer(bytes: Uint8Array | Buffer): Promise<PdfTextLayer | null>;
585
568
 
586
- export { ChecksumMismatchError, EXTRACTORS, MAX_PDF_BYTES, RepresentationMissing, WorkingTreeStore, archivistContentReads, calculateChecksum, createAnchoredTextStore, extractPdfTextLayer, verifyChecksum, withinByteBudget };
587
- export type { AnchoredTextStore, CachedAnchoredText, CachedLine, ContentExtractor, ContentReads, ExtractedText, ExtractionCache, ExtractionDecline, MissingReason, PdfFormField, PdfPageInfo, PdfTextLayer, StoredResource };
569
+ export { ChecksumMismatchError, MAX_PDF_BYTES, RepresentationMissing, WorkingTreeStore, archivistContentReads, calculateChecksum, createAnchoredTextStore, createStager, derivingExtractorFor, extractPdfTextLayer, verifyChecksum, withinByteBudget };
570
+ export type { AnchoredTextStore, CachedAnchoredText, CachedLine, ContentReads, ExtractedText, ExtractionCache, ExtractionDecline, MissingReason, PdfFormField, PdfPageInfo, PdfTextLayer, Stager, StagerOptions, TextExtractor };