@semiont/content 0.5.4 → 0.5.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/README.md CHANGED
@@ -64,14 +64,7 @@ console.log(duplicate.checksum === stored.checksum); // true
64
64
  ## Documentation
65
65
 
66
66
  - [API Reference](./docs/API.md) - Complete API documentation
67
- - [Architecture](./docs/ARCHITECTURE.md) - Design principles
68
- - [Patterns](./docs/PATTERNS.md) - Usage patterns and best practices
69
-
70
- ## Examples
71
-
72
- - [Basic Example](./examples/basic.ts) - Storage and retrieval
73
- - [Deduplication](./examples/deduplication.ts) - Content addressing benefits
74
- - [Binary Content](./examples/binary.ts) - Images and documents
67
+ - [Architecture](./docs/architecture.md) - Design principles
75
68
 
76
69
  ## Storage Architecture
77
70
 
package/dist/index.d.ts CHANGED
@@ -1,9 +1,247 @@
1
+ import { SemiontProject } from '@semiont/core/node';
2
+ import { Logger, PdfCoordinate } from '@semiont/core';
3
+
1
4
  /**
2
- * @semiont/content
5
+ * WorkingTreeStore - Manages files in the project working tree
3
6
  *
4
- * Working tree storage for project resources.
7
+ * Unlike the old content-addressed RepresentationStore, this store treats
8
+ * the working tree (project root) as the source of truth for file content.
9
+ * Resources are identified by their file:// URI, which is stable across
10
+ * content changes and moves (tracked by events).
11
+ *
12
+ * Two write paths:
13
+ * - store(content, storageUri): Write bytes to disk (API/GUI/AI path).
14
+ * Used when the file does not yet exist and the caller provides content.
15
+ * - register(storageUri, expectedChecksum?): Read an existing file and
16
+ * return its metadata (CLI path). The file is already on disk; we just
17
+ * verify and record it. If expectedChecksum is provided, throws on mismatch.
18
+ *
19
+ * Storage layout:
20
+ * {projectRoot}/{path-from-uri}
21
+ *
22
+ * For example, storageUri "file://docs/overview.md" resolves to
23
+ * {projectRoot}/docs/overview.md
24
+ */
25
+
26
+ /**
27
+ * Result of store() or register()
28
+ */
29
+ interface StoredResource {
30
+ storageUri: string;
31
+ checksum: string;
32
+ byteSize: number;
33
+ created: string;
34
+ }
35
+ /**
36
+ * Manages files in the project working tree
37
+ */
38
+ declare class WorkingTreeStore {
39
+ private projectRoot;
40
+ private gitSync;
41
+ private logger?;
42
+ constructor(project: SemiontProject, logger?: Logger);
43
+ private shouldRunGit;
44
+ /**
45
+ * Write content to disk at the location indicated by storageUri.
46
+ *
47
+ * API/GUI/AI path: caller provides bytes; file may not yet exist.
48
+ *
49
+ * @param content - Raw bytes to write
50
+ * @param storageUri - file:// URI (e.g. "file://docs/overview.md")
51
+ * @returns Stored resource metadata
52
+ */
53
+ store(content: Buffer, storageUri: string, options?: {
54
+ noGit?: boolean;
55
+ }): Promise<StoredResource>;
56
+ /**
57
+ * Read an existing file and return its metadata.
58
+ *
59
+ * CLI path: the file is already on disk. We read it to compute the checksum.
60
+ * If expectedChecksum is provided, throws ChecksumMismatchError on mismatch.
61
+ *
62
+ * @param storageUri - file:// URI (e.g. "file://docs/overview.md")
63
+ * @param expectedChecksum - Optional SHA-256 to verify against
64
+ * @returns Stored resource metadata
65
+ * @throws ChecksumMismatchError if expectedChecksum is provided and does not match
66
+ * @throws Error if file does not exist
67
+ */
68
+ register(storageUri: string, expectedChecksum?: string, options?: {
69
+ noGit?: boolean;
70
+ }): Promise<StoredResource>;
71
+ /**
72
+ * Read file content by URI.
73
+ *
74
+ * @param storageUri - file:// URI
75
+ * @returns Raw bytes
76
+ */
77
+ retrieve(storageUri: string): Promise<Buffer>;
78
+ /**
79
+ * Move a file from one URI to another.
80
+ *
81
+ * If .git/ exists in the project root and noGit is not set, runs `git mv`.
82
+ * Otherwise (no .git/ or noGit: true), runs fs.rename.
83
+ *
84
+ * @param fromUri - Current file:// URI
85
+ * @param toUri - New file:// URI
86
+ * @param options.noGit - Skip git mv even if .git/ is present
87
+ */
88
+ move(fromUri: string, toUri: string, options?: {
89
+ noGit?: boolean;
90
+ }): Promise<void>;
91
+ /**
92
+ * Remove a file from the working tree.
93
+ *
94
+ * If .git/ exists and noGit is not set:
95
+ * - keepFile false (default): runs `git rm` (removes from index and disk)
96
+ * - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)
97
+ * If no .git/ or noGit: true:
98
+ * - keepFile false: runs fs.unlink
99
+ * - keepFile true: no-op on filesystem
100
+ *
101
+ * @param storageUri - file:// URI
102
+ * @param options.noGit - Skip git rm even if .git/ is present
103
+ * @param options.keepFile - Remove from git index only; leave file on disk
104
+ */
105
+ remove(storageUri: string, options?: {
106
+ noGit?: boolean;
107
+ keepFile?: boolean;
108
+ }): Promise<void>;
109
+ /**
110
+ * Convert a file:// URI to an absolute filesystem path.
111
+ *
112
+ * "file://docs/overview.md" → "{projectRoot}/docs/overview.md"
113
+ *
114
+ * @param storageUri - file:// URI
115
+ * @returns Absolute path
116
+ */
117
+ resolveUri(storageUri: string): string;
118
+ }
119
+ /**
120
+ * Thrown when a registered file's checksum does not match the expected value.
121
+ * This indicates the file on disk differs from what was recorded (e.g. modified
122
+ * after staging, or wrong file path provided).
123
+ */
124
+ declare class ChecksumMismatchError extends Error {
125
+ readonly storageUri: string;
126
+ readonly expected: string;
127
+ readonly actual: string;
128
+ constructor(storageUri: string, expected: string, actual: string);
129
+ }
130
+
131
+ /**
132
+ * MIME Type to File Extension Mapping
133
+ *
134
+ * Maps common MIME types to their standard file extensions.
135
+ * Used by RepresentationStore to save files with proper extensions.
136
+ */
137
+ /**
138
+ * Get file extension for a MIME type
139
+ *
140
+ * @param mediaType - MIME type (e.g., "text/markdown")
141
+ * @returns File extension with leading dot (e.g., ".md") or ".dat" if unknown
142
+ *
143
+ * @example
144
+ * getExtensionForMimeType('text/markdown') // => '.md'
145
+ * getExtensionForMimeType('image/png') // => '.png'
146
+ * getExtensionForMimeType('unknown/type') // => '.dat'
147
+ */
148
+ declare function getExtensionForMimeType(mediaType: string): string;
149
+ /**
150
+ * Derive a file:// storage URI from a resource name and MIME type.
151
+ *
152
+ * @example
153
+ * deriveStorageUri("My Document", "text/markdown") // => "file://my-document.md"
154
+ */
155
+ declare function deriveStorageUri(name: string, format: string): string;
156
+ /**
157
+ * Check if a MIME type has a known extension mapping
158
+ *
159
+ * @param mediaType - MIME type to check
160
+ * @returns true if extension is known, false if would fallback to .dat
161
+ */
162
+ declare function hasKnownExtension(mediaType: string): boolean;
163
+
164
+ /**
165
+ * Checksum utilities for content verification
166
+ */
167
+ /**
168
+ * Calculate SHA-256 checksum of content
169
+ * @param content The content to hash
170
+ * @returns Hex-encoded SHA-256 hash
171
+ */
172
+ declare function calculateChecksum(content: string | Buffer): string;
173
+ /**
174
+ * Verify content against a checksum
175
+ * @param content The content to verify
176
+ * @param checksum The expected checksum
177
+ * @returns True if content matches checksum
178
+ */
179
+ declare function verifyChecksum(content: string | Buffer, checksum: string): boolean;
180
+
181
+ /**
182
+ * PDF Text Layer Types
183
+ *
184
+ * Represents the extracted text layer from a native PDF, including per-run (word-level)
185
+ * geometry in PDF point coordinates originating from the bottom-left of the page
186
+ * (Y increases upward). The Y-flip to canvas pixels happens downstream in the
187
+ * browser; the server has no canvas.
188
+ *
189
+ * `PdfCoordinate` — the geometry `locate()` emits — lives in `@semiont/core`
190
+ * alongside the viewrect FragmentSelector codec.
191
+ */
192
+ /**
193
+ * A single text item (one text run, roughly a word) from the PDF text layer.
194
+ * Character offsets refer to positions in `PdfTextLayer.text`.
195
+ */
196
+ interface PdfTextItem {
197
+ start: number;
198
+ end: number;
199
+ page: number;
200
+ x: number;
201
+ y: number;
202
+ width: number;
203
+ height: number;
204
+ }
205
+ /** Page dimensions in PDF points */
206
+ interface PdfPageInfo {
207
+ pageNumber: number;
208
+ widthPt: number;
209
+ heightPt: number;
210
+ }
211
+ /**
212
+ * The full extracted text layer for a PDF.
213
+ * `text` is the reading-order concatenation across all pages.
214
+ * Each `item` is one text run carrying its character range into `text` plus PDF-point geometry.
215
+ */
216
+ interface PdfTextLayer {
217
+ pages: PdfPageInfo[];
218
+ text: string;
219
+ items: PdfTextItem[];
220
+ }
221
+
222
+ /**
223
+ * PDF Text Layer Extraction
224
+ *
225
+ * Extracts positioned text from native, non-scanned PDFs using pdfjs-dist.
226
+ * Returns null for scanned/image-only PDFs (no text items).
227
+ *
228
+ * Coordinates are in PDF point space, originating from the bottom-left.
229
+ * The Y-flip to canvas pixels happens downstream.
230
+ */
231
+
232
+ declare function extractPdfTextLayer(bytes: Uint8Array | Buffer): Promise<PdfTextLayer | null>;
233
+
234
+ /**
235
+ * Locates bounding rectangles for a span of text in a PdfTextLayer
236
+ * (single-line or multi-line).
237
+ *
238
+ * Finds all overlapping items [start, end), groups them by page and line,
239
+ * and records one bounding rectangle per line as a PdfCoordinate.
240
+ *
241
+ * Returns array of PdfCoordinate, one per line of text covered by the span.
242
+ * Returns empty array if no items overlap the span.
5
243
  */
6
- export { WorkingTreeStore, ChecksumMismatchError, type StoredResource, } from './working-tree-store';
7
- export { getExtensionForMimeType, hasKnownExtension, deriveStorageUri, } from './mime-extensions';
8
- export { calculateChecksum, verifyChecksum } from './checksum';
9
- //# sourceMappingURL=index.d.ts.map
244
+ declare function locate(layer: PdfTextLayer, start: number, end: number): PdfCoordinate[];
245
+
246
+ export { ChecksumMismatchError, WorkingTreeStore, calculateChecksum, deriveStorageUri, extractPdfTextLayer, getExtensionForMimeType, hasKnownExtension, locate, verifyChecksum };
247
+ export type { PdfPageInfo, PdfTextItem, PdfTextLayer, StoredResource };
package/dist/index.js CHANGED
@@ -190,6 +190,9 @@ The file on disk differs from the recorded checksum. Has it been modified since
190
190
  this.actual = actual;
191
191
  this.name = "ChecksumMismatchError";
192
192
  }
193
+ storageUri;
194
+ expected;
195
+ actual;
193
196
  };
194
197
 
195
198
  // src/mime-extensions.ts
@@ -282,13 +285,113 @@ function hasKnownExtension(mediaType) {
282
285
  const normalized = mediaType.toLowerCase().split(";")[0].trim();
283
286
  return normalized in MIME_TO_EXTENSION;
284
287
  }
288
+
289
+ // src/extract-pdf-text-layer.ts
290
+ import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs";
291
+ async function extractPdfTextLayer(bytes) {
292
+ const doc = await pdfjs.getDocument({ data: bytes }).promise;
293
+ try {
294
+ const pages = [];
295
+ const items = [];
296
+ let text = "";
297
+ let hasAnyTextItems = false;
298
+ for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
299
+ const page = await doc.getPage(pageNum);
300
+ const viewport = page.getViewport({ scale: 1 });
301
+ const content = await page.getTextContent();
302
+ pages.push({
303
+ pageNumber: pageNum,
304
+ widthPt: viewport.width,
305
+ heightPt: viewport.height
306
+ });
307
+ for (const item of content.items) {
308
+ if (!("str" in item)) continue;
309
+ if (item.str.trim()) {
310
+ hasAnyTextItems = true;
311
+ const start = text.length;
312
+ text += item.str;
313
+ const end = text.length;
314
+ const [, , , , x, y] = item.transform;
315
+ items.push({
316
+ start,
317
+ end,
318
+ page: pageNum,
319
+ x,
320
+ y,
321
+ width: item.width,
322
+ height: item.height
323
+ });
324
+ text += item.hasEOL ? "\n" : " ";
325
+ } else if (item.hasEOL) {
326
+ text += "\n";
327
+ }
328
+ }
329
+ text += "\n";
330
+ }
331
+ if (!hasAnyTextItems) return null;
332
+ return { pages, text, items };
333
+ } finally {
334
+ await doc.destroy();
335
+ }
336
+ }
337
+
338
+ // src/locate.ts
339
+ var SAME_LINE_THRESHOLD_PT = 2;
340
+ function locate(layer, start, end) {
341
+ const overlap = layer.items.filter(
342
+ (item) => item.start < end && item.end > start
343
+ );
344
+ if (overlap.length === 0) return [];
345
+ const pages = groupItemsByPage(overlap);
346
+ const rects = [];
347
+ for (const [page, pageItems] of pages) {
348
+ const lines = groupItemsByLine(pageItems, SAME_LINE_THRESHOLD_PT);
349
+ for (const lineItems of lines) {
350
+ const x = Math.min(...lineItems.map((i) => i.x));
351
+ const right = Math.max(...lineItems.map((i) => i.x + i.width));
352
+ const y = Math.min(...lineItems.map((i) => i.y));
353
+ const top = Math.max(...lineItems.map((i) => i.y + i.height));
354
+ rects.push({ page, x, y, width: right - x, height: top - y });
355
+ }
356
+ }
357
+ return rects;
358
+ }
359
+ function groupItemsByPage(items) {
360
+ const map = /* @__PURE__ */ new Map();
361
+ for (const item of items) {
362
+ const existing = map.get(item.page);
363
+ if (existing) {
364
+ existing.push(item);
365
+ } else {
366
+ map.set(item.page, [item]);
367
+ }
368
+ }
369
+ return map;
370
+ }
371
+ function groupItemsByLine(items, sameLineThreshold) {
372
+ const sorted = [...items].sort((a, b) => b.y - a.y || a.x - b.x);
373
+ const lines = [];
374
+ let currentLine = [];
375
+ for (const item of sorted) {
376
+ if (currentLine.length === 0 || Math.abs(item.y - currentLine[0].y) <= sameLineThreshold) {
377
+ currentLine.push(item);
378
+ } else {
379
+ lines.push(currentLine);
380
+ currentLine = [item];
381
+ }
382
+ }
383
+ if (currentLine.length > 0) lines.push(currentLine);
384
+ return lines;
385
+ }
285
386
  export {
286
387
  ChecksumMismatchError,
287
388
  WorkingTreeStore,
288
389
  calculateChecksum,
289
390
  deriveStorageUri,
391
+ extractPdfTextLayer,
290
392
  getExtensionForMimeType,
291
393
  hasKnownExtension,
394
+ locate,
292
395
  verifyChecksum
293
396
  };
294
397
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/working-tree-store.ts","../src/checksum.ts","../src/mime-extensions.ts"],"sourcesContent":["/**\n * WorkingTreeStore - Manages files in the project working tree\n *\n * Unlike the old content-addressed RepresentationStore, this store treats\n * the working tree (project root) as the source of truth for file content.\n * Resources are identified by their file:// URI, which is stable across\n * content changes and moves (tracked by events).\n *\n * Two write paths:\n * - store(content, storageUri): Write bytes to disk (API/GUI/AI path).\n * Used when the file does not yet exist and the caller provides content.\n * - register(storageUri, expectedChecksum?): Read an existing file and\n * return its metadata (CLI path). The file is already on disk; we just\n * verify and record it. If expectedChecksum is provided, throws on mismatch.\n *\n * Storage layout:\n * {projectRoot}/{path-from-uri}\n *\n * For example, storageUri \"file://docs/overview.md\" resolves to\n * {projectRoot}/docs/overview.md\n */\n\nimport { promises as fs } from 'fs';\nimport { execFileSync } from 'child_process';\nimport path from 'path';\nimport type { SemiontProject } from '@semiont/core/node';\nimport type { Logger } from '@semiont/core';\nimport { calculateChecksum, verifyChecksum } from './checksum';\n\n/**\n * Result of store() or register()\n */\nexport interface StoredResource {\n storageUri: string; // file:// URI (e.g. \"file://docs/overview.md\")\n checksum: string; // SHA-256 hex of content\n byteSize: number; // Size in bytes\n created: string; // ISO 8601 timestamp\n}\n\n/**\n * Manages files in the project working tree\n */\nexport class WorkingTreeStore {\n private projectRoot: string;\n private gitSync: boolean;\n private logger?: Logger;\n\n constructor(project: SemiontProject, logger?: Logger) {\n this.projectRoot = project.root;\n this.gitSync = project.gitSync;\n this.logger = logger;\n }\n\n private shouldRunGit(noGit?: boolean): boolean {\n return this.gitSync && !noGit;\n }\n\n /**\n * Write content to disk at the location indicated by storageUri.\n *\n * API/GUI/AI path: caller provides bytes; file may not yet exist.\n *\n * @param content - Raw bytes to write\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @returns Stored resource metadata\n */\n async store(content: Buffer, storageUri: string, options?: { noGit?: boolean }): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n const checksum = calculateChecksum(content);\n\n this.logger?.debug('Storing resource', { storageUri, byteSize: content.length });\n\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(filePath, content);\n\n if (this.shouldRunGit(options?.noGit)) {\n execFileSync('git', ['add', filePath], { cwd: this.projectRoot });\n }\n\n this.logger?.info('Resource stored', { storageUri, checksum, byteSize: content.length });\n\n return {\n storageUri,\n checksum,\n byteSize: content.length,\n created: new Date().toISOString(),\n };\n }\n\n /**\n * Read an existing file and return its metadata.\n *\n * CLI path: the file is already on disk. We read it to compute the checksum.\n * If expectedChecksum is provided, throws ChecksumMismatchError on mismatch.\n *\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @param expectedChecksum - Optional SHA-256 to verify against\n * @returns Stored resource metadata\n * @throws ChecksumMismatchError if expectedChecksum is provided and does not match\n * @throws Error if file does not exist\n */\n async register(storageUri: string, expectedChecksum?: string, options?: { noGit?: boolean }): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n\n this.logger?.debug('Registering resource', { storageUri });\n\n const content = await fs.readFile(filePath);\n const checksum = calculateChecksum(content);\n\n if (expectedChecksum !== undefined && !verifyChecksum(content, expectedChecksum)) {\n throw new ChecksumMismatchError(storageUri, expectedChecksum, checksum);\n }\n\n if (this.shouldRunGit(options?.noGit)) {\n execFileSync('git', ['add', filePath], { cwd: this.projectRoot });\n }\n\n this.logger?.info('Resource registered', { storageUri, checksum, byteSize: content.length });\n\n return {\n storageUri,\n checksum,\n byteSize: content.length,\n created: new Date().toISOString(),\n };\n }\n\n /**\n * Read file content by URI.\n *\n * @param storageUri - file:// URI\n * @returns Raw bytes\n */\n async retrieve(storageUri: string): Promise<Buffer> {\n const filePath = this.resolveUri(storageUri);\n try {\n return await fs.readFile(filePath);\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n throw new Error(`Resource not found: ${storageUri}`);\n }\n throw error;\n }\n }\n\n /**\n * Move a file from one URI to another.\n *\n * If .git/ exists in the project root and noGit is not set, runs `git mv`.\n * Otherwise (no .git/ or noGit: true), runs fs.rename.\n *\n * @param fromUri - Current file:// URI\n * @param toUri - New file:// URI\n * @param options.noGit - Skip git mv even if .git/ is present\n */\n async move(fromUri: string, toUri: string, options?: { noGit?: boolean }): Promise<void> {\n const fromPath = this.resolveUri(fromUri);\n const toPath = this.resolveUri(toUri);\n\n this.logger?.debug('Moving resource', { fromUri, toUri });\n\n await fs.mkdir(path.dirname(toPath), { recursive: true });\n\n if (this.shouldRunGit(options?.noGit)) {\n // git mv handles both the filesystem rename and the index update\n execFileSync('git', ['mv', fromPath, toPath], { cwd: this.projectRoot });\n } else {\n await fs.rename(fromPath, toPath);\n }\n\n this.logger?.info('Resource moved', { fromUri, toUri });\n }\n\n /**\n * Remove a file from the working tree.\n *\n * If .git/ exists and noGit is not set:\n * - keepFile false (default): runs `git rm` (removes from index and disk)\n * - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)\n * If no .git/ or noGit: true:\n * - keepFile false: runs fs.unlink\n * - keepFile true: no-op on filesystem\n *\n * @param storageUri - file:// URI\n * @param options.noGit - Skip git rm even if .git/ is present\n * @param options.keepFile - Remove from git index only; leave file on disk\n */\n async remove(storageUri: string, options?: { noGit?: boolean; keepFile?: boolean }): Promise<void> {\n const filePath = this.resolveUri(storageUri);\n const keepFile = options?.keepFile ?? false;\n\n this.logger?.debug('Removing resource', { storageUri, keepFile });\n\n const useGit = this.shouldRunGit(options?.noGit);\n\n if (useGit) {\n const gitArgs = keepFile\n ? ['rm', '--cached', filePath]\n : ['rm', filePath];\n execFileSync('git', gitArgs, { cwd: this.projectRoot });\n this.logger?.info('Resource removed', { storageUri, keepFile, git: true });\n return;\n }\n\n if (keepFile) {\n this.logger?.info('Resource removed from index (file kept on disk)', { storageUri });\n return;\n }\n\n try {\n await fs.unlink(filePath);\n this.logger?.info('Resource removed', { storageUri });\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n this.logger?.warn('Resource file already absent', { storageUri });\n return;\n }\n throw error;\n }\n }\n\n /**\n * Convert a file:// URI to an absolute filesystem path.\n *\n * \"file://docs/overview.md\" → \"{projectRoot}/docs/overview.md\"\n *\n * @param storageUri - file:// URI\n * @returns Absolute path\n */\n resolveUri(storageUri: string): string {\n if (!storageUri.startsWith('file://')) {\n throw new Error(`Invalid storage URI (must start with file://): ${storageUri}`);\n }\n const relativePath = storageUri.slice('file://'.length);\n return path.join(this.projectRoot, relativePath);\n }\n}\n\n/**\n * Thrown when a registered file's checksum does not match the expected value.\n * This indicates the file on disk differs from what was recorded (e.g. modified\n * after staging, or wrong file path provided).\n */\nexport class ChecksumMismatchError extends Error {\n constructor(\n readonly storageUri: string,\n readonly expected: string,\n readonly actual: string,\n ) {\n super(\n `Checksum mismatch for ${storageUri}: expected ${expected.slice(0, 8)}... but got ${actual.slice(0, 8)}...\\n` +\n `The file on disk differs from the recorded checksum. Has it been modified since staging?`\n );\n this.name = 'ChecksumMismatchError';\n }\n}\n","/**\n * Checksum utilities for content verification\n */\n\nimport { createHash } from 'crypto';\n\n/**\n * Calculate SHA-256 checksum of content\n * @param content The content to hash\n * @returns Hex-encoded SHA-256 hash\n */\nexport function calculateChecksum(content: string | Buffer): string {\n const hash = createHash('sha256');\n hash.update(content);\n return hash.digest('hex');\n}\n\n/**\n * Verify content against a checksum\n * @param content The content to verify\n * @param checksum The expected checksum\n * @returns True if content matches checksum\n */\nexport function verifyChecksum(content: string | Buffer, checksum: string): boolean {\n return calculateChecksum(content) === checksum;\n}\n","/**\n * MIME Type to File Extension Mapping\n *\n * Maps common MIME types to their standard file extensions.\n * Used by RepresentationStore to save files with proper extensions.\n */\n\n/**\n * Comprehensive MIME type to extension mapping\n */\nconst MIME_TO_EXTENSION: Record<string, string> = {\n // Text formats\n 'text/plain': '.txt',\n 'text/markdown': '.md',\n 'text/html': '.html',\n 'text/css': '.css',\n 'text/csv': '.csv',\n 'text/xml': '.xml',\n\n // Application formats - structured data\n 'application/json': '.json',\n 'application/xml': '.xml',\n 'application/yaml': '.yaml',\n 'application/x-yaml': '.yaml',\n\n // Application formats - documents\n 'application/pdf': '.pdf',\n 'application/msword': '.doc',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',\n 'application/vnd.ms-excel': '.xls',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',\n 'application/vnd.ms-powerpoint': '.ppt',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',\n\n // Application formats - archives\n 'application/zip': '.zip',\n 'application/gzip': '.gz',\n 'application/x-tar': '.tar',\n 'application/x-7z-compressed': '.7z',\n\n // Application formats - executables/binaries\n 'application/octet-stream': '.bin',\n 'application/wasm': '.wasm',\n\n // Image formats\n 'image/png': '.png',\n 'image/jpeg': '.jpg',\n 'image/gif': '.gif',\n 'image/webp': '.webp',\n 'image/svg+xml': '.svg',\n 'image/bmp': '.bmp',\n 'image/tiff': '.tiff',\n 'image/x-icon': '.ico',\n\n // Audio formats\n 'audio/mpeg': '.mp3',\n 'audio/wav': '.wav',\n 'audio/ogg': '.ogg',\n 'audio/webm': '.webm',\n 'audio/aac': '.aac',\n 'audio/flac': '.flac',\n\n // Video formats\n 'video/mp4': '.mp4',\n 'video/mpeg': '.mpeg',\n 'video/webm': '.webm',\n 'video/ogg': '.ogv',\n 'video/quicktime': '.mov',\n 'video/x-msvideo': '.avi',\n\n // Programming languages\n 'text/javascript': '.js',\n 'application/javascript': '.js',\n 'text/x-typescript': '.ts',\n 'application/typescript': '.ts',\n 'text/x-python': '.py',\n 'text/x-java': '.java',\n 'text/x-c': '.c',\n 'text/x-c++': '.cpp',\n 'text/x-csharp': '.cs',\n 'text/x-go': '.go',\n 'text/x-rust': '.rs',\n 'text/x-ruby': '.rb',\n 'text/x-php': '.php',\n 'text/x-swift': '.swift',\n 'text/x-kotlin': '.kt',\n 'text/x-shell': '.sh',\n\n // Font formats\n 'font/woff': '.woff',\n 'font/woff2': '.woff2',\n 'font/ttf': '.ttf',\n 'font/otf': '.otf',\n};\n\n/**\n * Get file extension for a MIME type\n *\n * @param mediaType - MIME type (e.g., \"text/markdown\")\n * @returns File extension with leading dot (e.g., \".md\") or \".dat\" if unknown\n *\n * @example\n * getExtensionForMimeType('text/markdown') // => '.md'\n * getExtensionForMimeType('image/png') // => '.png'\n * getExtensionForMimeType('unknown/type') // => '.dat'\n */\nexport function getExtensionForMimeType(mediaType: string): string {\n // Normalize MIME type (lowercase, remove parameters)\n const normalized = mediaType.toLowerCase().split(';')[0]!.trim();\n\n // Look up in mapping\n const extension = MIME_TO_EXTENSION[normalized];\n\n // Return mapped extension or fallback to .dat\n return extension || '.dat';\n}\n\n/**\n * Derive a file:// storage URI from a resource name and MIME type.\n *\n * @example\n * deriveStorageUri(\"My Document\", \"text/markdown\") // => \"file://my-document.md\"\n */\nexport function deriveStorageUri(name: string, format: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n const ext = getExtensionForMimeType(format);\n return `file://${slug}${ext}`;\n}\n\n/**\n * Check if a MIME type has a known extension mapping\n *\n * @param mediaType - MIME type to check\n * @returns true if extension is known, false if would fallback to .dat\n */\nexport function hasKnownExtension(mediaType: string): boolean {\n const normalized = mediaType.toLowerCase().split(';')[0]!.trim();\n return normalized in MIME_TO_EXTENSION;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/working-tree-store.ts","../src/checksum.ts","../src/mime-extensions.ts","../src/extract-pdf-text-layer.ts","../src/locate.ts"],"sourcesContent":["/**\n * WorkingTreeStore - Manages files in the project working tree\n *\n * Unlike the old content-addressed RepresentationStore, this store treats\n * the working tree (project root) as the source of truth for file content.\n * Resources are identified by their file:// URI, which is stable across\n * content changes and moves (tracked by events).\n *\n * Two write paths:\n * - store(content, storageUri): Write bytes to disk (API/GUI/AI path).\n * Used when the file does not yet exist and the caller provides content.\n * - register(storageUri, expectedChecksum?): Read an existing file and\n * return its metadata (CLI path). The file is already on disk; we just\n * verify and record it. If expectedChecksum is provided, throws on mismatch.\n *\n * Storage layout:\n * {projectRoot}/{path-from-uri}\n *\n * For example, storageUri \"file://docs/overview.md\" resolves to\n * {projectRoot}/docs/overview.md\n */\n\nimport { promises as fs } from 'fs';\nimport { execFileSync } from 'child_process';\nimport path from 'path';\nimport type { SemiontProject } from '@semiont/core/node';\nimport type { Logger } from '@semiont/core';\nimport { calculateChecksum, verifyChecksum } from './checksum';\n\n/**\n * Result of store() or register()\n */\nexport interface StoredResource {\n storageUri: string; // file:// URI (e.g. \"file://docs/overview.md\")\n checksum: string; // SHA-256 hex of content\n byteSize: number; // Size in bytes\n created: string; // ISO 8601 timestamp\n}\n\n/**\n * Manages files in the project working tree\n */\nexport class WorkingTreeStore {\n private projectRoot: string;\n private gitSync: boolean;\n private logger?: Logger;\n\n constructor(project: SemiontProject, logger?: Logger) {\n this.projectRoot = project.root;\n this.gitSync = project.gitSync;\n this.logger = logger;\n }\n\n private shouldRunGit(noGit?: boolean): boolean {\n return this.gitSync && !noGit;\n }\n\n /**\n * Write content to disk at the location indicated by storageUri.\n *\n * API/GUI/AI path: caller provides bytes; file may not yet exist.\n *\n * @param content - Raw bytes to write\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @returns Stored resource metadata\n */\n async store(content: Buffer, storageUri: string, options?: { noGit?: boolean }): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n const checksum = calculateChecksum(content);\n\n this.logger?.debug('Storing resource', { storageUri, byteSize: content.length });\n\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n await fs.writeFile(filePath, content);\n\n if (this.shouldRunGit(options?.noGit)) {\n execFileSync('git', ['add', filePath], { cwd: this.projectRoot });\n }\n\n this.logger?.info('Resource stored', { storageUri, checksum, byteSize: content.length });\n\n return {\n storageUri,\n checksum,\n byteSize: content.length,\n created: new Date().toISOString(),\n };\n }\n\n /**\n * Read an existing file and return its metadata.\n *\n * CLI path: the file is already on disk. We read it to compute the checksum.\n * If expectedChecksum is provided, throws ChecksumMismatchError on mismatch.\n *\n * @param storageUri - file:// URI (e.g. \"file://docs/overview.md\")\n * @param expectedChecksum - Optional SHA-256 to verify against\n * @returns Stored resource metadata\n * @throws ChecksumMismatchError if expectedChecksum is provided and does not match\n * @throws Error if file does not exist\n */\n async register(storageUri: string, expectedChecksum?: string, options?: { noGit?: boolean }): Promise<StoredResource> {\n const filePath = this.resolveUri(storageUri);\n\n this.logger?.debug('Registering resource', { storageUri });\n\n const content = await fs.readFile(filePath);\n const checksum = calculateChecksum(content);\n\n if (expectedChecksum !== undefined && !verifyChecksum(content, expectedChecksum)) {\n throw new ChecksumMismatchError(storageUri, expectedChecksum, checksum);\n }\n\n if (this.shouldRunGit(options?.noGit)) {\n execFileSync('git', ['add', filePath], { cwd: this.projectRoot });\n }\n\n this.logger?.info('Resource registered', { storageUri, checksum, byteSize: content.length });\n\n return {\n storageUri,\n checksum,\n byteSize: content.length,\n created: new Date().toISOString(),\n };\n }\n\n /**\n * Read file content by URI.\n *\n * @param storageUri - file:// URI\n * @returns Raw bytes\n */\n async retrieve(storageUri: string): Promise<Buffer> {\n const filePath = this.resolveUri(storageUri);\n try {\n return await fs.readFile(filePath);\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n throw new Error(`Resource not found: ${storageUri}`);\n }\n throw error;\n }\n }\n\n /**\n * Move a file from one URI to another.\n *\n * If .git/ exists in the project root and noGit is not set, runs `git mv`.\n * Otherwise (no .git/ or noGit: true), runs fs.rename.\n *\n * @param fromUri - Current file:// URI\n * @param toUri - New file:// URI\n * @param options.noGit - Skip git mv even if .git/ is present\n */\n async move(fromUri: string, toUri: string, options?: { noGit?: boolean }): Promise<void> {\n const fromPath = this.resolveUri(fromUri);\n const toPath = this.resolveUri(toUri);\n\n this.logger?.debug('Moving resource', { fromUri, toUri });\n\n await fs.mkdir(path.dirname(toPath), { recursive: true });\n\n if (this.shouldRunGit(options?.noGit)) {\n // git mv handles both the filesystem rename and the index update\n execFileSync('git', ['mv', fromPath, toPath], { cwd: this.projectRoot });\n } else {\n await fs.rename(fromPath, toPath);\n }\n\n this.logger?.info('Resource moved', { fromUri, toUri });\n }\n\n /**\n * Remove a file from the working tree.\n *\n * If .git/ exists and noGit is not set:\n * - keepFile false (default): runs `git rm` (removes from index and disk)\n * - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)\n * If no .git/ or noGit: true:\n * - keepFile false: runs fs.unlink\n * - keepFile true: no-op on filesystem\n *\n * @param storageUri - file:// URI\n * @param options.noGit - Skip git rm even if .git/ is present\n * @param options.keepFile - Remove from git index only; leave file on disk\n */\n async remove(storageUri: string, options?: { noGit?: boolean; keepFile?: boolean }): Promise<void> {\n const filePath = this.resolveUri(storageUri);\n const keepFile = options?.keepFile ?? false;\n\n this.logger?.debug('Removing resource', { storageUri, keepFile });\n\n const useGit = this.shouldRunGit(options?.noGit);\n\n if (useGit) {\n const gitArgs = keepFile\n ? ['rm', '--cached', filePath]\n : ['rm', filePath];\n execFileSync('git', gitArgs, { cwd: this.projectRoot });\n this.logger?.info('Resource removed', { storageUri, keepFile, git: true });\n return;\n }\n\n if (keepFile) {\n this.logger?.info('Resource removed from index (file kept on disk)', { storageUri });\n return;\n }\n\n try {\n await fs.unlink(filePath);\n this.logger?.info('Resource removed', { storageUri });\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n this.logger?.warn('Resource file already absent', { storageUri });\n return;\n }\n throw error;\n }\n }\n\n /**\n * Convert a file:// URI to an absolute filesystem path.\n *\n * \"file://docs/overview.md\" → \"{projectRoot}/docs/overview.md\"\n *\n * @param storageUri - file:// URI\n * @returns Absolute path\n */\n resolveUri(storageUri: string): string {\n if (!storageUri.startsWith('file://')) {\n throw new Error(`Invalid storage URI (must start with file://): ${storageUri}`);\n }\n const relativePath = storageUri.slice('file://'.length);\n return path.join(this.projectRoot, relativePath);\n }\n}\n\n/**\n * Thrown when a registered file's checksum does not match the expected value.\n * This indicates the file on disk differs from what was recorded (e.g. modified\n * after staging, or wrong file path provided).\n */\nexport class ChecksumMismatchError extends Error {\n constructor(\n readonly storageUri: string,\n readonly expected: string,\n readonly actual: string,\n ) {\n super(\n `Checksum mismatch for ${storageUri}: expected ${expected.slice(0, 8)}... but got ${actual.slice(0, 8)}...\\n` +\n `The file on disk differs from the recorded checksum. Has it been modified since staging?`\n );\n this.name = 'ChecksumMismatchError';\n }\n}\n","/**\n * Checksum utilities for content verification\n */\n\nimport { createHash } from 'crypto';\n\n/**\n * Calculate SHA-256 checksum of content\n * @param content The content to hash\n * @returns Hex-encoded SHA-256 hash\n */\nexport function calculateChecksum(content: string | Buffer): string {\n const hash = createHash('sha256');\n hash.update(content);\n return hash.digest('hex');\n}\n\n/**\n * Verify content against a checksum\n * @param content The content to verify\n * @param checksum The expected checksum\n * @returns True if content matches checksum\n */\nexport function verifyChecksum(content: string | Buffer, checksum: string): boolean {\n return calculateChecksum(content) === checksum;\n}\n","/**\n * MIME Type to File Extension Mapping\n *\n * Maps common MIME types to their standard file extensions.\n * Used by RepresentationStore to save files with proper extensions.\n */\n\n/**\n * Comprehensive MIME type to extension mapping\n */\nconst MIME_TO_EXTENSION: Record<string, string> = {\n // Text formats\n 'text/plain': '.txt',\n 'text/markdown': '.md',\n 'text/html': '.html',\n 'text/css': '.css',\n 'text/csv': '.csv',\n 'text/xml': '.xml',\n\n // Application formats - structured data\n 'application/json': '.json',\n 'application/xml': '.xml',\n 'application/yaml': '.yaml',\n 'application/x-yaml': '.yaml',\n\n // Application formats - documents\n 'application/pdf': '.pdf',\n 'application/msword': '.doc',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',\n 'application/vnd.ms-excel': '.xls',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',\n 'application/vnd.ms-powerpoint': '.ppt',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',\n\n // Application formats - archives\n 'application/zip': '.zip',\n 'application/gzip': '.gz',\n 'application/x-tar': '.tar',\n 'application/x-7z-compressed': '.7z',\n\n // Application formats - executables/binaries\n 'application/octet-stream': '.bin',\n 'application/wasm': '.wasm',\n\n // Image formats\n 'image/png': '.png',\n 'image/jpeg': '.jpg',\n 'image/gif': '.gif',\n 'image/webp': '.webp',\n 'image/svg+xml': '.svg',\n 'image/bmp': '.bmp',\n 'image/tiff': '.tiff',\n 'image/x-icon': '.ico',\n\n // Audio formats\n 'audio/mpeg': '.mp3',\n 'audio/wav': '.wav',\n 'audio/ogg': '.ogg',\n 'audio/webm': '.webm',\n 'audio/aac': '.aac',\n 'audio/flac': '.flac',\n\n // Video formats\n 'video/mp4': '.mp4',\n 'video/mpeg': '.mpeg',\n 'video/webm': '.webm',\n 'video/ogg': '.ogv',\n 'video/quicktime': '.mov',\n 'video/x-msvideo': '.avi',\n\n // Programming languages\n 'text/javascript': '.js',\n 'application/javascript': '.js',\n 'text/x-typescript': '.ts',\n 'application/typescript': '.ts',\n 'text/x-python': '.py',\n 'text/x-java': '.java',\n 'text/x-c': '.c',\n 'text/x-c++': '.cpp',\n 'text/x-csharp': '.cs',\n 'text/x-go': '.go',\n 'text/x-rust': '.rs',\n 'text/x-ruby': '.rb',\n 'text/x-php': '.php',\n 'text/x-swift': '.swift',\n 'text/x-kotlin': '.kt',\n 'text/x-shell': '.sh',\n\n // Font formats\n 'font/woff': '.woff',\n 'font/woff2': '.woff2',\n 'font/ttf': '.ttf',\n 'font/otf': '.otf',\n};\n\n/**\n * Get file extension for a MIME type\n *\n * @param mediaType - MIME type (e.g., \"text/markdown\")\n * @returns File extension with leading dot (e.g., \".md\") or \".dat\" if unknown\n *\n * @example\n * getExtensionForMimeType('text/markdown') // => '.md'\n * getExtensionForMimeType('image/png') // => '.png'\n * getExtensionForMimeType('unknown/type') // => '.dat'\n */\nexport function getExtensionForMimeType(mediaType: string): string {\n // Normalize MIME type (lowercase, remove parameters)\n const normalized = mediaType.toLowerCase().split(';')[0]!.trim();\n\n // Look up in mapping\n const extension = MIME_TO_EXTENSION[normalized];\n\n // Return mapped extension or fallback to .dat\n return extension || '.dat';\n}\n\n/**\n * Derive a file:// storage URI from a resource name and MIME type.\n *\n * @example\n * deriveStorageUri(\"My Document\", \"text/markdown\") // => \"file://my-document.md\"\n */\nexport function deriveStorageUri(name: string, format: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n const ext = getExtensionForMimeType(format);\n return `file://${slug}${ext}`;\n}\n\n/**\n * Check if a MIME type has a known extension mapping\n *\n * @param mediaType - MIME type to check\n * @returns true if extension is known, false if would fallback to .dat\n */\nexport function hasKnownExtension(mediaType: string): boolean {\n const normalized = mediaType.toLowerCase().split(';')[0]!.trim();\n return normalized in MIME_TO_EXTENSION;\n}\n","/**\n * PDF Text Layer Extraction\n *\n * Extracts positioned text from native, non-scanned PDFs using pdfjs-dist.\n * Returns null for scanned/image-only PDFs (no text items).\n *\n * Coordinates are in PDF point space, originating from the bottom-left.\n * The Y-flip to canvas pixels happens downstream.\n */\n\nimport * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs';\nimport type { PdfTextLayer, PdfPageInfo, PdfTextItem } from './pdf-text-layer';\n\nexport async function extractPdfTextLayer(\n bytes: Uint8Array | Buffer\n): Promise<PdfTextLayer | null> {\n // pdf.js v5 removed the isEvalSupported option; this path only calls\n // getTextContent (no rendering / no PDF functions).\n const doc = await pdfjs.getDocument({ data: bytes }).promise;\n\n try {\n const pages: PdfPageInfo[] = [];\n const items: PdfTextItem[] = [];\n let text = '';\n let hasAnyTextItems = false;\n\n for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {\n const page = await doc.getPage(pageNum);\n const viewport = page.getViewport({ scale: 1.0 });\n const content = await page.getTextContent(); // all text items on the page\n\n pages.push({\n pageNumber: pageNum,\n widthPt: viewport.width,\n heightPt: viewport.height,\n });\n\n for (const item of content.items) {\n if (!('str' in item)) continue; // skip marked-content items (no text)\n\n if (item.str.trim()) {\n hasAnyTextItems = true;\n const start = text.length;\n text += item.str;\n const end = text.length; // range covers only this run's own chars\n\n const [, , , , x, y] = item.transform as number[];\n\n items.push({\n start,\n end,\n page: pageNum,\n x,\n y,\n width: item.width,\n height: item.height,\n });\n\n // Separator AFTER recording the run, so its [start, end) never\n // includes it. pdf.js flags the last run on a line with hasEOL —\n // newline there, space between words otherwise, so reading-order\n // lines don't glue (e.g. \"textsecond\").\n text += item.hasEOL ? '\\n' : ' ';\n } else if (item.hasEOL) {\n // Standalone end-of-line marker (empty/whitespace str): keep the\n // line break without letting whitespace-only runs add stray spaces.\n text += '\\n';\n }\n }\n\n text += '\\n'; // page break\n }\n\n if (!hasAnyTextItems) return null;\n\n return { pages, text, items };\n } finally {\n // Release the pdf.js document — Phase 2 runs this in a long-lived worker pool.\n await doc.destroy();\n }\n}\n","import type { PdfCoordinate } from '@semiont/core';\nimport type { PdfTextLayer, PdfTextItem } from './pdf-text-layer';\n\n/**\n * Items whose baseline Y is within this many PDF points are treated as being on\n * the same line. Tuned for ~12pt body text; revisit for documents with large or\n * variable font sizes (Phase 4 / #738).\n */\nconst SAME_LINE_THRESHOLD_PT = 2;\n\n/**\n * Locates bounding rectangles for a span of text in a PdfTextLayer\n * (single-line or multi-line).\n * \n * Finds all overlapping items [start, end), groups them by page and line,\n * and records one bounding rectangle per line as a PdfCoordinate.\n * \n * Returns array of PdfCoordinate, one per line of text covered by the span.\n * Returns empty array if no items overlap the span.\n */\nexport function locate(\n layer: PdfTextLayer,\n start: number,\n end: number\n): PdfCoordinate[] {\n const overlap: PdfTextItem[] = layer.items.filter(\n item => item.start < end && item.end > start\n );\n if (overlap.length === 0) return [];\n\n const pages: Map<number, PdfTextItem[]> = groupItemsByPage(overlap);\n const rects: PdfCoordinate[] = [];\n\n // for each page, group items into lines and compute one rectangle per line\n for (const [page, pageItems] of pages) {\n const lines = groupItemsByLine(pageItems, SAME_LINE_THRESHOLD_PT);\n // Compute one bounding rectangle per line and add it to rects\n for (const lineItems of lines) {\n const x = Math.min(...lineItems.map(i => i.x));\n const right = Math.max(...lineItems.map(i => i.x + i.width));\n const y = Math.min(...lineItems.map(i => i.y));\n const top = Math.max(...lineItems.map(i => i.y + i.height));\n rects.push({page, x, y, width: right - x, height: top - y});\n }\n }\n return rects;\n}\n\nfunction groupItemsByPage(items: PdfTextItem[]): Map<number, PdfTextItem[]> {\n const map = new Map<number, PdfTextItem[]>();\n for (const item of items) {\n const existing = map.get(item.page);\n if (existing) {\n existing.push(item);\n } else {\n map.set(item.page, [item]);\n }\n }\n return map;\n}\n\n\n/**\n * Sorts text items into lines when their y coordinates are\n * within `sameLineThreshold` points of each other.\n * Sorted top-to-bottom (descending y in PDF space), then left-to-right.\n * \n * Returns 2D array: \n * Outer array = list of lines\n * Inner array = list of items on that line\n*/\nfunction groupItemsByLine(items: PdfTextItem[], sameLineThreshold: number): PdfTextItem[][] {\n // Sort top-to-bottom by y; if y is equal, sort left-to-right by x\n const sorted = [...items].sort((a, b) => b.y - a.y || a.x - b.x);\n const lines: PdfTextItem[][] = [];\n let currentLine: PdfTextItem[] = [];\n\n for (const item of sorted) {\n if (currentLine.length === 0 || Math.abs(item.y - currentLine[0].y) <= sameLineThreshold) {\n currentLine.push(item);\n } else {\n lines.push(currentLine);\n currentLine = [item];\n }\n }\n if (currentLine.length > 0) lines.push(currentLine);\n return lines;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@semiont/content",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
+ "engines": {
5
+ "node": ">=24.0.0"
6
+ },
4
7
  "type": "module",
5
8
  "description": "Content-addressed storage for resource representations",
6
9
  "main": "./dist/index.js",
@@ -16,20 +19,25 @@
16
19
  "README.md"
17
20
  ],
18
21
  "scripts": {
19
- "build": "npm run typecheck && tsup && tsc -p tsconfig.build.json",
22
+ "build": "npm run typecheck && tsup && tsc -p tsconfig.build.json && rollup -c rollup.dts.config.mjs && rm -rf dist-types",
20
23
  "typecheck": "tsc --noEmit",
21
- "clean": "rm -rf dist",
24
+ "clean": "rm -rf dist dist-types",
22
25
  "test": "vitest run",
23
26
  "test:watch": "vitest",
24
27
  "test:coverage": "vitest run --coverage"
25
28
  },
26
29
  "dependencies": {
27
- "@semiont/core": "*"
30
+ "@semiont/core": "*",
31
+ "pdfjs-dist": "^5.7.284"
28
32
  },
29
33
  "devDependencies": {
30
- "@vitest/coverage-v8": "^4.1.0",
34
+ "@vitest/coverage-v8": "^4.1.8",
35
+ "pdf-lib": "^1.17.1",
36
+ "rollup": "^4.61.0",
37
+ "rollup-plugin-dts": "^6.4.1",
31
38
  "tsup": "^8.0.1",
32
- "typescript": "^6.0.2"
39
+ "typescript": "^6.0.2",
40
+ "vitest": "^4.1.8"
33
41
  },
34
42
  "keywords": [
35
43
  "content",
@@ -1,17 +0,0 @@
1
- /**
2
- * Checksum utilities for content verification
3
- */
4
- /**
5
- * Calculate SHA-256 checksum of content
6
- * @param content The content to hash
7
- * @returns Hex-encoded SHA-256 hash
8
- */
9
- export declare function calculateChecksum(content: string | Buffer): string;
10
- /**
11
- * Verify content against a checksum
12
- * @param content The content to verify
13
- * @param checksum The expected checksum
14
- * @returns True if content matches checksum
15
- */
16
- export declare function verifyChecksum(content: string | Buffer, checksum: string): boolean;
17
- //# sourceMappingURL=checksum.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"checksum.d.ts","sourceRoot":"","sources":["../src/checksum.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAIlE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAElF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,iBAAiB,EACjB,cAAc,EACf,MAAM,YAAY,CAAC"}
@@ -1,33 +0,0 @@
1
- /**
2
- * MIME Type to File Extension Mapping
3
- *
4
- * Maps common MIME types to their standard file extensions.
5
- * Used by RepresentationStore to save files with proper extensions.
6
- */
7
- /**
8
- * Get file extension for a MIME type
9
- *
10
- * @param mediaType - MIME type (e.g., "text/markdown")
11
- * @returns File extension with leading dot (e.g., ".md") or ".dat" if unknown
12
- *
13
- * @example
14
- * getExtensionForMimeType('text/markdown') // => '.md'
15
- * getExtensionForMimeType('image/png') // => '.png'
16
- * getExtensionForMimeType('unknown/type') // => '.dat'
17
- */
18
- export declare function getExtensionForMimeType(mediaType: string): string;
19
- /**
20
- * Derive a file:// storage URI from a resource name and MIME type.
21
- *
22
- * @example
23
- * deriveStorageUri("My Document", "text/markdown") // => "file://my-document.md"
24
- */
25
- export declare function deriveStorageUri(name: string, format: string): string;
26
- /**
27
- * Check if a MIME type has a known extension mapping
28
- *
29
- * @param mediaType - MIME type to check
30
- * @returns true if extension is known, false if would fallback to .dat
31
- */
32
- export declare function hasKnownExtension(mediaType: string): boolean;
33
- //# sourceMappingURL=mime-extensions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"mime-extensions.d.ts","sourceRoot":"","sources":["../src/mime-extensions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA0FH;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CASjE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAOrE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAG5D"}
@@ -1,94 +0,0 @@
1
- /**
2
- * RepresentationStore - Content-addressed storage for byte-level resource representations
3
- *
4
- * Handles storage and retrieval of concrete byte-level renditions of resources.
5
- * Uses content-addressed storage where the checksum IS the filename.
6
- * Supports multiple storage backends (filesystem, S3, IPFS, etc.)
7
- *
8
- * Storage structure (filesystem):
9
- * basePath/representations/{mediaType}/{ab}/{cd}/rep-{checksum}{extension}
10
- *
11
- * Where:
12
- * - {mediaType} is base MIME type with "/" encoded as "~1" (e.g., "text~1markdown")
13
- * - {ab}/{cd} are first 4 hex digits of checksum for sharding
14
- * - {checksum} is the raw SHA-256 hex hash (e.g., "5aaa0b72abc123...")
15
- * - {extension} is derived from base MIME type (.md, .txt, .png, etc.)
16
- *
17
- * Example:
18
- * For content with checksum "5aaa0b72abc123..." and mediaType "text/markdown; charset=iso-8859-1":
19
- * - Storage path: basePath/representations/text~1markdown/5a/aa/rep-5aaa0b72abc123....md
20
- * - Stored mediaType: "text/markdown; charset=iso-8859-1" (full type with charset preserved)
21
- *
22
- * Character Encoding:
23
- * - Charset parameters in mediaType are preserved in metadata (e.g., "text/plain; charset=iso-8859-1")
24
- * - Storage path uses only base MIME type (strips charset for directory structure)
25
- * - Content stored as raw bytes - charset only affects decoding on retrieval
26
- *
27
- * This design provides:
28
- * - O(1) content retrieval by checksum + mediaType
29
- * - Automatic deduplication (identical content = same file)
30
- * - Idempotent storage operations
31
- * - Proper file extensions for filesystem browsing
32
- * - Faithful preservation of character encoding metadata
33
- */
34
- import type { SemiontProject } from '@semiont/core/node';
35
- import type { Logger } from '@semiont/core';
36
- /**
37
- * Metadata for a representation being stored
38
- */
39
- export interface RepresentationMetadata {
40
- mediaType: string;
41
- filename?: string;
42
- encoding?: string;
43
- language?: string;
44
- rel?: 'original' | 'thumbnail' | 'preview' | 'optimized' | 'derived' | 'other';
45
- }
46
- /**
47
- * Complete representation information
48
- */
49
- export interface StoredRepresentation extends RepresentationMetadata {
50
- '@id': string;
51
- byteSize: number;
52
- checksum: string;
53
- created: string;
54
- }
55
- /**
56
- * Interface for representation storage backends
57
- */
58
- export interface RepresentationStore {
59
- /**
60
- * Store content and return representation metadata
61
- *
62
- * @param content - Raw bytes to store
63
- * @param metadata - Representation metadata
64
- * @returns Complete representation info with checksum
65
- */
66
- store(content: Buffer, metadata: RepresentationMetadata): Promise<StoredRepresentation>;
67
- /**
68
- * Retrieve content by checksum (content-addressed lookup)
69
- *
70
- * @param checksum - Content checksum as raw hex (e.g., "5aaa0b72...")
71
- * @param mediaType - MIME type (e.g., "text/markdown")
72
- * @returns Raw bytes
73
- */
74
- retrieve(checksum: string, mediaType: string): Promise<Buffer>;
75
- }
76
- /**
77
- * Filesystem implementation of RepresentationStore
78
- */
79
- export declare class FilesystemRepresentationStore implements RepresentationStore {
80
- private basePath;
81
- private logger?;
82
- constructor(project: SemiontProject, logger?: Logger);
83
- store(content: Buffer, metadata: RepresentationMetadata): Promise<StoredRepresentation>;
84
- retrieve(checksum: string, mediaType: string): Promise<Buffer>;
85
- /**
86
- * Encode media type for filesystem path
87
- * Replaces "/" with "~1" to avoid directory separators
88
- *
89
- * @param mediaType - MIME type (e.g., "text/markdown")
90
- * @returns Encoded path segment (e.g., "text~1markdown")
91
- */
92
- private encodeMediaType;
93
- }
94
- //# sourceMappingURL=representation-store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"representation-store.d.ts","sourceRoot":"","sources":["../src/representation-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;CAChF;AAED;;GAEG;AACH,MAAM,WAAW,oBAAqB,SAAQ,sBAAsB;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAExF;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAChE;AAED;;GAEG;AACH,qBAAa,6BAA8B,YAAW,mBAAmB;IACvE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,MAAM;IAK9C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAwDvF,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyDpE;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;CAGxB"}
@@ -1,128 +0,0 @@
1
- /**
2
- * WorkingTreeStore - Manages files in the project working tree
3
- *
4
- * Unlike the old content-addressed RepresentationStore, this store treats
5
- * the working tree (project root) as the source of truth for file content.
6
- * Resources are identified by their file:// URI, which is stable across
7
- * content changes and moves (tracked by events).
8
- *
9
- * Two write paths:
10
- * - store(content, storageUri): Write bytes to disk (API/GUI/AI path).
11
- * Used when the file does not yet exist and the caller provides content.
12
- * - register(storageUri, expectedChecksum?): Read an existing file and
13
- * return its metadata (CLI path). The file is already on disk; we just
14
- * verify and record it. If expectedChecksum is provided, throws on mismatch.
15
- *
16
- * Storage layout:
17
- * {projectRoot}/{path-from-uri}
18
- *
19
- * For example, storageUri "file://docs/overview.md" resolves to
20
- * {projectRoot}/docs/overview.md
21
- */
22
- import type { SemiontProject } from '@semiont/core/node';
23
- import type { Logger } from '@semiont/core';
24
- /**
25
- * Result of store() or register()
26
- */
27
- export interface StoredResource {
28
- storageUri: string;
29
- checksum: string;
30
- byteSize: number;
31
- created: string;
32
- }
33
- /**
34
- * Manages files in the project working tree
35
- */
36
- export declare class WorkingTreeStore {
37
- private projectRoot;
38
- private gitSync;
39
- private logger?;
40
- constructor(project: SemiontProject, logger?: Logger);
41
- private shouldRunGit;
42
- /**
43
- * Write content to disk at the location indicated by storageUri.
44
- *
45
- * API/GUI/AI path: caller provides bytes; file may not yet exist.
46
- *
47
- * @param content - Raw bytes to write
48
- * @param storageUri - file:// URI (e.g. "file://docs/overview.md")
49
- * @returns Stored resource metadata
50
- */
51
- store(content: Buffer, storageUri: string, options?: {
52
- noGit?: boolean;
53
- }): Promise<StoredResource>;
54
- /**
55
- * Read an existing file and return its metadata.
56
- *
57
- * CLI path: the file is already on disk. We read it to compute the checksum.
58
- * If expectedChecksum is provided, throws ChecksumMismatchError on mismatch.
59
- *
60
- * @param storageUri - file:// URI (e.g. "file://docs/overview.md")
61
- * @param expectedChecksum - Optional SHA-256 to verify against
62
- * @returns Stored resource metadata
63
- * @throws ChecksumMismatchError if expectedChecksum is provided and does not match
64
- * @throws Error if file does not exist
65
- */
66
- register(storageUri: string, expectedChecksum?: string, options?: {
67
- noGit?: boolean;
68
- }): Promise<StoredResource>;
69
- /**
70
- * Read file content by URI.
71
- *
72
- * @param storageUri - file:// URI
73
- * @returns Raw bytes
74
- */
75
- retrieve(storageUri: string): Promise<Buffer>;
76
- /**
77
- * Move a file from one URI to another.
78
- *
79
- * If .git/ exists in the project root and noGit is not set, runs `git mv`.
80
- * Otherwise (no .git/ or noGit: true), runs fs.rename.
81
- *
82
- * @param fromUri - Current file:// URI
83
- * @param toUri - New file:// URI
84
- * @param options.noGit - Skip git mv even if .git/ is present
85
- */
86
- move(fromUri: string, toUri: string, options?: {
87
- noGit?: boolean;
88
- }): Promise<void>;
89
- /**
90
- * Remove a file from the working tree.
91
- *
92
- * If .git/ exists and noGit is not set:
93
- * - keepFile false (default): runs `git rm` (removes from index and disk)
94
- * - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)
95
- * If no .git/ or noGit: true:
96
- * - keepFile false: runs fs.unlink
97
- * - keepFile true: no-op on filesystem
98
- *
99
- * @param storageUri - file:// URI
100
- * @param options.noGit - Skip git rm even if .git/ is present
101
- * @param options.keepFile - Remove from git index only; leave file on disk
102
- */
103
- remove(storageUri: string, options?: {
104
- noGit?: boolean;
105
- keepFile?: boolean;
106
- }): Promise<void>;
107
- /**
108
- * Convert a file:// URI to an absolute filesystem path.
109
- *
110
- * "file://docs/overview.md" → "{projectRoot}/docs/overview.md"
111
- *
112
- * @param storageUri - file:// URI
113
- * @returns Absolute path
114
- */
115
- resolveUri(storageUri: string): string;
116
- }
117
- /**
118
- * Thrown when a registered file's checksum does not match the expected value.
119
- * This indicates the file on disk differs from what was recorded (e.g. modified
120
- * after staging, or wrong file path provided).
121
- */
122
- export declare class ChecksumMismatchError extends Error {
123
- readonly storageUri: string;
124
- readonly expected: string;
125
- readonly actual: string;
126
- constructor(storageUri: string, expected: string, actual: string);
127
- }
128
- //# sourceMappingURL=working-tree-store.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"working-tree-store.d.ts","sourceRoot":"","sources":["../src/working-tree-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAG5C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,MAAM,CAAC,CAAS;gBAEZ,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,MAAM;IAMpD,OAAO,CAAC,YAAY;IAIpB;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,CAAC;IAuBxG;;;;;;;;;;;OAWG;IACG,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,CAAC;IA0BrH;;;;;OAKG;IACG,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYnD;;;;;;;;;OASG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBxF;;;;;;;;;;;;;OAaG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAkClG;;;;;;;OAOG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAOvC;AAED;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAE5C,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAFd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAQ1B"}