@semiont/content 0.3.7 → 0.4.0

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
@@ -2,96 +2,130 @@ import { SemiontProject } from '@semiont/core/node';
2
2
  import { Logger } from '@semiont/core';
3
3
 
4
4
  /**
5
- * RepresentationStore - Content-addressed storage for byte-level resource representations
5
+ * WorkingTreeStore - Manages files in the project working tree
6
6
  *
7
- * Handles storage and retrieval of concrete byte-level renditions of resources.
8
- * Uses content-addressed storage where the checksum IS the filename.
9
- * Supports multiple storage backends (filesystem, S3, IPFS, etc.)
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).
10
11
  *
11
- * Storage structure (filesystem):
12
- * basePath/representations/{mediaType}/{ab}/{cd}/rep-{checksum}{extension}
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.
13
18
  *
14
- * Where:
15
- * - {mediaType} is base MIME type with "/" encoded as "~1" (e.g., "text~1markdown")
16
- * - {ab}/{cd} are first 4 hex digits of checksum for sharding
17
- * - {checksum} is the raw SHA-256 hex hash (e.g., "5aaa0b72abc123...")
18
- * - {extension} is derived from base MIME type (.md, .txt, .png, etc.)
19
+ * Storage layout:
20
+ * {projectRoot}/{path-from-uri}
19
21
  *
20
- * Example:
21
- * For content with checksum "5aaa0b72abc123..." and mediaType "text/markdown; charset=iso-8859-1":
22
- * - Storage path: basePath/representations/text~1markdown/5a/aa/rep-5aaa0b72abc123....md
23
- * - Stored mediaType: "text/markdown; charset=iso-8859-1" (full type with charset preserved)
24
- *
25
- * Character Encoding:
26
- * - Charset parameters in mediaType are preserved in metadata (e.g., "text/plain; charset=iso-8859-1")
27
- * - Storage path uses only base MIME type (strips charset for directory structure)
28
- * - Content stored as raw bytes - charset only affects decoding on retrieval
29
- *
30
- * This design provides:
31
- * - O(1) content retrieval by checksum + mediaType
32
- * - Automatic deduplication (identical content = same file)
33
- * - Idempotent storage operations
34
- * - Proper file extensions for filesystem browsing
35
- * - Faithful preservation of character encoding metadata
22
+ * For example, storageUri "file://docs/overview.md" resolves to
23
+ * {projectRoot}/docs/overview.md
36
24
  */
37
25
 
38
26
  /**
39
- * Metadata for a representation being stored
40
- */
41
- interface RepresentationMetadata {
42
- mediaType: string;
43
- filename?: string;
44
- encoding?: string;
45
- language?: string;
46
- rel?: 'original' | 'thumbnail' | 'preview' | 'optimized' | 'derived' | 'other';
47
- }
48
- /**
49
- * Complete representation information
27
+ * Result of store() or register()
50
28
  */
51
- interface StoredRepresentation extends RepresentationMetadata {
52
- '@id': string;
53
- byteSize: number;
29
+ interface StoredResource {
30
+ storageUri: string;
54
31
  checksum: string;
32
+ byteSize: number;
55
33
  created: string;
56
34
  }
57
35
  /**
58
- * Interface for representation storage backends
36
+ * Manages files in the project working tree
59
37
  */
60
- interface RepresentationStore {
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>;
61
56
  /**
62
- * Store content and return representation metadata
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.
63
61
  *
64
- * @param content - Raw bytes to store
65
- * @param metadata - Representation metadata
66
- * @returns Complete representation info with checksum
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
67
  */
68
- store(content: Buffer, metadata: RepresentationMetadata): Promise<StoredRepresentation>;
68
+ register(storageUri: string, expectedChecksum?: string, options?: {
69
+ noGit?: boolean;
70
+ }): Promise<StoredResource>;
69
71
  /**
70
- * Retrieve content by checksum (content-addressed lookup)
72
+ * Read file content by URI.
71
73
  *
72
- * @param checksum - Content checksum as raw hex (e.g., "5aaa0b72...")
73
- * @param mediaType - MIME type (e.g., "text/markdown")
74
+ * @param storageUri - file:// URI
74
75
  * @returns Raw bytes
75
76
  */
76
- retrieve(checksum: string, mediaType: string): Promise<Buffer>;
77
- }
78
- /**
79
- * Filesystem implementation of RepresentationStore
80
- */
81
- declare class FilesystemRepresentationStore implements RepresentationStore {
82
- private basePath;
83
- private logger?;
84
- constructor(project: SemiontProject, logger?: Logger);
85
- store(content: Buffer, metadata: RepresentationMetadata): Promise<StoredRepresentation>;
86
- retrieve(checksum: string, mediaType: string): Promise<Buffer>;
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>;
87
91
  /**
88
- * Encode media type for filesystem path
89
- * Replaces "/" with "~1" to avoid directory separators
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
90
100
  *
91
- * @param mediaType - MIME type (e.g., "text/markdown")
92
- * @returns Encoded path segment (e.g., "text~1markdown")
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
93
104
  */
94
- private encodeMediaType;
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);
95
129
  }
96
130
 
97
131
  /**
@@ -137,4 +171,4 @@ declare function calculateChecksum(content: string | Buffer): string;
137
171
  */
138
172
  declare function verifyChecksum(content: string | Buffer, checksum: string): boolean;
139
173
 
140
- export { FilesystemRepresentationStore, type RepresentationMetadata, type RepresentationStore, type StoredRepresentation, calculateChecksum, getExtensionForMimeType, hasKnownExtension, verifyChecksum };
174
+ export { ChecksumMismatchError, type StoredResource, WorkingTreeStore, calculateChecksum, getExtensionForMimeType, hasKnownExtension, verifyChecksum };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- // src/representation-store.ts
1
+ // src/working-tree-store.ts
2
2
  import { promises as fs } from "fs";
3
+ import { execFileSync } from "child_process";
3
4
  import path from "path";
4
5
 
5
6
  // src/checksum.ts
@@ -13,6 +14,184 @@ function verifyChecksum(content, checksum) {
13
14
  return calculateChecksum(content) === checksum;
14
15
  }
15
16
 
17
+ // src/working-tree-store.ts
18
+ var WorkingTreeStore = class {
19
+ projectRoot;
20
+ gitSync;
21
+ logger;
22
+ constructor(project, logger) {
23
+ this.projectRoot = project.root;
24
+ this.gitSync = project.gitSync;
25
+ this.logger = logger;
26
+ }
27
+ shouldRunGit(noGit) {
28
+ return this.gitSync && !noGit;
29
+ }
30
+ /**
31
+ * Write content to disk at the location indicated by storageUri.
32
+ *
33
+ * API/GUI/AI path: caller provides bytes; file may not yet exist.
34
+ *
35
+ * @param content - Raw bytes to write
36
+ * @param storageUri - file:// URI (e.g. "file://docs/overview.md")
37
+ * @returns Stored resource metadata
38
+ */
39
+ async store(content, storageUri, options) {
40
+ const filePath = this.resolveUri(storageUri);
41
+ const checksum = calculateChecksum(content);
42
+ this.logger?.debug("Storing resource", { storageUri, byteSize: content.length });
43
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
44
+ await fs.writeFile(filePath, content);
45
+ if (this.shouldRunGit(options?.noGit)) {
46
+ execFileSync("git", ["add", filePath], { cwd: this.projectRoot });
47
+ }
48
+ this.logger?.info("Resource stored", { storageUri, checksum, byteSize: content.length });
49
+ return {
50
+ storageUri,
51
+ checksum,
52
+ byteSize: content.length,
53
+ created: (/* @__PURE__ */ new Date()).toISOString()
54
+ };
55
+ }
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
+ async register(storageUri, expectedChecksum, options) {
69
+ const filePath = this.resolveUri(storageUri);
70
+ this.logger?.debug("Registering resource", { storageUri });
71
+ const content = await fs.readFile(filePath);
72
+ const checksum = calculateChecksum(content);
73
+ if (expectedChecksum !== void 0 && !verifyChecksum(content, expectedChecksum)) {
74
+ throw new ChecksumMismatchError(storageUri, expectedChecksum, checksum);
75
+ }
76
+ if (this.shouldRunGit(options?.noGit)) {
77
+ execFileSync("git", ["add", filePath], { cwd: this.projectRoot });
78
+ }
79
+ this.logger?.info("Resource registered", { storageUri, checksum, byteSize: content.length });
80
+ return {
81
+ storageUri,
82
+ checksum,
83
+ byteSize: content.length,
84
+ created: (/* @__PURE__ */ new Date()).toISOString()
85
+ };
86
+ }
87
+ /**
88
+ * Read file content by URI.
89
+ *
90
+ * @param storageUri - file:// URI
91
+ * @returns Raw bytes
92
+ */
93
+ async retrieve(storageUri) {
94
+ const filePath = this.resolveUri(storageUri);
95
+ try {
96
+ return await fs.readFile(filePath);
97
+ } catch (error) {
98
+ if (error.code === "ENOENT") {
99
+ throw new Error(`Resource not found: ${storageUri}`);
100
+ }
101
+ throw error;
102
+ }
103
+ }
104
+ /**
105
+ * Move a file from one URI to another.
106
+ *
107
+ * If .git/ exists in the project root and noGit is not set, runs `git mv`.
108
+ * Otherwise (no .git/ or noGit: true), runs fs.rename.
109
+ *
110
+ * @param fromUri - Current file:// URI
111
+ * @param toUri - New file:// URI
112
+ * @param options.noGit - Skip git mv even if .git/ is present
113
+ */
114
+ async move(fromUri, toUri, options) {
115
+ const fromPath = this.resolveUri(fromUri);
116
+ const toPath = this.resolveUri(toUri);
117
+ this.logger?.debug("Moving resource", { fromUri, toUri });
118
+ await fs.mkdir(path.dirname(toPath), { recursive: true });
119
+ if (this.shouldRunGit(options?.noGit)) {
120
+ execFileSync("git", ["mv", fromPath, toPath], { cwd: this.projectRoot });
121
+ } else {
122
+ await fs.rename(fromPath, toPath);
123
+ }
124
+ this.logger?.info("Resource moved", { fromUri, toUri });
125
+ }
126
+ /**
127
+ * Remove a file from the working tree.
128
+ *
129
+ * If .git/ exists and noGit is not set:
130
+ * - keepFile false (default): runs `git rm` (removes from index and disk)
131
+ * - keepFile true: runs `git rm --cached` (removes from index only, file stays on disk)
132
+ * If no .git/ or noGit: true:
133
+ * - keepFile false: runs fs.unlink
134
+ * - keepFile true: no-op on filesystem
135
+ *
136
+ * @param storageUri - file:// URI
137
+ * @param options.noGit - Skip git rm even if .git/ is present
138
+ * @param options.keepFile - Remove from git index only; leave file on disk
139
+ */
140
+ async remove(storageUri, options) {
141
+ const filePath = this.resolveUri(storageUri);
142
+ const keepFile = options?.keepFile ?? false;
143
+ this.logger?.debug("Removing resource", { storageUri, keepFile });
144
+ const useGit = this.shouldRunGit(options?.noGit);
145
+ if (useGit) {
146
+ const gitArgs = keepFile ? ["rm", "--cached", filePath] : ["rm", filePath];
147
+ execFileSync("git", gitArgs, { cwd: this.projectRoot });
148
+ this.logger?.info("Resource removed", { storageUri, keepFile, git: true });
149
+ return;
150
+ }
151
+ if (keepFile) {
152
+ this.logger?.info("Resource removed from index (file kept on disk)", { storageUri });
153
+ return;
154
+ }
155
+ try {
156
+ await fs.unlink(filePath);
157
+ this.logger?.info("Resource removed", { storageUri });
158
+ } catch (error) {
159
+ if (error.code === "ENOENT") {
160
+ this.logger?.warn("Resource file already absent", { storageUri });
161
+ return;
162
+ }
163
+ throw error;
164
+ }
165
+ }
166
+ /**
167
+ * Convert a file:// URI to an absolute filesystem path.
168
+ *
169
+ * "file://docs/overview.md" → "{projectRoot}/docs/overview.md"
170
+ *
171
+ * @param storageUri - file:// URI
172
+ * @returns Absolute path
173
+ */
174
+ resolveUri(storageUri) {
175
+ if (!storageUri.startsWith("file://")) {
176
+ throw new Error(`Invalid storage URI (must start with file://): ${storageUri}`);
177
+ }
178
+ const relativePath = storageUri.slice("file://".length);
179
+ return path.join(this.projectRoot, relativePath);
180
+ }
181
+ };
182
+ var ChecksumMismatchError = class extends Error {
183
+ constructor(storageUri, expected, actual) {
184
+ super(
185
+ `Checksum mismatch for ${storageUri}: expected ${expected.slice(0, 8)}... but got ${actual.slice(0, 8)}...
186
+ The file on disk differs from the recorded checksum. Has it been modified since staging?`
187
+ );
188
+ this.storageUri = storageUri;
189
+ this.expected = expected;
190
+ this.actual = actual;
191
+ this.name = "ChecksumMismatchError";
192
+ }
193
+ };
194
+
16
195
  // src/mime-extensions.ts
17
196
  var MIME_TO_EXTENSION = {
18
197
  // Text formats
@@ -98,115 +277,9 @@ function hasKnownExtension(mediaType) {
98
277
  const normalized = mediaType.toLowerCase().split(";")[0].trim();
99
278
  return normalized in MIME_TO_EXTENSION;
100
279
  }
101
-
102
- // src/representation-store.ts
103
- var FilesystemRepresentationStore = class {
104
- basePath;
105
- logger;
106
- constructor(project, logger) {
107
- this.logger = logger;
108
- this.basePath = project.representationsDir;
109
- }
110
- async store(content, metadata) {
111
- const checksum = calculateChecksum(content);
112
- const baseMediaType = metadata.mediaType.split(";")[0].trim();
113
- const mediaTypePath = this.encodeMediaType(baseMediaType);
114
- const extension = getExtensionForMimeType(baseMediaType);
115
- if (!checksum || checksum.length < 4) {
116
- throw new Error(`Invalid checksum: ${checksum}`);
117
- }
118
- const ab = checksum.substring(0, 2);
119
- const cd = checksum.substring(2, 4);
120
- const filePath = path.join(
121
- this.basePath,
122
- mediaTypePath,
123
- ab,
124
- cd,
125
- `rep-${checksum}${extension}`
126
- );
127
- this.logger?.debug("Storing representation", {
128
- checksum,
129
- mediaType: baseMediaType,
130
- byteSize: content.length,
131
- filename: metadata.filename
132
- });
133
- await fs.mkdir(path.dirname(filePath), { recursive: true });
134
- await fs.writeFile(filePath, content);
135
- this.logger?.info("Representation stored", {
136
- checksum,
137
- mediaType: baseMediaType,
138
- byteSize: content.length,
139
- path: filePath
140
- });
141
- return {
142
- "@id": checksum,
143
- // Use checksum as the ID (content-addressed)
144
- ...metadata,
145
- byteSize: content.length,
146
- checksum,
147
- created: (/* @__PURE__ */ new Date()).toISOString()
148
- };
149
- }
150
- async retrieve(checksum, mediaType) {
151
- const baseMediaType = mediaType.split(";")[0].trim();
152
- const mediaTypePath = this.encodeMediaType(baseMediaType);
153
- const extension = getExtensionForMimeType(baseMediaType);
154
- if (!checksum || checksum.length < 4) {
155
- throw new Error(`Invalid checksum: ${checksum}`);
156
- }
157
- const ab = checksum.substring(0, 2);
158
- const cd = checksum.substring(2, 4);
159
- const filePath = path.join(
160
- this.basePath,
161
- mediaTypePath,
162
- ab,
163
- cd,
164
- `rep-${checksum}${extension}`
165
- );
166
- this.logger?.debug("Retrieving representation", {
167
- checksum,
168
- mediaType: baseMediaType
169
- });
170
- try {
171
- const content = await fs.readFile(filePath);
172
- this.logger?.info("Representation retrieved", {
173
- checksum,
174
- mediaType: baseMediaType,
175
- byteSize: content.length,
176
- path: filePath
177
- });
178
- return content;
179
- } catch (error) {
180
- if (error.code === "ENOENT") {
181
- this.logger?.warn("Representation not found", {
182
- checksum,
183
- mediaType: baseMediaType,
184
- path: filePath
185
- });
186
- throw new Error(`Representation not found for checksum ${checksum} with mediaType ${mediaType}`);
187
- }
188
- this.logger?.error("Failed to retrieve representation", {
189
- checksum,
190
- mediaType: baseMediaType,
191
- error: error.message,
192
- path: filePath
193
- });
194
- throw error;
195
- }
196
- }
197
- /**
198
- * Encode media type for filesystem path
199
- * Replaces "/" with "~1" to avoid directory separators
200
- *
201
- * @param mediaType - MIME type (e.g., "text/markdown")
202
- * @returns Encoded path segment (e.g., "text~1markdown")
203
- */
204
- encodeMediaType(mediaType) {
205
- return mediaType.replace(/\//g, "~1");
206
- }
207
- };
208
280
  export {
209
- FilesystemRepresentationStore,
281
+ ChecksumMismatchError,
282
+ WorkingTreeStore,
210
283
  calculateChecksum,
211
284
  getExtensionForMimeType,
212
285
  hasKnownExtension,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/representation-store.ts","../src/checksum.ts","../src/mime-extensions.ts"],"sourcesContent":["/**\n * RepresentationStore - Content-addressed storage for byte-level resource representations\n *\n * Handles storage and retrieval of concrete byte-level renditions of resources.\n * Uses content-addressed storage where the checksum IS the filename.\n * Supports multiple storage backends (filesystem, S3, IPFS, etc.)\n *\n * Storage structure (filesystem):\n * basePath/representations/{mediaType}/{ab}/{cd}/rep-{checksum}{extension}\n *\n * Where:\n * - {mediaType} is base MIME type with \"/\" encoded as \"~1\" (e.g., \"text~1markdown\")\n * - {ab}/{cd} are first 4 hex digits of checksum for sharding\n * - {checksum} is the raw SHA-256 hex hash (e.g., \"5aaa0b72abc123...\")\n * - {extension} is derived from base MIME type (.md, .txt, .png, etc.)\n *\n * Example:\n * For content with checksum \"5aaa0b72abc123...\" and mediaType \"text/markdown; charset=iso-8859-1\":\n * - Storage path: basePath/representations/text~1markdown/5a/aa/rep-5aaa0b72abc123....md\n * - Stored mediaType: \"text/markdown; charset=iso-8859-1\" (full type with charset preserved)\n *\n * Character Encoding:\n * - Charset parameters in mediaType are preserved in metadata (e.g., \"text/plain; charset=iso-8859-1\")\n * - Storage path uses only base MIME type (strips charset for directory structure)\n * - Content stored as raw bytes - charset only affects decoding on retrieval\n *\n * This design provides:\n * - O(1) content retrieval by checksum + mediaType\n * - Automatic deduplication (identical content = same file)\n * - Idempotent storage operations\n * - Proper file extensions for filesystem browsing\n * - Faithful preservation of character encoding metadata\n */\n\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport type { SemiontProject } from '@semiont/core/node';\nimport type { Logger } from '@semiont/core';\nimport { calculateChecksum } from './checksum';\nimport { getExtensionForMimeType } from './mime-extensions';\n\n/**\n * Metadata for a representation being stored\n */\nexport interface RepresentationMetadata {\n mediaType: string; // REQUIRED - MIME type\n filename?: string;\n encoding?: string;\n language?: string;\n rel?: 'original' | 'thumbnail' | 'preview' | 'optimized' | 'derived' | 'other';\n}\n\n/**\n * Complete representation information\n */\nexport interface StoredRepresentation extends RepresentationMetadata {\n '@id': string; // Representation ID (same as checksum)\n byteSize: number; // Size in bytes\n checksum: string; // Raw SHA-256 hex hash\n created: string; // ISO 8601 timestamp\n}\n\n/**\n * Interface for representation storage backends\n */\nexport interface RepresentationStore {\n /**\n * Store content and return representation metadata\n *\n * @param content - Raw bytes to store\n * @param metadata - Representation metadata\n * @returns Complete representation info with checksum\n */\n store(content: Buffer, metadata: RepresentationMetadata): Promise<StoredRepresentation>;\n\n /**\n * Retrieve content by checksum (content-addressed lookup)\n *\n * @param checksum - Content checksum as raw hex (e.g., \"5aaa0b72...\")\n * @param mediaType - MIME type (e.g., \"text/markdown\")\n * @returns Raw bytes\n */\n retrieve(checksum: string, mediaType: string): Promise<Buffer>;\n}\n\n/**\n * Filesystem implementation of RepresentationStore\n */\nexport class FilesystemRepresentationStore implements RepresentationStore {\n private basePath: string;\n private logger?: Logger;\n\n constructor(project: SemiontProject, logger?: Logger) {\n this.logger = logger;\n this.basePath = project.representationsDir;\n }\n\n async store(content: Buffer, metadata: RepresentationMetadata): Promise<StoredRepresentation> {\n // Compute checksum (raw hex) - this will be used as the content address\n const checksum = calculateChecksum(content);\n\n // Strip charset/parameters for path - only use base MIME type for directory structure\n // e.g., \"text/plain; charset=iso-8859-1\" -> \"text/plain\"\n const baseMediaType = metadata.mediaType.split(';')[0]!.trim();\n const mediaTypePath = this.encodeMediaType(baseMediaType);\n const extension = getExtensionForMimeType(baseMediaType);\n\n if (!checksum || checksum.length < 4) {\n throw new Error(`Invalid checksum: ${checksum}`);\n }\n\n // Use first 4 hex digits for sharding: 5a/aa\n const ab = checksum.substring(0, 2);\n const cd = checksum.substring(2, 4);\n\n // Build file path using raw hex checksum as filename with proper extension\n const filePath = path.join(\n this.basePath,\n mediaTypePath,\n ab,\n cd,\n `rep-${checksum}${extension}`\n );\n\n this.logger?.debug('Storing representation', {\n checksum,\n mediaType: baseMediaType,\n byteSize: content.length,\n filename: metadata.filename\n });\n\n // Create directory structure programmatically\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n\n // Write content (idempotent - same content = same file)\n await fs.writeFile(filePath, content);\n\n this.logger?.info('Representation stored', {\n checksum,\n mediaType: baseMediaType,\n byteSize: content.length,\n path: filePath\n });\n\n return {\n '@id': checksum, // Use checksum as the ID (content-addressed)\n ...metadata,\n byteSize: content.length,\n checksum,\n created: new Date().toISOString(),\n };\n }\n\n async retrieve(checksum: string, mediaType: string): Promise<Buffer> {\n // Strip charset/parameters for path - only use base MIME type for directory lookup\n // e.g., \"text/plain; charset=iso-8859-1\" -> \"text/plain\"\n const baseMediaType = mediaType.split(';')[0]!.trim();\n const mediaTypePath = this.encodeMediaType(baseMediaType);\n const extension = getExtensionForMimeType(baseMediaType);\n\n if (!checksum || checksum.length < 4) {\n throw new Error(`Invalid checksum: ${checksum}`);\n }\n\n // Use first 4 hex digits for sharding: 5a/aa\n const ab = checksum.substring(0, 2);\n const cd = checksum.substring(2, 4);\n\n // Build file path from raw hex checksum with proper extension\n const filePath = path.join(\n this.basePath,\n mediaTypePath,\n ab,\n cd,\n `rep-${checksum}${extension}`\n );\n\n this.logger?.debug('Retrieving representation', {\n checksum,\n mediaType: baseMediaType\n });\n\n try {\n const content = await fs.readFile(filePath);\n this.logger?.info('Representation retrieved', {\n checksum,\n mediaType: baseMediaType,\n byteSize: content.length,\n path: filePath\n });\n return content;\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n this.logger?.warn('Representation not found', {\n checksum,\n mediaType: baseMediaType,\n path: filePath\n });\n throw new Error(`Representation not found for checksum ${checksum} with mediaType ${mediaType}`);\n }\n this.logger?.error('Failed to retrieve representation', {\n checksum,\n mediaType: baseMediaType,\n error: error.message,\n path: filePath\n });\n throw error;\n }\n }\n\n /**\n * Encode media type for filesystem path\n * Replaces \"/\" with \"~1\" to avoid directory separators\n *\n * @param mediaType - MIME type (e.g., \"text/markdown\")\n * @returns Encoded path segment (e.g., \"text~1markdown\")\n */\n private encodeMediaType(mediaType: string): string {\n return mediaType.replace(/\\//g, '~1');\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 * 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"],"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 * 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/content",
3
- "version": "0.3.7",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Content-addressed storage for resource representations",
6
6
  "main": "./dist/index.js",