@ibgib/vcs-gib 0.0.23 → 0.0.29

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 (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/VCS_CLI_USER_GUIDE.md +94 -89
  3. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  4. package/dist/AUTO-GENERATED-version.mjs +1 -1
  5. package/dist/cli/cli-arg-parser.d.mts +1 -0
  6. package/dist/cli/cli-arg-parser.d.mts.map +1 -1
  7. package/dist/cli/cli-arg-parser.mjs +5 -1
  8. package/dist/cli/cli-arg-parser.mjs.map +1 -1
  9. package/dist/cli/cli-constants.d.mts +6 -0
  10. package/dist/cli/cli-constants.d.mts.map +1 -1
  11. package/dist/cli/cli-constants.mjs +12 -0
  12. package/dist/cli/cli-constants.mjs.map +1 -1
  13. package/dist/cli/commands/add-cmd.d.mts.map +1 -1
  14. package/dist/cli/commands/add-cmd.mjs +11 -3
  15. package/dist/cli/commands/add-cmd.mjs.map +1 -1
  16. package/dist/cli/commands/commit-cmd.d.mts +5 -4
  17. package/dist/cli/commands/commit-cmd.d.mts.map +1 -1
  18. package/dist/cli/commands/commit-cmd.mjs +155 -35
  19. package/dist/cli/commands/commit-cmd.mjs.map +1 -1
  20. package/dist/cli/commands/generate-comment-cmd.d.mts +19 -0
  21. package/dist/cli/commands/generate-comment-cmd.d.mts.map +1 -0
  22. package/dist/cli/commands/generate-comment-cmd.mjs +140 -0
  23. package/dist/cli/commands/generate-comment-cmd.mjs.map +1 -0
  24. package/dist/cli/commands/log-cmd.d.mts +12 -3
  25. package/dist/cli/commands/log-cmd.d.mts.map +1 -1
  26. package/dist/cli/commands/log-cmd.mjs +242 -39
  27. package/dist/cli/commands/log-cmd.mjs.map +1 -1
  28. package/dist/cli/commands/status-cmd.d.mts +5 -6
  29. package/dist/cli/commands/status-cmd.d.mts.map +1 -1
  30. package/dist/cli/commands/status-cmd.mjs +51 -205
  31. package/dist/cli/commands/status-cmd.mjs.map +1 -1
  32. package/dist/cli/vcs-cli-main.d.mts.map +1 -1
  33. package/dist/cli/vcs-cli-main.mjs +25 -4
  34. package/dist/cli/vcs-cli-main.mjs.map +1 -1
  35. package/dist/engine/branch/branch-helper.d.mts +14 -0
  36. package/dist/engine/branch/branch-helper.d.mts.map +1 -1
  37. package/dist/engine/branch/branch-helper.mjs +21 -0
  38. package/dist/engine/branch/branch-helper.mjs.map +1 -1
  39. package/dist/engine/index/index-helper.d.mts.map +1 -1
  40. package/dist/engine/index/index-helper.mjs +3 -0
  41. package/dist/engine/index/index-helper.mjs.map +1 -1
  42. package/dist/engine/item/item-helper.d.mts.map +1 -1
  43. package/dist/engine/item/item-helper.mjs +5 -2
  44. package/dist/engine/item/item-helper.mjs.map +1 -1
  45. package/dist/engine/merge/b2tfs-3way-graft-strategy.d.mts +28 -0
  46. package/dist/engine/merge/b2tfs-3way-graft-strategy.d.mts.map +1 -1
  47. package/dist/engine/merge/b2tfs-3way-graft-strategy.mjs +84 -12
  48. package/dist/engine/merge/b2tfs-3way-graft-strategy.mjs.map +1 -1
  49. package/dist/engine/status/status-helper.d.mts +90 -0
  50. package/dist/engine/status/status-helper.d.mts.map +1 -0
  51. package/dist/engine/status/status-helper.mjs +286 -0
  52. package/dist/engine/status/status-helper.mjs.map +1 -0
  53. package/package.json +3 -3
  54. package/src/AUTO-GENERATED-version.mts +1 -1
  55. package/src/cli/cli-arg-parser.mts +5 -0
  56. package/src/cli/cli-constants.mts +13 -0
  57. package/src/cli/commands/add-cmd.mts +11 -3
  58. package/src/cli/commands/branch-checkout-cmd.respec.mts +54 -0
  59. package/src/cli/commands/commit-cmd.mts +171 -44
  60. package/src/cli/commands/commit-log-cmd.respec.mts +216 -3
  61. package/src/cli/commands/generate-comment-cmd.mts +171 -0
  62. package/src/cli/commands/log-cmd.mts +294 -42
  63. package/src/cli/commands/status-cmd.mts +58 -256
  64. package/src/cli/vcs-cli-main.mts +25 -4
  65. package/src/engine/branch/branch-helper.mts +33 -0
  66. package/src/engine/index/index-helper.mts +3 -0
  67. package/src/engine/item/item-helper.mts +5 -2
  68. package/src/engine/merge/b2tfs-3way-graft-strategy.mts +89 -10
  69. package/src/engine/merge/b2tfs-sync-comments.respec.mts +399 -0
  70. package/src/engine/status/status-helper.mts +383 -0
  71. package/tsconfig.test.tsbuildinfo +1 -1
  72. package/tsconfig.tsbuildinfo +1 -1
@@ -2,13 +2,15 @@
2
2
  * @module commit-cmd
3
3
  *
4
4
  * Command handler for `vcs commit -m "message"`.
5
- * Validates staged items, evolves the B2tFS root folder item graph in localSpace,
6
- * creates a CommentIbGib_V1 commit comment node, advances active B2tFSBranchIbGib_V1 pointer,
7
- * clears staged items in B2tFSIndexIbGib_V1, and updates read-only index projection cache.
5
+ * Ingests staged items and co-located .ib.*.md comment metadata files,
6
+ * attaches comments to corresponding file and folder b2tfs_items,
7
+ * attaches overall commit message to root b2tfs_item, evolves root item,
8
+ * advances B2tFSBranchIbGib_V1 with committedPaths, and clears index staging area.
8
9
  */
9
10
 
10
11
  import path from 'node:path';
11
12
  import fs from 'node:fs/promises';
13
+ import { Dirent } from 'node:fs';
12
14
  import crypto from 'node:crypto';
13
15
 
14
16
  import { getUUID, extractErrorMsg, delay } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
@@ -16,19 +18,18 @@ import { Factory_V1 } from '@ibgib/ts-gib/dist/V1/factory.mjs';
16
18
  import { ROOT } from '@ibgib/ts-gib/dist/V1/constants.mjs';
17
19
  import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
18
20
  import { putInSpace, persistTransformResult } from '@ibgib/core-gib/dist/witness/space/space-helper.mjs';
19
- import { getIbGibsFromCache_fallbackToSpaces } from '@ibgib/core-gib/dist/common/other/ibgib-helper.mjs';
20
21
  import { createCommentIbGib } from '@ibgib/core-gib/dist/common/comment/comment-helper.mjs';
21
22
  import { CommentIbGib_V1 } from '@ibgib/core-gib/dist/common/comment/comment-types.mjs';
22
23
 
23
24
  import { bootstrapVcsMetaspace } from '../../bootstrap/node-bootstrap.mjs';
24
25
  import { getOrCreateBranchSpace } from '../../engine/branch/branch-space-helper.mjs';
25
- import { getB2tFSItemIb } from '../../engine/item/item-atom-helper.mjs';
26
26
  import { B2tFSItemIbGib_V1, B2TFS_CHILD_ITEM_REL8N_NAME } from '../../engine/item/item-types.mjs';
27
27
  import { getIbGibWithMetaspaceFallback, createB2tFSItemIbGib, evolveB2tFSItemIbGib } from '../../engine/item/item-helper.mjs';
28
- import { getB2tFSBranchSpaceName } from '../../engine/branch/branch-helper.mjs';
28
+ import { getB2tFSBranchSpaceName, resolveContextualBranchName } from '../../engine/branch/branch-helper.mjs';
29
29
  import { B2tFSBranchIbGib_V1 } from '../../engine/branch/branch-types.mjs';
30
30
  import { getB2tFSIndexIbGib, updateB2tFSIndexIbGib, getSpaceQualifiedIbGibAddr } from '../../engine/index/index-helper.mjs';
31
31
  import { B2tFSStagedItemRecord } from '../../engine/index/index-types.mjs';
32
+ import { isCommentMetadataFilename, extractCommentTopicName } from '../../engine/status/status-helper.mjs';
32
33
  import { TransformResult } from '@ibgib/ts-gib/dist/types.mjs';
33
34
  import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
34
35
  import { GLOBAL_LOG_A_LOT } from '../../vcs-gib-constants.mjs';
@@ -45,7 +46,7 @@ export interface CommitCmdOpts {
45
46
 
46
47
  export interface CommitCmdResult {
47
48
  commitAddr: string;
48
- commentAddr: string;
49
+ commentAddr?: string;
49
50
  branchName: string;
50
51
  committedPaths: string[];
51
52
  rootItemAddr: string;
@@ -78,18 +79,18 @@ export async function executeCommitCmd({
78
79
  throw new Error(`Not an ibgib repository (directory ${vcsPath} does not exist) (E: 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d)`);
79
80
  }
80
81
 
81
- // Validate message
82
- if (!message || message.trim().length === 0) {
83
- throw new Error(`Commit message is required (E: 2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e)`);
84
- }
85
-
86
82
  // Fetch authoritative repository index from localSpace
87
83
  const indexIbGib = await getB2tFSIndexIbGib({ space: localSpace, vcsPath });
88
84
  if (!indexIbGib) {
89
85
  throw new Error(`Repository index B2tFSIndexIbGib_V1 not found in localSpace (E: 3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f)`);
90
86
  }
91
87
 
92
- const branchName = indexIbGib.data?.activeBranch || 'main';
88
+ const branchName = resolveContextualBranchName({
89
+ targetDir: absTargetDir,
90
+ repoRoot,
91
+ indexIbGib,
92
+ });
93
+ const activeWorktreeDir = branchName === 'main' ? repoRoot : path.join(repoRoot, 'b', branchName);
93
94
 
94
95
  // Target active branch space b2s_<activeBranch>
95
96
  const { space: activeSpace } = await getOrCreateBranchSpace({
@@ -98,24 +99,22 @@ export async function executeCommitCmd({
98
99
  branchName,
99
100
  });
100
101
 
101
- // Validate staged items
102
- const stagedPathsMap = indexIbGib.data?.stagedPaths || {};
103
- const stagedAddrs = indexIbGib.data?.staged_item || [];
102
+ // Verify there are staged items
103
+ const stagedPathsMap: { [relPath: string]: B2tFSStagedItemRecord } = { ...(indexIbGib.data?.stagedPaths || {}) };
104
104
  const committedPaths = Object.keys(stagedPathsMap);
105
-
106
- if (committedPaths.length === 0 || stagedAddrs.length === 0) {
107
- throw new Error(`Nothing to commit (no staged items) (E: 4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a)`);
105
+ if (committedPaths.length === 0) {
106
+ throw new Error(`Nothing to commit (no staged items found in index) (E: 4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a)`);
108
107
  }
109
108
 
110
- // Fetch active branch node from activeSpace
109
+ // Find active branch node
110
+ let currentBranchIbGib: B2tFSBranchIbGib_V1 | undefined;
111
+ let currentBranchAddr: string | undefined;
112
+
111
113
  const branchAddrs = [
112
114
  ...(indexIbGib.rel8ns?.branch || []),
113
115
  ...(indexIbGib.rel8ns?.branch_tjp || []),
114
116
  ];
115
117
 
116
- let currentBranchIbGib: B2tFSBranchIbGib_V1 | undefined;
117
- let currentBranchAddr: string | undefined;
118
-
119
118
  if (branchAddrs.length > 0) {
120
119
  let branchIbGibs: B2tFSBranchIbGib_V1[] = [];
121
120
  for (const bAddr of branchAddrs) {
@@ -133,35 +132,54 @@ export async function executeCommitCmd({
133
132
  (b) => (b as B2tFSBranchIbGib_V1)?.data?.name === branchName,
134
133
  ) as B2tFSBranchIbGib_V1 || (branchIbGibs.at(-1) as B2tFSBranchIbGib_V1);
135
134
 
136
-
137
135
  if (currentBranchIbGib) {
138
136
  currentBranchAddr = getIbGibAddr({ ibGib: currentBranchIbGib });
139
137
  }
140
138
  }
141
139
 
142
- const currentRootAddr = currentBranchIbGib?.rel8ns?.b2tfs_item?.at(-1);
143
-
140
+ const currentRootAddr = currentBranchIbGib?.rel8ns?.b2tFS_item?.at(-1) || currentBranchIbGib?.rel8ns?.b2tfs_item?.at(-1);
144
141
 
145
- // 1. Create CommentIbGib_V1 commit comment node in activeSpace
146
- const commentRes = await createCommentIbGib({
147
- text: message.trim(),
148
- saveInSpace: true,
149
- space: activeSpace,
150
- });
151
- const commentIbGib = commentRes.newIbGib as CommentIbGib_V1;
152
- const commentAddr = getIbGibAddr({ ibGib: commentIbGib });
153
- await metaspace.registerNewIbGib({ ibGib: commentIbGib, space: activeSpace });
154
-
155
- // 2. Evolve root folder item graph with staged items in activeSpace
142
+ // 1. Evolve root item graph with staged items and ingest co-located .ib.*.md comment files
156
143
  const repoName = path.basename(repoRoot) || 'root';
157
- const { newRootAddr } = await commitCmd_buildEvolvedRootItem({
144
+ let { newRootItem, newRootAddr, ingestedCommentCount } = await commitCmd_buildEvolvedRootItem({
158
145
  currentRootAddr,
159
146
  stagedPathsMap,
160
147
  space: activeSpace,
161
148
  metaspace,
162
149
  repoName,
150
+ worktreeDir: activeWorktreeDir,
163
151
  });
164
152
 
153
+ if ((!message || message.trim().length === 0) && ingestedCommentCount === 0) {
154
+ throw new Error(`Commit message is required (-m "message" or companion comment files) (E: 2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e)`);
155
+ }
156
+
157
+ // 2. If CLI message is provided, attach CommentIbGib to the root b2tfs item
158
+ let commentAddr: string | undefined;
159
+ if (message && message.trim().length > 0) {
160
+ const commentRes = await createCommentIbGib({
161
+ text: message.trim(),
162
+ saveInSpace: true,
163
+ space: activeSpace,
164
+ });
165
+ const commentIbGib = commentRes.newIbGib as CommentIbGib_V1;
166
+ commentAddr = getIbGibAddr({ ibGib: commentIbGib });
167
+ await metaspace.registerNewIbGib({ ibGib: commentIbGib, space: activeSpace });
168
+
169
+ const existingComments = newRootItem.rel8ns?.comment || [];
170
+ const evolveRootRes = await evolveB2tFSItemIbGib({
171
+ existingItem: newRootItem,
172
+ rel8nsToUpdate: {
173
+ comment: [...existingComments, commentAddr],
174
+ },
175
+ });
176
+ await persistTransformResult({ resTransform: evolveRootRes, space: activeSpace });
177
+ await metaspace.registerNewIbGib({ ibGib: evolveRootRes.newIbGib, space: activeSpace });
178
+
179
+ newRootItem = evolveRootRes.newIbGib as B2tFSItemIbGib_V1;
180
+ newRootAddr = getIbGibAddr({ ibGib: newRootItem });
181
+ }
182
+
165
183
  // 3. Advance B2tFSBranchIbGib_V1 node inside activeSpace
166
184
  const branchUuid = currentBranchIbGib?.data?.uuid || (await getUUID());
167
185
  const newBranchRes = await Factory_V1.firstGen({
@@ -173,10 +191,11 @@ export async function executeCommitCmd({
173
191
  uuid: branchUuid,
174
192
  classname: 'B2tFSBranchIbGib_V1',
175
193
  committedAt: new Date().toISOString(),
194
+ committedPaths,
176
195
  },
177
196
  rel8ns: {
178
197
  b2tfs_item: [newRootAddr],
179
- comment: [commentAddr],
198
+ ...(commentAddr ? { comment: [commentAddr] } : {}),
180
199
  },
181
200
  });
182
201
 
@@ -212,7 +231,8 @@ export async function executeCommitCmd({
212
231
 
213
232
  if (!quiet) {
214
233
  const shortGib = newBranchIbGib.gib ? newBranchIbGib.gib.slice(0, 7) : 'commit';
215
- console.log(`[${branchName} ${shortGib}] ${message.trim()} (I: 5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b)`);
234
+ const logMsg = message ? message.trim() : '(committed)';
235
+ console.log(`[${branchName} ${shortGib}] ${logMsg} (I: 5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b)`);
216
236
  console.log(` ${committedPaths.length} file(s) changed`);
217
237
  }
218
238
 
@@ -241,13 +261,16 @@ async function commitCmd_buildEvolvedRootItem({
241
261
  space,
242
262
  metaspace,
243
263
  repoName,
264
+ worktreeDir,
244
265
  }: {
245
266
  currentRootAddr?: string;
246
267
  stagedPathsMap: { [relPath: string]: B2tFSStagedItemRecord };
247
268
  space: any;
248
269
  metaspace: any;
249
270
  repoName: string;
250
- }): Promise<{ newRootItem: B2tFSItemIbGib_V1; newRootAddr: string }> {
271
+ worktreeDir: string;
272
+ }): Promise<{ newRootItem: B2tFSItemIbGib_V1; newRootAddr: string; ingestedCommentCount: number }> {
273
+ let ingestedCommentCount = 0;
251
274
  const itemsByRelPath = new Map<string, { itemAddr: string; fsType: 'file' | 'folder'; item?: B2tFSItemIbGib_V1 }>();
252
275
 
253
276
  if (currentRootAddr) {
@@ -276,6 +299,63 @@ async function commitCmd_buildEvolvedRootItem({
276
299
  });
277
300
  }
278
301
 
302
+ // 1. Ingest co-located .ib.<filename>.md comment files for staged files
303
+ for (const [relPath, itemRec] of itemsByRelPath.entries()) {
304
+ if (itemRec.fsType !== 'file') { continue; }
305
+
306
+ const dir = path.dirname(relPath);
307
+ const base = path.basename(relPath);
308
+ const commentDir = dir === '.' ? worktreeDir : path.join(worktreeDir, dir);
309
+ const commentFullPath = path.join(commentDir, `.ib.${base}.md`);
310
+
311
+ let commentText = '';
312
+ try {
313
+ commentText = await fs.readFile(commentFullPath, 'utf8');
314
+ } catch {
315
+ // No comment file for this file
316
+ continue;
317
+ }
318
+
319
+ if (commentText.trim().length > 0) {
320
+ const commentRes = await createCommentIbGib({
321
+ text: commentText.trim(),
322
+ saveInSpace: true,
323
+ space,
324
+ });
325
+ const commentIbGib = commentRes.newIbGib as CommentIbGib_V1;
326
+ const commentAddr = getIbGibAddr({ ibGib: commentIbGib });
327
+ await metaspace.registerNewIbGib({ ibGib: commentIbGib, space });
328
+ ingestedCommentCount++;
329
+
330
+ const currentItem = itemRec.item || (await getIbGibWithMetaspaceFallback({
331
+ addr: itemRec.itemAddr,
332
+ space,
333
+ metaspace,
334
+ }) as B2tFSItemIbGib_V1);
335
+
336
+ if (currentItem) {
337
+ const existingComments = currentItem.rel8ns?.comment || [];
338
+ const evolvedItemRes = await evolveB2tFSItemIbGib({
339
+ existingItem: currentItem,
340
+ rel8nsToUpdate: {
341
+ comment: [...existingComments, commentAddr],
342
+ },
343
+ });
344
+ await persistTransformResult({ resTransform: evolvedItemRes, space });
345
+ await metaspace.registerNewIbGib({ ibGib: evolvedItemRes.newIbGib, space });
346
+
347
+ itemRec.itemAddr = getIbGibAddr({ ibGib: evolvedItemRes.newIbGib });
348
+ itemRec.item = evolvedItemRes.newIbGib as B2tFSItemIbGib_V1;
349
+ }
350
+
351
+ try {
352
+ await fs.unlink(commentFullPath);
353
+ } catch {
354
+ // Delete failed / ignore
355
+ }
356
+ }
357
+ }
358
+
279
359
  // Ensure parent folder entries exist for all items
280
360
  const allRelPaths = Array.from(itemsByRelPath.keys());
281
361
  for (const relPath of allRelPaths) {
@@ -331,6 +411,46 @@ async function commitCmd_buildEvolvedRootItem({
331
411
  }
332
412
  }
333
413
 
414
+ // Check for folder-level .ib.<topic>.md files in this folder
415
+ const folderDiskPath = folderRelPath ? path.join(worktreeDir, folderRelPath) : worktreeDir;
416
+ const folderCommentAddrs: string[] = [];
417
+
418
+ try {
419
+ const dirents = await fs.readdir(folderDiskPath, { withFileTypes: true });
420
+ for (const dirent of dirents) {
421
+ if (dirent.isFile() && isCommentMetadataFilename(dirent.name)) {
422
+ const topicName = extractCommentTopicName(dirent.name);
423
+ const isCoLocatedFileComment = itemsByRelPath.has(folderRelPath ? `${folderRelPath}/${topicName}` : topicName);
424
+
425
+ if (!isCoLocatedFileComment) {
426
+ const fullCommentPath = path.join(folderDiskPath, dirent.name);
427
+ const commentContent = await fs.readFile(fullCommentPath, 'utf8');
428
+
429
+ if (commentContent.trim().length > 0) {
430
+ const commentRes = await createCommentIbGib({
431
+ text: commentContent.trim(),
432
+ saveInSpace: true,
433
+ space,
434
+ });
435
+ const commentIbGib = commentRes.newIbGib as CommentIbGib_V1;
436
+ const cAddr = getIbGibAddr({ ibGib: commentIbGib });
437
+ await metaspace.registerNewIbGib({ ibGib: commentIbGib, space });
438
+ folderCommentAddrs.push(cAddr);
439
+ ingestedCommentCount++;
440
+ }
441
+
442
+ try {
443
+ await fs.unlink(fullCommentPath);
444
+ } catch {
445
+ // ignore delete error
446
+ }
447
+ }
448
+ }
449
+ }
450
+ } catch {
451
+ // Folder may not exist on disk
452
+ }
453
+
334
454
  const folderName = folderRelPath ? path.basename(folderRelPath) : repoName;
335
455
  const nameHash = crypto.createHash('sha256').update(folderName).digest('hex');
336
456
 
@@ -341,13 +461,17 @@ async function commitCmd_buildEvolvedRootItem({
341
461
  if (existingFolderRec && existingFolderRec.item) {
342
462
  const oldChildAddrs = existingFolderRec.item.rel8ns?.[B2TFS_CHILD_ITEM_REL8N_NAME] || [];
343
463
  const toRemoveChildAddrs = oldChildAddrs.filter(a => !childAddrs.includes(a));
464
+ const existingComments = existingFolderRec.item.rel8ns?.comment || [];
344
465
 
345
466
  folderRes = await evolveB2tFSItemIbGib({
346
467
  existingItem: existingFolderRec.item,
347
468
  dataToUpdate: {
348
469
  nameHash,
349
470
  },
350
- rel8nsToUpdate: childAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: childAddrs } : undefined,
471
+ rel8nsToUpdate: {
472
+ ...(childAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: childAddrs } : {}),
473
+ ...(folderCommentAddrs.length > 0 ? { comment: [...existingComments, ...folderCommentAddrs] } : {}),
474
+ },
351
475
  rel8nsToRemove: toRemoveChildAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: toRemoveChildAddrs } : undefined,
352
476
  });
353
477
  } else {
@@ -357,7 +481,10 @@ async function commitCmd_buildEvolvedRootItem({
357
481
  data: {
358
482
  nameHash,
359
483
  },
360
- rel8ns: childAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: childAddrs } : undefined,
484
+ rel8ns: {
485
+ ...(childAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: childAddrs } : {}),
486
+ ...(folderCommentAddrs.length > 0 ? { comment: folderCommentAddrs } : {}),
487
+ },
361
488
  });
362
489
  }
363
490
 
@@ -374,7 +501,7 @@ async function commitCmd_buildEvolvedRootItem({
374
501
  }
375
502
  }
376
503
 
377
- return { newRootItem: rootFolderItem!, newRootAddr: rootFolderAddr };
504
+ return { newRootItem: rootFolderItem!, newRootAddr: rootFolderAddr, ingestedCommentCount };
378
505
  }
379
506
 
380
507
  function commitCmd_getParentRelPath(relPath: string): string {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @module commit-log-cmd.respec
3
- *
3
+ *
4
4
  * Unit tests for `vcs commit` and `vcs log` command handlers.
5
5
  */
6
6
 
@@ -8,9 +8,11 @@ import path from 'node:path';
8
8
  import fs from 'node:fs/promises';
9
9
 
10
10
  import {
11
- respecfully, ifWe, iReckon
11
+ respecfully, ifWe, ifWeMight, iReckon
12
12
  } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs';
13
13
 
14
+ import { getDependencyGraph } from '@ibgib/core-gib/dist/common/other/graph-helper.mjs';
15
+ import { getFromSpace } from '@ibgib/core-gib/dist/witness/space/space-helper.mjs';
14
16
  import { executeInitCmd } from './init-cmd.mjs';
15
17
  import { executeAddCmd } from './add-cmd.mjs';
16
18
  import { executeStatusCmd } from './status-cmd.mjs';
@@ -18,8 +20,12 @@ import { executeCommitCmd } from './commit-cmd.mjs';
18
20
  import { executeLogCmd } from './log-cmd.mjs';
19
21
  import { executeBranchCmd } from './branch-cmd.mjs';
20
22
  import { executeCheckoutCmd } from './checkout-cmd.mjs';
23
+ import { executeMergeCmd } from './merge-cmd.mjs';
24
+ import { executeGenerateCommentCmd } from './generate-comment-cmd.mjs';
21
25
  import { bootstrapVcsMetaspace } from '../../bootstrap/node-bootstrap.mjs';
22
26
  import { getB2tFSIndexIbGib } from '../../engine/index/index-helper.mjs';
27
+ import { getOrCreateBranchSpace } from '../../engine/branch/branch-space-helper.mjs';
28
+ import { getIbGibWithMetaspaceFallback } from '../../engine/item/item-helper.mjs';
23
29
 
24
30
  const lc = `[apps/vcs-gib/src/cli/commands/commit-log-cmd.respec.node.mts]`;
25
31
 
@@ -267,8 +273,215 @@ await respecfully(lc, `vcs commit and vcs log Command Handlers`, async () => {
267
273
  });
268
274
 
269
275
  iReckon(lc, logRes.branchName).willEqual('feature-log');
270
- iReckon(lc, logRes.commits.length).willEqual(1);
276
+ iReckon(lc, logRes.commits.length).willEqual(2);
271
277
  iReckon(lc, logRes.commits[0].message).willEqual('Feature branch commit');
278
+ iReckon(lc, logRes.commits[1].message).willEqual('Main branch commit');
279
+ } finally {
280
+ await fs.rm(testDir, { recursive: true, force: true });
281
+ }
282
+ });
283
+
284
+ await ifWe(lc, `generates companion comment files, displays in status, ingests on commit, and projects in file log`, async () => {
285
+ const testDir = path.join(process.cwd(), '.test_commit_log_companion');
286
+ const vcsFolderName = '.vcsgib_test_commit_companion';
287
+ try {
288
+ await executeInitCmd({
289
+ targetDir: testDir,
290
+ vcsFolderName,
291
+ quiet: true,
292
+ });
293
+
294
+ const filePath = path.join(testDir, 'app.ts');
295
+ await fs.writeFile(filePath, 'console.log("hello world");', 'utf8');
296
+
297
+ // 1. Test generate-comment
298
+ const genRes = await executeGenerateCommentCmd({
299
+ targetDir: testDir,
300
+ vcsFolderName,
301
+ paths: ['app.ts'],
302
+ quiet: true,
303
+ });
304
+
305
+ iReckon(lc, genRes.createdFiles.length).willEqual(1);
306
+ iReckon(lc, genRes.createdFiles[0]).includes('.ib.app.ts.md');
307
+
308
+ const companionPath = path.join(testDir, '.ib.app.ts.md');
309
+ let companionExists = false;
310
+ try {
311
+ await fs.access(companionPath);
312
+ companionExists = true;
313
+ } catch { }
314
+ iReckon(lc, companionExists).isGonnaBeTrue();
315
+
316
+ // Write custom notes into companion file
317
+ await fs.writeFile(companionPath, 'feat: initial implementation of app.ts\n* added console log', 'utf8');
318
+
319
+ // Write folder-level notes
320
+ const folderCompanionPath = path.join(testDir, '.ib.architecture.md');
321
+ await fs.writeFile(folderCompanionPath, 'Arch overview: client-side entrypoint', 'utf8');
322
+
323
+ // Stage app.ts
324
+ await executeAddCmd({
325
+ paths: ['app.ts'],
326
+ targetDir: testDir,
327
+ vcsFolderName,
328
+ quiet: true,
329
+ });
330
+
331
+ // 2. Test status with comments
332
+ const statusRes = await executeStatusCmd({
333
+ targetDir: testDir,
334
+ vcsFolderName,
335
+ comments: true,
336
+ quiet: true,
337
+ });
338
+
339
+ iReckon(lc, statusRes.pendingComments.length).willEqual(2);
340
+ const fileComment = statusRes.pendingComments.find(c => c.targetType === 'file');
341
+ iReckon(lc, Boolean(fileComment)).isGonnaBeTrue();
342
+ iReckon(lc, fileComment?.content).includes('initial implementation of app.ts');
343
+
344
+ // 3. Commit staged files and notes
345
+ const commitRes = await executeCommitCmd({
346
+ message: 'Initial root commit',
347
+ targetDir: testDir,
348
+ vcsFolderName,
349
+ quiet: true,
350
+ });
351
+
352
+ iReckon(lc, commitRes.committedPaths).includes('app.ts');
353
+
354
+ // Verify companion files were consumed and removed
355
+ let companionStillExists = false;
356
+ try {
357
+ await fs.access(companionPath);
358
+ companionStillExists = true;
359
+ } catch { }
360
+ iReckon(lc, companionStillExists).isGonnaBeFalse();
361
+
362
+ let folderCompanionStillExists = false;
363
+ try {
364
+ await fs.access(folderCompanionPath);
365
+ folderCompanionStillExists = true;
366
+ } catch { }
367
+ iReckon(lc, folderCompanionStillExists).isGonnaBeFalse();
368
+
369
+ // 4. Test file-specific log (vcs log app.ts)
370
+ const fileLogRes = await executeLogCmd({
371
+ targetDir: testDir,
372
+ vcsFolderName,
373
+ filePath: 'app.ts',
374
+ quiet: true,
375
+ });
376
+
377
+ iReckon(lc, fileLogRes.commits.length >= 1).isGonnaBeTrue();
378
+ iReckon(lc, fileLogRes.commits[0].message).includes('initial implementation of app.ts');
379
+
380
+ // 5. Test root log with allComments
381
+ const rootLogRes = await executeLogCmd({
382
+ targetDir: testDir,
383
+ vcsFolderName,
384
+ allComments: true,
385
+ quiet: true,
386
+ });
387
+
388
+ iReckon(lc, rootLogRes.commits.length).willEqual(1);
389
+ iReckon(lc, rootLogRes.commits[0].message).willEqual('Initial root commit');
390
+ iReckon(lc, rootLogRes.commits[0].committedPaths).includes('app.ts');
391
+ } finally {
392
+ await fs.rm(testDir, { recursive: true, force: true });
393
+ }
394
+ });
395
+
396
+ await ifWe(lc, `preserves and unions file comments across 3-way branch merges`, async () => {
397
+ const testDir = path.join(process.cwd(), '.test_commit_log_merge_comments');
398
+ const vcsFolderName = '.vcsgib_test_commit_merge_comments';
399
+ try {
400
+ await executeInitCmd({
401
+ targetDir: testDir,
402
+ vcsFolderName,
403
+ quiet: true,
404
+ });
405
+
406
+ // Base commit on main
407
+ const appFile = path.join(testDir, 'app.ts');
408
+ await fs.writeFile(appFile, 'const a = 1;\nconst b = 2;\n', 'utf8');
409
+ await executeAddCmd({ paths: ['app.ts'], targetDir: testDir, vcsFolderName, quiet: true });
410
+ await executeCommitCmd({ message: 'Base commit on main', targetDir: testDir, vcsFolderName, quiet: true });
411
+
412
+ // Create feature branch
413
+ await executeBranchCmd({ branchName: 'feature-comments', targetDir: testDir, vcsFolderName, quiet: true });
414
+ const checkoutRes = await executeCheckoutCmd({ branchName: 'feature-comments', targetDir: testDir, vcsFolderName, quiet: true });
415
+
416
+ // On feature branch: modify app.ts and attach companion note
417
+ const featureAppFile = path.join(checkoutRes.worktreePath, 'app.ts');
418
+ await fs.writeFile(featureAppFile, 'const a = 1;\nconst b = 200;\n', 'utf8');
419
+ const featureNoteFile = path.join(checkoutRes.worktreePath, '.ib.app.ts.md');
420
+ await fs.writeFile(featureNoteFile, 'feat(feature-comments): updated b to 200', 'utf8');
421
+
422
+ await executeAddCmd({ paths: ['app.ts'], targetDir: checkoutRes.worktreePath, vcsFolderName, quiet: true });
423
+ await executeCommitCmd({ message: 'Feature branch commit', targetDir: checkoutRes.worktreePath, vcsFolderName, quiet: true });
424
+
425
+ // Switch back to main branch
426
+ await executeCheckoutCmd({ branchName: 'main', targetDir: testDir, vcsFolderName, quiet: true });
427
+
428
+ // On main branch: modify app.ts and attach companion note
429
+ await fs.writeFile(appFile, 'const a = 100;\nconst b = 2;\n', 'utf8');
430
+ const mainNoteFile = path.join(testDir, '.ib.app.ts.md');
431
+ await fs.writeFile(mainNoteFile, 'feat(main): updated a to 100', 'utf8');
432
+
433
+ await executeAddCmd({ paths: ['app.ts'], targetDir: testDir, vcsFolderName, quiet: true });
434
+ await executeCommitCmd({ message: 'Main branch update', targetDir: testDir, vcsFolderName, quiet: true });
435
+
436
+ // Merge feature-comments into main
437
+ await executeMergeCmd({ incomingBranchName: 'feature-comments', targetDir: testDir, vcsFolderName, quiet: true });
438
+
439
+ // Check file log on main: both comments should be present!
440
+ const mergedFileLog = await executeLogCmd({
441
+ targetDir: testDir,
442
+ vcsFolderName,
443
+ filePath: 'app.ts',
444
+ quiet: true,
445
+ });
446
+
447
+ const allMessages = mergedFileLog.commits.map(c => c.message).join(' -- ');
448
+ iReckon(lc, allMessages).includes('updated b to 200');
449
+ iReckon(lc, allMessages).includes('updated a to 100');
450
+
451
+ // Verify getDependencyGraph works 100% within main branch space alone (no metaspace fallback)
452
+ const { metaspace, vcsPath } = await bootstrapVcsMetaspace({
453
+ repoPath: testDir,
454
+ vcsFolderName,
455
+ });
456
+ const { space: mainSpace } = await getOrCreateBranchSpace({
457
+ metaspace,
458
+ vcsPath,
459
+ branchName: 'main',
460
+ });
461
+ const indexIbGib = await getB2tFSIndexIbGib({ space: metaspace.zeroSpace, vcsPath });
462
+ const mainBranchAddr = indexIbGib?.rel8ns?.branch?.at(-1) || indexIbGib?.rel8ns?.branch_tjp?.at(-1);
463
+ const mainBranch = await getIbGibWithMetaspaceFallback({ addr: mainBranchAddr!, space: mainSpace, metaspace });
464
+ const mainRootAddr = mainBranch?.rel8ns?.b2tfs_item?.at(-1);
465
+ const mainRoot = await getIbGibWithMetaspaceFallback({ addr: mainRootAddr!, space: mainSpace, metaspace });
466
+ let appItem: any;
467
+ for (const childAddr of mainRoot?.rel8ns?.b2tfs_child || []) {
468
+ const child = await getIbGibWithMetaspaceFallback({ addr: childAddr, space: mainSpace, metaspace });
469
+ if (child?.data?.name === 'app.ts') {
470
+ appItem = child;
471
+ break;
472
+ }
473
+ }
474
+ iReckon(lc, appItem).isGonnaBeTruthy();
475
+
476
+ // Ensure getDependencyGraph runs strictly within mainSpace with NO metaspace fallback
477
+ const depGraph = await getDependencyGraph({
478
+ ibGib: appItem,
479
+ space: mainSpace,
480
+ });
481
+
482
+ const graphValues = Object.values(depGraph);
483
+ const foundCommentB = graphValues.some(x => (x.data as any)?.text === 'feat(feature-comments): updated b to 200');
484
+ iReckon(lc, foundCommentB).isGonnaBeTruthy();
272
485
  } finally {
273
486
  await fs.rm(testDir, { recursive: true, force: true });
274
487
  }