@ibgib/node-gib 0.0.22 → 0.0.24

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.
Files changed (81) hide show
  1. package/README.md +4 -0
  2. package/dist/email/aws/email-receiver-aws.mjs +3 -3
  3. package/dist/email/aws/email-receiver-aws.mjs.map +1 -1
  4. package/dist/email/aws/email-sender-aws.mjs +1 -1
  5. package/dist/email/aws/email-sender-aws.mjs.map +1 -1
  6. package/dist/email/email-sender.mjs +3 -3
  7. package/dist/email/email-sender.mjs.map +1 -1
  8. package/dist/email/verify/email-verify-helpers.mjs +5 -5
  9. package/dist/email/verify/email-verify-helpers.mjs.map +1 -1
  10. package/dist/identity/sso/custodian-auth-helpers.mjs +1 -1
  11. package/dist/identity/sso/custodian-auth-helpers.mjs.map +1 -1
  12. package/dist/identity/sso/sso-custodian-service.mjs +4 -4
  13. package/dist/identity/sso/sso-custodian-service.mjs.map +1 -1
  14. package/dist/serve-gib/handlers/api/email/email-inbound.handler.mjs +2 -2
  15. package/dist/serve-gib/handlers/api/email/email-inbound.handler.mjs.map +1 -1
  16. package/dist/serve-gib/handlers/api/email/email-outbound.handler.mjs +3 -3
  17. package/dist/serve-gib/handlers/api/email/email-outbound.handler.mjs.map +1 -1
  18. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.mjs +1 -1
  19. package/dist/serve-gib/handlers/api/email/email-verification-code.handler.mjs.map +1 -1
  20. package/dist/serve-gib/handlers/api/email/email-verify-code-onetime.handler.mjs +1 -1
  21. package/dist/serve-gib/handlers/api/email/email-verify-code-onetime.handler.mjs.map +1 -1
  22. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mjs +15 -15
  23. package/dist/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mjs.map +1 -1
  24. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mjs +2 -2
  25. package/dist/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mjs.map +1 -1
  26. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs +1 -1
  27. package/dist/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mjs.map +1 -1
  28. package/dist/serve-gib/handlers/api/keystone/keystone-genesis.handler.mjs +2 -2
  29. package/dist/serve-gib/handlers/api/keystone/keystone-genesis.handler.mjs.map +1 -1
  30. package/dist/serve-gib/handlers/api/keystone/keystone-lookup.handler.mjs +1 -1
  31. package/dist/serve-gib/handlers/api/keystone/keystone-lookup.handler.mjs.map +1 -1
  32. package/dist/serve-gib/handlers/api/keystone/sso-link.handler.mjs +6 -6
  33. package/dist/serve-gib/handlers/api/keystone/sso-link.handler.mjs.map +1 -1
  34. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs +3 -3
  35. package/dist/serve-gib/handlers/api/keystone/sso-login.handler.mjs.map +1 -1
  36. package/dist/serve-gib/identity-index/identity-index-helpers.mjs +1 -1
  37. package/dist/serve-gib/identity-index/identity-index-helpers.mjs.map +1 -1
  38. package/dist/serve-gib/identity-index/identity-index-service.mjs +1 -1
  39. package/dist/serve-gib/identity-index/identity-index-service.mjs.map +1 -1
  40. package/dist/vcs/workspace-space-helper.d.mts +26 -0
  41. package/dist/vcs/workspace-space-helper.d.mts.map +1 -0
  42. package/dist/vcs/workspace-space-helper.mjs +71 -0
  43. package/dist/vcs/workspace-space-helper.mjs.map +1 -0
  44. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-constants.d.mts +7 -0
  45. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-constants.d.mts.map +1 -0
  46. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-constants.mjs +7 -0
  47. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-constants.mjs.map +1 -0
  48. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-types.d.mts +15 -0
  49. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-types.d.mts.map +1 -0
  50. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-types.mjs +23 -0
  51. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-types.mjs.map +1 -0
  52. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-v1.d.mts +66 -0
  53. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-v1.d.mts.map +1 -0
  54. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-v1.mjs +326 -0
  55. package/dist/witness/space/node-workspace-projection-space/node-workspace-projection-space-v1.mjs.map +1 -0
  56. package/package.json +5 -5
  57. package/src/email/aws/email-receiver-aws.mts +3 -3
  58. package/src/email/aws/email-sender-aws.mts +1 -1
  59. package/src/email/email-sender.mts +3 -3
  60. package/src/email/verify/email-verify-helpers.mts +5 -5
  61. package/src/identity/sso/custodian-auth-helpers.mts +1 -1
  62. package/src/identity/sso/sso-custodian-service.mts +4 -4
  63. package/src/serve-gib/handlers/api/email/email-inbound.handler.mts +2 -2
  64. package/src/serve-gib/handlers/api/email/email-outbound.handler.mts +3 -3
  65. package/src/serve-gib/handlers/api/email/email-verification-code.handler.mts +1 -1
  66. package/src/serve-gib/handlers/api/email/email-verify-code-onetime.handler.mts +1 -1
  67. package/src/serve-gib/handlers/api/email/email-verify.respec.mts +4 -4
  68. package/src/serve-gib/handlers/api/identity/keystone-custodian-add-master-password.handler.mts +15 -15
  69. package/src/serve-gib/handlers/api/identity/keystone-custodian-genesis.handler.mts +2 -2
  70. package/src/serve-gib/handlers/api/identity/keystone-custodian-register-delegate.handler.mts +1 -1
  71. package/src/serve-gib/handlers/api/keystone/keystone-genesis.handler.mts +2 -2
  72. package/src/serve-gib/handlers/api/keystone/keystone-lookup.handler.mts +1 -1
  73. package/src/serve-gib/handlers/api/keystone/sso-link.handler.mts +6 -6
  74. package/src/serve-gib/handlers/api/keystone/sso-login.handler.mts +3 -3
  75. package/src/serve-gib/identity-index/identity-index-helpers.mts +1 -1
  76. package/src/serve-gib/identity-index/identity-index-service.mts +1 -1
  77. package/src/vcs/workspace-space-helper.mts +104 -0
  78. package/src/witness/space/node-workspace-projection-space/node-workspace-projection-space-constants.mts +8 -0
  79. package/src/witness/space/node-workspace-projection-space/node-workspace-projection-space-types.mts +43 -0
  80. package/src/witness/space/node-workspace-projection-space/node-workspace-projection-space-v1.mts +416 -0
  81. package/src/witness/space/node-workspace-projection-space/node-workspace-projection-space-v1.respec.mts +212 -0
@@ -0,0 +1,416 @@
1
+ /**
2
+ * @module node-workspace-projection-space-v1
3
+ *
4
+ * Concrete Node.js workspace projection space implementation.
5
+ * Projects B2tFS items to physical OS disk and scans physical directories.
6
+ */
7
+
8
+ import { Dirent } from 'node:fs';
9
+ import fs from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import crypto from 'node:crypto';
12
+
13
+ import { clone, extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
14
+ import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
15
+ import { IbGibAddr } from '@ibgib/ts-gib/dist/types.mjs';
16
+ import { GLOBAL_LOG_A_LOT } from '@ibgib/core-gib/dist/core-constants.mjs';
17
+ import { IbGibSpaceAny } from '@ibgib/core-gib/dist/witness/space/space-base-v1.mjs';
18
+ import { getFromSpace } from '@ibgib/core-gib/dist/witness/space/space-helper.mjs';
19
+ import { WorkspaceProjectionSpaceBase_V1 } from '@ibgib/core-gib/dist/witness/space/workspace-projection-space/workspace-projection-space-base-v1.mjs';
20
+ import {
21
+ WorkspaceScanResultData,
22
+ WorkspaceFileObservation,
23
+ PendingCommentObservation,
24
+ } from '@ibgib/core-gib/dist/witness/space/workspace-projection-space/workspace-projection-space-types.mjs';
25
+ import { B2tFSItemIbGib_V1 } from '@ibgib/core-gib/dist/vcs/item/item-types.mjs';
26
+ import { isPathFiltered, parseFilterFileContent } from '@ibgib/core-gib/dist/vcs/filter/glob-helper.mjs';
27
+
28
+ import {
29
+ NodeWorkspaceProjectionSpaceData_V1,
30
+ NodeWorkspaceProjectionSpaceRel8ns_V1,
31
+ DEFAULT_NODE_WORKSPACE_PROJECTION_SPACE_DATA_V1,
32
+ } from './node-workspace-projection-space-types.mjs';
33
+
34
+ const logalot = GLOBAL_LOG_A_LOT;
35
+
36
+ export interface NodeWorkspaceProjectionSpaceOpts {
37
+ initialData?: NodeWorkspaceProjectionSpaceData_V1;
38
+ initialRel8ns?: NodeWorkspaceProjectionSpaceRel8ns_V1;
39
+ backingSpace?: IbGibSpaceAny;
40
+ }
41
+
42
+ /**
43
+ * Node.js Workspace Projection Space that projects B2tFS item DAGs directly onto
44
+ * the local filesystem and scans physical directories to report workspace state.
45
+ */
46
+ export class NodeWorkspaceProjectionSpace_V1 extends WorkspaceProjectionSpaceBase_V1<
47
+ NodeWorkspaceProjectionSpaceData_V1,
48
+ NodeWorkspaceProjectionSpaceRel8ns_V1
49
+ > {
50
+ protected override lc: string = `[${NodeWorkspaceProjectionSpace_V1.name}]`;
51
+
52
+ /** Backing CAS space where source BinIbGibs and child items reside */
53
+ backingSpace?: IbGibSpaceAny;
54
+
55
+ constructor(
56
+ initialDataOrOpts?: NodeWorkspaceProjectionSpaceData_V1 | NodeWorkspaceProjectionSpaceOpts,
57
+ initialRel8ns?: NodeWorkspaceProjectionSpaceRel8ns_V1,
58
+ ) {
59
+ let data: NodeWorkspaceProjectionSpaceData_V1 | undefined;
60
+ let rel8ns: NodeWorkspaceProjectionSpaceRel8ns_V1 | undefined = initialRel8ns;
61
+ let backingSpace: IbGibSpaceAny | undefined;
62
+
63
+ if (initialDataOrOpts && ('workspaceRoot' in initialDataOrOpts || 'name' in initialDataOrOpts)) {
64
+ data = initialDataOrOpts as NodeWorkspaceProjectionSpaceData_V1;
65
+ } else if (initialDataOrOpts && typeof initialDataOrOpts === 'object') {
66
+ const opts = initialDataOrOpts as NodeWorkspaceProjectionSpaceOpts;
67
+ data = opts.initialData;
68
+ rel8ns = opts.initialRel8ns ?? initialRel8ns;
69
+ backingSpace = opts.backingSpace;
70
+ }
71
+
72
+ super(data ?? (clone(DEFAULT_NODE_WORKSPACE_PROJECTION_SPACE_DATA_V1) as NodeWorkspaceProjectionSpaceData_V1), rel8ns);
73
+ this.backingSpace = backingSpace;
74
+ }
75
+
76
+ // #region Implementation of Abstract Extension Hooks
77
+
78
+ /**
79
+ * Projects an item (and its recursive child hierarchy or binary payload) onto the physical workspace.
80
+ */
81
+ protected override async putItemToWorkspaceImpl({
82
+ item,
83
+ subPath,
84
+ }: {
85
+ item: B2tFSItemIbGib_V1;
86
+ subPath?: string;
87
+ }): Promise<void> {
88
+ const lc = `${this.lc}[${this.putItemToWorkspaceImpl.name}]`;
89
+ try {
90
+ if (logalot) { console.log(`${lc} projecting item to workspace...`); }
91
+
92
+ const workspaceRoot = this.data?.workspaceRoot;
93
+ if (!workspaceRoot) {
94
+ throw new Error(`${lc} workspaceRoot is required in space data (E: genuuid)`);
95
+ }
96
+
97
+ if (!item || !item.data) {
98
+ throw new Error(`${lc} item and item.data are required (E: genuuid)`);
99
+ }
100
+
101
+ const { fsType, name } = item.data;
102
+
103
+ let itemRelPath: string;
104
+ if (subPath === undefined) {
105
+ itemRelPath = fsType === 'folder' ? '' : (name || '');
106
+ } else {
107
+ itemRelPath = name ? (subPath ? path.join(subPath, name) : name) : subPath;
108
+ }
109
+
110
+ const targetPath = itemRelPath ? path.join(workspaceRoot, itemRelPath) : workspaceRoot;
111
+
112
+ if (fsType === 'folder') {
113
+ await fs.mkdir(targetPath, { recursive: true });
114
+
115
+ const childAddrs = item.rel8ns?.b2tFS_child || item.rel8ns?.b2tfs_child || [];
116
+ for (const childAddr of childAddrs) {
117
+ const childItem = (await this.fetchIbGib(childAddr)) as B2tFSItemIbGib_V1;
118
+ await this.putItemToWorkspaceImpl({
119
+ item: childItem,
120
+ subPath: itemRelPath,
121
+ });
122
+ }
123
+ } else if (fsType === 'file') {
124
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
125
+
126
+ if (!item.rel8ns) {
127
+ throw new Error(`${lc} item.rel8ns required for file item (E: genuuid)`);
128
+ }
129
+
130
+ const binAddrs = item.rel8ns.bin;
131
+ if (!binAddrs || binAddrs.length === 0) {
132
+ throw new Error(`${lc} item.rel8ns.bin required for file item ${name} (E: genuuid)`);
133
+ }
134
+
135
+ const latestBinAddr = binAddrs.at(-1)!;
136
+ const binIbGib = await this.fetchIbGib(latestBinAddr);
137
+
138
+ if (binIbGib.data === undefined) {
139
+ throw new Error(`${lc} BinIbGib at addr ${latestBinAddr} has undefined data (E: genuuid)`);
140
+ }
141
+
142
+ let fileData: Uint8Array | string;
143
+ if (binIbGib.data instanceof Uint8Array || Buffer.isBuffer(binIbGib.data)) {
144
+ fileData = binIbGib.data as Uint8Array;
145
+ } else if (typeof binIbGib.data === 'string') {
146
+ fileData = binIbGib.data;
147
+ } else if (typeof binIbGib.data === 'object') {
148
+ const vals = Object.values(binIbGib.data as object);
149
+ fileData = new Uint8Array(vals);
150
+ } else {
151
+ fileData = new Uint8Array(0);
152
+ }
153
+
154
+ await fs.writeFile(targetPath, fileData);
155
+ }
156
+ } catch (error) {
157
+ console.error(`${lc} ${extractErrorMsg(error)} (E: genuuid)`);
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Scans the physical workspace directory and returns file observations and pending companion notes.
164
+ */
165
+ protected override async scanWorkspaceImpl({
166
+ subPath,
167
+ filterPatterns,
168
+ }: {
169
+ subPath?: string;
170
+ filterPatterns?: string[];
171
+ }): Promise<WorkspaceScanResultData> {
172
+ const lc = `${this.lc}[${this.scanWorkspaceImpl.name}]`;
173
+ try {
174
+ if (logalot) { console.log(`${lc} starting physical scan...`); }
175
+
176
+ const workspaceRoot = this.data?.workspaceRoot;
177
+ if (!workspaceRoot) {
178
+ throw new Error(`${lc} workspaceRoot is required in space data (E: genuuid)`);
179
+ }
180
+
181
+ const scanDir = subPath ? path.join(workspaceRoot, subPath) : workspaceRoot;
182
+ const vcsFolderName = this.data?.vcsFolderName || '.vcsgib';
183
+ let patterns = filterPatterns ?? this.data?.filterPatterns;
184
+
185
+ if (!patterns || patterns.length === 0) {
186
+ try {
187
+ const ignorePath = path.join(workspaceRoot, '.ibgibignore');
188
+ const ignoreContent = await fs.readFile(ignorePath, 'utf8');
189
+ patterns = parseFilterFileContent(ignoreContent);
190
+ } catch {
191
+ patterns = [];
192
+ }
193
+ }
194
+
195
+ const files = new Map<string, WorkspaceFileObservation>();
196
+ const pendingComments: PendingCommentObservation[] = [];
197
+
198
+ await this.scanDirRecursive({
199
+ dirPath: scanDir,
200
+ baseDir: workspaceRoot,
201
+ vcsFolderName,
202
+ filterPatterns: patterns,
203
+ outFiles: files,
204
+ outComments: pendingComments,
205
+ });
206
+
207
+ return {
208
+ files,
209
+ pendingComments,
210
+ };
211
+ } catch (error) {
212
+ console.error(`${lc} ${extractErrorMsg(error)} (E: genuuid)`);
213
+ throw error;
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Unlinks a file, folder, or comment file from disk.
219
+ */
220
+ protected override async deleteFromWorkspaceImpl({
221
+ relPath,
222
+ ibGib,
223
+ }: {
224
+ relPath?: string;
225
+ ibGib?: IbGib_V1;
226
+ }): Promise<void> {
227
+ const lc = `${this.lc}[${this.deleteFromWorkspaceImpl.name}]`;
228
+ try {
229
+ if (logalot) { console.log(`${lc} deleting from workspace...`); }
230
+
231
+ const workspaceRoot = this.data?.workspaceRoot;
232
+ if (!workspaceRoot) {
233
+ throw new Error(`${lc} workspaceRoot is required (E: genuuid)`);
234
+ }
235
+
236
+ const targetRelPath = relPath || (ibGib?.data as any)?.name;
237
+ if (!targetRelPath) {
238
+ throw new Error(`${lc} relPath or item name required for deletion (E: genuuid)`);
239
+ }
240
+
241
+ const fullPath = path.join(workspaceRoot, targetRelPath);
242
+ await fs.rm(fullPath, { recursive: true, force: true });
243
+ } catch (error) {
244
+ console.error(`${lc} ${extractErrorMsg(error)} (E: genuuid)`);
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Reads raw bytes of a physical workspace file from disk.
251
+ */
252
+ protected override async getFileBytesImpl({
253
+ relPath,
254
+ }: {
255
+ relPath: string;
256
+ }): Promise<Uint8Array | undefined> {
257
+ const lc = `${this.lc}[${this.getFileBytesImpl.name}]`;
258
+ try {
259
+ const workspaceRoot = this.data?.workspaceRoot;
260
+ if (!workspaceRoot) { return undefined; }
261
+ const fullPath = path.isAbsolute(relPath) ? relPath : path.join(workspaceRoot, relPath);
262
+ const buf = await fs.readFile(fullPath);
263
+ return new Uint8Array(buf);
264
+ } catch {
265
+ return undefined;
266
+ }
267
+ }
268
+
269
+ // #endregion
270
+
271
+ // #region Internal Helpers
272
+
273
+ /**
274
+ * Recursively scans physical directories on disk.
275
+ */
276
+ private async scanDirRecursive({
277
+ dirPath,
278
+ baseDir,
279
+ vcsFolderName,
280
+ filterPatterns,
281
+ outFiles,
282
+ outComments,
283
+ }: {
284
+ dirPath: string;
285
+ baseDir: string;
286
+ vcsFolderName: string;
287
+ filterPatterns: string[];
288
+ outFiles: Map<string, WorkspaceFileObservation>;
289
+ outComments: PendingCommentObservation[];
290
+ }): Promise<void> {
291
+ const lc = `${this.lc}[${this.scanDirRecursive.name}]`;
292
+ let entries: Dirent[] = [];
293
+ try {
294
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
295
+ } catch (error) {
296
+ console.error(`${lc} Failed to read directory ${dirPath}: ${extractErrorMsg(error)} (E: genuuid)`);
297
+ throw error;
298
+ }
299
+
300
+ for (const entry of entries) {
301
+ const fullEntryPath = path.join(dirPath, entry.name);
302
+ const relPath = path.relative(baseDir, fullEntryPath).replace(/\\/g, '/');
303
+
304
+ // Exclude VCS metadata folders, .git, and worktrees folder 'b'
305
+ if (
306
+ relPath === vcsFolderName || relPath.startsWith(vcsFolderName + '/') ||
307
+ relPath === '.git' || relPath.startsWith('.git/') ||
308
+ relPath === 'b' || relPath.startsWith('b/')
309
+ ) {
310
+ continue;
311
+ }
312
+
313
+ if (this.isFiltered(relPath, entry.isDirectory(), filterPatterns)) {
314
+ continue;
315
+ }
316
+
317
+ if (entry.isDirectory()) {
318
+ outFiles.set(relPath, {
319
+ relPath,
320
+ fullPath: fullEntryPath,
321
+ isDirectory: true,
322
+ });
323
+ await this.scanDirRecursive({
324
+ dirPath: fullEntryPath,
325
+ baseDir,
326
+ vcsFolderName,
327
+ filterPatterns,
328
+ outFiles,
329
+ outComments,
330
+ });
331
+ } else if (entry.isFile()) {
332
+ if (this.isCommentMetadataFilename(entry.name)) {
333
+ const parentRelPath = path.relative(baseDir, dirPath).replace(/\\/g, '/');
334
+ const normalizedParent = parentRelPath === '.' ? '' : parentRelPath;
335
+ let content = '';
336
+ try {
337
+ content = await fs.readFile(fullEntryPath, 'utf8');
338
+ } catch (error) {
339
+ console.error(`${lc} Failed to read comment file ${fullEntryPath}: ${extractErrorMsg(error)} (E: genuuid)`);
340
+ throw error;
341
+ }
342
+
343
+ const topicName = this.extractCommentTopicName(entry.name);
344
+ const targetRelPath = normalizedParent ? `${normalizedParent}/${topicName}` : topicName;
345
+
346
+ outComments.push({
347
+ commentRelPath: relPath,
348
+ commentFullPath: fullEntryPath,
349
+ targetRelPath,
350
+ targetType: entry.name.startsWith('.ib.') ? 'file' : 'folder',
351
+ topicName,
352
+ content,
353
+ });
354
+ } else {
355
+ let fileHash: string;
356
+ let size: number;
357
+ try {
358
+ const fileData = await fs.readFile(fullEntryPath);
359
+ fileHash = crypto.createHash('sha256').update(fileData).digest('hex');
360
+ size = fileData.length;
361
+ } catch (error) {
362
+ console.error(`${lc} Failed to read file ${fullEntryPath}: ${extractErrorMsg(error)} (E: genuuid)`);
363
+ throw error;
364
+ }
365
+
366
+ outFiles.set(relPath, {
367
+ relPath,
368
+ fullPath: fullEntryPath,
369
+ isDirectory: false,
370
+ fileHash,
371
+ size,
372
+ });
373
+ }
374
+ }
375
+ }
376
+ }
377
+
378
+ private isCommentMetadataFilename(filename: string): boolean {
379
+ return filename.startsWith('.ib.') && filename.endsWith('.md') && filename.length > '.ib..md'.length;
380
+ }
381
+
382
+ private extractCommentTopicName(filename: string): string {
383
+ if (!this.isCommentMetadataFilename(filename)) { return ''; }
384
+ return filename.slice(4, -3);
385
+ }
386
+
387
+ private isFiltered(relPath: string, isDirectory: boolean, filterPatterns: string[]): boolean {
388
+ return isPathFiltered({ relativePath: relPath, isDirectory, filterPatterns });
389
+ }
390
+
391
+ /**
392
+ * Helper to fetch an IbGib strictly from backingSpace with fail-fast validation.
393
+ */
394
+ private async fetchIbGib(addr: IbGibAddr): Promise<IbGib_V1> {
395
+ const lc = `${this.lc}[${this.fetchIbGib.name}]`;
396
+ try {
397
+ if (!this.backingSpace) {
398
+ throw new Error(`${lc} backingSpace required to fetch IbGib at ${addr} (E: genuuid)`);
399
+ }
400
+
401
+ const resGet = await getFromSpace({ space: this.backingSpace, addrs: [addr] });
402
+ if (!resGet.success || !resGet.ibGibs || resGet.ibGibs.length === 0) {
403
+ const resIbGib = resGet.rawResultIbGib as any;
404
+ const addrsNotFound = resIbGib?.data?.addrsNotFound ?? 'unknown';
405
+ throw new Error(`${lc} couldn't find addr ${addr} in backingSpace. addrsNotFound: ${addrsNotFound} (E: genuuid)`);
406
+ }
407
+
408
+ return resGet.ibGibs[0];
409
+ } catch (error) {
410
+ console.error(`${lc} ${extractErrorMsg(error)} (E: genuuid)`);
411
+ throw error;
412
+ }
413
+ }
414
+
415
+ // #endregion
416
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * @module node-workspace-projection-space-v1.respec
3
+ *
4
+ * Unit tests for NodeWorkspaceProjectionSpace_V1.
5
+ */
6
+
7
+ import path from 'node:path';
8
+ import fs from 'node:fs/promises';
9
+ import crypto from 'node:crypto';
10
+
11
+ import {
12
+ respecfully, ifWe, ifWeMight, iReckon, firstOfAll, lastOfAll, // don't remove ifWe/ifWeMight even if unused
13
+ } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
14
+ import { Factory_V1 as factory } from '@ibgib/ts-gib/dist/V1/factory.mjs';
15
+ import { getGib } from '@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs';
16
+ import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
17
+ import { argy_ } from '@ibgib/core-gib/dist/witness/witness-helper.mjs';
18
+ import { putInSpace } from '@ibgib/core-gib/dist/witness/space/space-helper.mjs';
19
+ import { CMD_MODIFIER_WORKSPACE_SCAN } from '@ibgib/core-gib/dist/witness/space/workspace-projection-space/workspace-projection-space-constants.mjs';
20
+ import { WorkspaceScanResultData } from '@ibgib/core-gib/dist/witness/space/workspace-projection-space/workspace-projection-space-types.mjs';
21
+
22
+ import { NodeFilesystemSpace_V1 } from '../node-filesystem-space/node-filesystem-space-v1.mjs';
23
+ import { DEFAULT_NODE_FILESYSTEM_SPACE_DATA_V1 } from '../node-filesystem-space/node-filesystem-space-types.mjs';
24
+ import { NodeWorkspaceProjectionSpace_V1 } from './node-workspace-projection-space-v1.mjs';
25
+
26
+ const lc = `[node-workspace-projection-space-v1.respec.node.mts]`;
27
+
28
+ await respecfully(lc, `NodeWorkspaceProjectionSpace_V1`, async () => {
29
+
30
+ const testRootDir = path.join(process.cwd(), '.test_node_workspace_projection');
31
+ const casSpaceDir = path.join(testRootDir, '.vcsgib', 'ibgib', 'b2s_main');
32
+ const worktreeDir = path.join(testRootDir, 'working');
33
+
34
+ firstOfAll(lc, async () => {
35
+ await fs.mkdir(casSpaceDir, { recursive: true });
36
+ await fs.mkdir(worktreeDir, { recursive: true });
37
+ });
38
+
39
+ lastOfAll(lc, async () => {
40
+ await fs.rm(testRootDir, { recursive: true, force: true });
41
+ });
42
+
43
+ await ifWeMight(lc, `reifies files and child folders to disk and observes them via workspace_scan`, async () => {
44
+ // 1. Initialize CAS backing space
45
+ const backingSpace = new NodeFilesystemSpace_V1({
46
+ ...DEFAULT_NODE_FILESYSTEM_SPACE_DATA_V1,
47
+ workspaceRoot: testRootDir,
48
+ baseSubPath: '.vcsgib/ibgib',
49
+ spaceSubPath: 'b2s_main',
50
+ baseDir: testRootDir as any,
51
+ });
52
+ await backingSpace.initialized;
53
+
54
+ // 2. Initialize Projection Space
55
+ const projectionSpace = new NodeWorkspaceProjectionSpace_V1({
56
+ initialData: {
57
+ workspaceRoot: worktreeDir,
58
+ vcsFolderName: '.vcsgib',
59
+ classname: 'NodeWorkspaceProjectionSpace_V1',
60
+ name: 'test_projection',
61
+ uuid: 'proj-1',
62
+ version: '1',
63
+ },
64
+ backingSpace,
65
+ });
66
+ await projectionSpace.initialized;
67
+
68
+ // 3. Create BinIbGib and store in backing space
69
+ const fileContent = 'export const greeting = "hello projection space";\n';
70
+ const fileBytes = Buffer.from(fileContent);
71
+ const fileHash = crypto.createHash('sha256').update(fileBytes).digest('hex');
72
+ const binIb = `bin ${fileHash}`;
73
+ const binGib = await getGib({ ibGib: { ib: binIb, data: new Uint8Array(fileBytes) as any } });
74
+ const binIbGib = {
75
+ ib: binIb,
76
+ gib: binGib,
77
+ data: new Uint8Array(fileBytes),
78
+ };
79
+ const binAddr = getIbGibAddr({ ibGib: binIbGib });
80
+ await putInSpace({ ibGib: binIbGib, space: backingSpace });
81
+
82
+ // 4. Create child file item
83
+ const fileItemRes = await factory.firstGen({
84
+ ib: 'b2tfs_item file index.ts',
85
+ parentIbGib: factory.primitive({ ib: 'b2tfs_item' }),
86
+ data: {
87
+ name: 'index.ts',
88
+ fsType: 'file',
89
+ },
90
+ rel8ns: {
91
+ bin: [binAddr],
92
+ },
93
+ dna: false,
94
+ });
95
+ const fileItem = fileItemRes.newIbGib!;
96
+ const fileAddr = getIbGibAddr({ ibGib: fileItem });
97
+ await putInSpace({ ibGib: fileItem, space: backingSpace });
98
+
99
+ // 5. Create root folder item containing child file
100
+ const rootFolderRes = await factory.firstGen({
101
+ ib: 'b2tfs_item folder root',
102
+ parentIbGib: factory.primitive({ ib: 'b2tfs_item' }),
103
+ data: {
104
+ name: '',
105
+ fsType: 'folder',
106
+ },
107
+ rel8ns: {
108
+ b2tfs_child: [fileAddr],
109
+ },
110
+ dna: false,
111
+ });
112
+ const rootFolderItem = rootFolderRes.newIbGib!;
113
+
114
+ // 6. Put root folder into projection space (reify to disk)
115
+ await putInSpace({
116
+ ibGib: rootFolderItem,
117
+ space: projectionSpace,
118
+ });
119
+
120
+ // 7. Verify disk file physically exists and content matches
121
+ const diskContent = await fs.readFile(path.join(worktreeDir, 'index.ts'), 'utf8');
122
+ iReckon(lc, diskContent).willEqual(fileContent);
123
+
124
+ // 8. Add a companion comment file on disk manually
125
+ const commentContent = '# Companion Comment\nfeat: initial projection test';
126
+ await fs.writeFile(path.join(worktreeDir, '.ib.index.ts.md'), commentContent, 'utf8');
127
+
128
+ // 9. Scan workspace observation
129
+ const scanArg = await argy_({
130
+ ibMetadata: 'workspace_projection_space',
131
+ argData: {
132
+ cmd: 'get',
133
+ cmdModifiers: [CMD_MODIFIER_WORKSPACE_SCAN],
134
+ },
135
+ });
136
+ const scanRes = await projectionSpace.witness(scanArg);
137
+
138
+ const scanData = scanRes?.data?.scanResult as WorkspaceScanResultData;
139
+ iReckon(lc, scanData).isGonnaBeTruthy();
140
+ iReckon(lc, scanData.files.has('index.ts')).isGonnaBeTrue();
141
+ iReckon(lc, scanData.files.get('index.ts')?.isDirectory).isGonnaBeFalse();
142
+ iReckon(lc, scanData.files.get('index.ts')?.fileHash).isGonnaBeTruthy();
143
+
144
+ // 10. Verify companion comment file detected in pendingComments
145
+ iReckon(lc, scanData.pendingComments.length).willEqual(1);
146
+ const commentObs = scanData.pendingComments[0];
147
+ iReckon(lc, commentObs.commentRelPath).willEqual('.ib.index.ts.md');
148
+ iReckon(lc, commentObs.targetRelPath).willEqual('index.ts');
149
+ iReckon(lc, commentObs.content).includes('feat: initial projection test');
150
+
151
+ // 11. Delete companion comment file via deleteFromSpace
152
+ const delArg = await argy_({
153
+ ibMetadata: 'workspace_projection_space',
154
+ argData: {
155
+ cmd: 'delete',
156
+ relPath: '.ib.index.ts.md',
157
+ },
158
+ });
159
+ await projectionSpace.witness(delArg);
160
+
161
+ // 12. Verify deleted on disk and no longer in scan
162
+ const scanArgAfter = await argy_({
163
+ ibMetadata: 'workspace_projection_space',
164
+ argData: {
165
+ cmd: 'get',
166
+ cmdModifiers: [CMD_MODIFIER_WORKSPACE_SCAN],
167
+ },
168
+ });
169
+ const scanResAfter = await projectionSpace.witness(scanArgAfter);
170
+ const scanDataAfter = scanResAfter?.data?.scanResult as WorkspaceScanResultData;
171
+ iReckon(lc, scanDataAfter.pendingComments.length).willEqual(0);
172
+ });
173
+
174
+ await ifWeMight(lc, `filters ignored paths matching .ibgibignore patterns`, async () => {
175
+ const ignoreWorktreeDir = path.join(testRootDir, 'working_ignore');
176
+ await fs.mkdir(ignoreWorktreeDir, { recursive: true });
177
+
178
+ const projectionSpace = new NodeWorkspaceProjectionSpace_V1({
179
+ initialData: {
180
+ workspaceRoot: ignoreWorktreeDir,
181
+ vcsFolderName: '.vcsgib',
182
+ filterPatterns: ['*.log', 'build/', 'node_modules/'],
183
+ classname: 'NodeWorkspaceProjectionSpace_V1',
184
+ name: 'test_ignore_projection',
185
+ uuid: 'proj-ignore-1',
186
+ version: '1',
187
+ },
188
+ });
189
+ await projectionSpace.initialized;
190
+
191
+ // Write sample files on disk
192
+ await fs.writeFile(path.join(ignoreWorktreeDir, 'main.ts'), 'console.log("main");');
193
+ await fs.writeFile(path.join(ignoreWorktreeDir, 'debug.log'), 'error at line 1');
194
+ await fs.mkdir(path.join(ignoreWorktreeDir, 'build'), { recursive: true });
195
+ await fs.writeFile(path.join(ignoreWorktreeDir, 'build', 'bundle.js'), 'bundle');
196
+
197
+ const scanArg = await argy_({
198
+ ibMetadata: 'workspace_projection_space',
199
+ argData: {
200
+ cmd: 'get',
201
+ cmdModifiers: [CMD_MODIFIER_WORKSPACE_SCAN],
202
+ },
203
+ });
204
+ const scanRes = await projectionSpace.witness(scanArg);
205
+ const scanData = scanRes?.data?.scanResult as WorkspaceScanResultData;
206
+
207
+ iReckon(lc, scanData.files.has('main.ts')).isGonnaBeTrue();
208
+ iReckon(lc, scanData.files.has('debug.log')).isGonnaBeFalse();
209
+ iReckon(lc, scanData.files.has('build/bundle.js')).isGonnaBeFalse();
210
+ });
211
+
212
+ });