@ibgib/vcs-gib 0.0.21 → 0.0.26

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 (93) hide show
  1. package/CHANGELOG.md +19 -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/bootstrap/node-bootstrap.mjs +1 -1
  6. package/dist/cli/cli-arg-parser.d.mts +1 -0
  7. package/dist/cli/cli-arg-parser.d.mts.map +1 -1
  8. package/dist/cli/cli-arg-parser.mjs +5 -1
  9. package/dist/cli/cli-arg-parser.mjs.map +1 -1
  10. package/dist/cli/cli-constants.d.mts +6 -0
  11. package/dist/cli/cli-constants.d.mts.map +1 -1
  12. package/dist/cli/cli-constants.mjs +12 -0
  13. package/dist/cli/cli-constants.mjs.map +1 -1
  14. package/dist/cli/commands/add-cmd.d.mts.map +1 -1
  15. package/dist/cli/commands/add-cmd.mjs +5 -2
  16. package/dist/cli/commands/add-cmd.mjs.map +1 -1
  17. package/dist/cli/commands/commit-cmd.d.mts +5 -4
  18. package/dist/cli/commands/commit-cmd.d.mts.map +1 -1
  19. package/dist/cli/commands/commit-cmd.mjs +149 -33
  20. package/dist/cli/commands/commit-cmd.mjs.map +1 -1
  21. package/dist/cli/commands/generate-comment-cmd.d.mts +19 -0
  22. package/dist/cli/commands/generate-comment-cmd.d.mts.map +1 -0
  23. package/dist/cli/commands/generate-comment-cmd.mjs +140 -0
  24. package/dist/cli/commands/generate-comment-cmd.mjs.map +1 -0
  25. package/dist/cli/commands/log-cmd.d.mts +12 -3
  26. package/dist/cli/commands/log-cmd.d.mts.map +1 -1
  27. package/dist/cli/commands/log-cmd.mjs +236 -38
  28. package/dist/cli/commands/log-cmd.mjs.map +1 -1
  29. package/dist/cli/commands/merge-cmd.d.mts +21 -8
  30. package/dist/cli/commands/merge-cmd.d.mts.map +1 -1
  31. package/dist/cli/commands/merge-cmd.mjs +138 -70
  32. package/dist/cli/commands/merge-cmd.mjs.map +1 -1
  33. package/dist/cli/commands/status-cmd.d.mts +5 -6
  34. package/dist/cli/commands/status-cmd.d.mts.map +1 -1
  35. package/dist/cli/commands/status-cmd.mjs +50 -205
  36. package/dist/cli/commands/status-cmd.mjs.map +1 -1
  37. package/dist/cli/vcs-cli-main.d.mts.map +1 -1
  38. package/dist/cli/vcs-cli-main.mjs +27 -6
  39. package/dist/cli/vcs-cli-main.mjs.map +1 -1
  40. package/dist/engine/index/index-helper.d.mts.map +1 -1
  41. package/dist/engine/index/index-helper.mjs +3 -0
  42. package/dist/engine/index/index-helper.mjs.map +1 -1
  43. package/dist/engine/item/item-helper.d.mts +31 -1
  44. package/dist/engine/item/item-helper.d.mts.map +1 -1
  45. package/dist/engine/item/item-helper.mjs +44 -2
  46. package/dist/engine/item/item-helper.mjs.map +1 -1
  47. package/dist/engine/merge/b2tfs-3way-graft-strategy.d.mts +74 -0
  48. package/dist/engine/merge/b2tfs-3way-graft-strategy.d.mts.map +1 -0
  49. package/dist/engine/merge/{b2tfs-3way-merge-strategy.mjs → b2tfs-3way-graft-strategy.mjs} +133 -304
  50. package/dist/engine/merge/b2tfs-3way-graft-strategy.mjs.map +1 -0
  51. package/dist/engine/merge/{b2tfs-3way-merge-types.d.mts → b2tfs-3way-graft-types.d.mts} +2 -9
  52. package/dist/engine/merge/b2tfs-3way-graft-types.d.mts.map +1 -0
  53. package/dist/engine/merge/b2tfs-3way-graft-types.mjs +2 -0
  54. package/dist/engine/merge/b2tfs-3way-graft-types.mjs.map +1 -0
  55. package/dist/engine/status/status-helper.d.mts +89 -0
  56. package/dist/engine/status/status-helper.d.mts.map +1 -0
  57. package/dist/engine/status/status-helper.mjs +285 -0
  58. package/dist/engine/status/status-helper.mjs.map +1 -0
  59. package/dist/test/test-helpers.d.mts +51 -0
  60. package/dist/test/test-helpers.d.mts.map +1 -0
  61. package/dist/test/test-helpers.mjs +135 -0
  62. package/dist/test/test-helpers.mjs.map +1 -0
  63. package/package.json +4 -4
  64. package/src/AUTO-GENERATED-version.mts +1 -1
  65. package/src/bootstrap/node-bootstrap.mts +1 -1
  66. package/src/cli/cli-arg-parser.mts +5 -0
  67. package/src/cli/cli-constants.mts +13 -0
  68. package/src/cli/commands/add-cmd.mts +5 -2
  69. package/src/cli/commands/branch-checkout-cmd.respec.log +8 -101
  70. package/src/cli/commands/branch-checkout-cmd.respec.mts +11 -10
  71. package/src/cli/commands/commit-cmd.mts +165 -42
  72. package/src/cli/commands/commit-log-cmd.respec.mts +216 -3
  73. package/src/cli/commands/generate-comment-cmd.mts +171 -0
  74. package/src/cli/commands/log-cmd.mts +288 -41
  75. package/src/cli/commands/merge-cmd.mts +154 -76
  76. package/src/cli/commands/status-cmd.mts +57 -256
  77. package/src/cli/vcs-cli-main.mts +28 -7
  78. package/src/engine/index/index-helper.mts +3 -0
  79. package/src/engine/item/item-helper.mts +76 -3
  80. package/src/engine/merge/{b2tfs-3way-merge-strategy.mts → b2tfs-3way-graft-strategy.mts} +152 -374
  81. package/src/engine/merge/{b2tfs-3way-merge-strategy.respec.mts → b2tfs-3way-graft-strategy.respec.mts} +27 -122
  82. package/src/engine/merge/{b2tfs-3way-merge-types.mts → b2tfs-3way-graft-types.mts} +34 -42
  83. package/src/engine/merge/b2tfs-sync-comments.respec.mts +399 -0
  84. package/src/engine/status/status-helper.mts +380 -0
  85. package/src/test/test-helpers.mts +168 -0
  86. package/tsconfig.test.tsbuildinfo +1 -1
  87. package/tsconfig.tsbuildinfo +1 -1
  88. package/dist/engine/merge/b2tfs-3way-merge-strategy.d.mts +0 -48
  89. package/dist/engine/merge/b2tfs-3way-merge-strategy.d.mts.map +0 -1
  90. package/dist/engine/merge/b2tfs-3way-merge-strategy.mjs.map +0 -1
  91. package/dist/engine/merge/b2tfs-3way-merge-types.d.mts.map +0 -1
  92. package/dist/engine/merge/b2tfs-3way-merge-types.mjs +0 -2
  93. package/dist/engine/merge/b2tfs-3way-merge-types.mjs.map +0 -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
28
  import { getB2tFSBranchSpaceName } 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,11 +79,6 @@ 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) {
@@ -90,6 +86,7 @@ export async function executeCommitCmd({
90
86
  }
91
87
 
92
88
  const branchName = indexIbGib.data?.activeBranch || 'main';
89
+ const activeWorktreeDir = branchName === 'main' ? repoRoot : path.join(repoRoot, 'b', branchName);
93
90
 
94
91
  // Target active branch space b2s_<activeBranch>
95
92
  const { space: activeSpace } = await getOrCreateBranchSpace({
@@ -98,24 +95,22 @@ export async function executeCommitCmd({
98
95
  branchName,
99
96
  });
100
97
 
101
- // Validate staged items
102
- const stagedPathsMap = indexIbGib.data?.stagedPaths || {};
103
- const stagedAddrs = indexIbGib.data?.staged_item || [];
98
+ // Verify there are staged items
99
+ const stagedPathsMap: { [relPath: string]: B2tFSStagedItemRecord } = { ...(indexIbGib.data?.stagedPaths || {}) };
104
100
  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)`);
101
+ if (committedPaths.length === 0) {
102
+ throw new Error(`Nothing to commit (no staged items found in index) (E: 4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a)`);
108
103
  }
109
104
 
110
- // Fetch active branch node from activeSpace
105
+ // Find active branch node
106
+ let currentBranchIbGib: B2tFSBranchIbGib_V1 | undefined;
107
+ let currentBranchAddr: string | undefined;
108
+
111
109
  const branchAddrs = [
112
110
  ...(indexIbGib.rel8ns?.branch || []),
113
111
  ...(indexIbGib.rel8ns?.branch_tjp || []),
114
112
  ];
115
113
 
116
- let currentBranchIbGib: B2tFSBranchIbGib_V1 | undefined;
117
- let currentBranchAddr: string | undefined;
118
-
119
114
  if (branchAddrs.length > 0) {
120
115
  let branchIbGibs: B2tFSBranchIbGib_V1[] = [];
121
116
  for (const bAddr of branchAddrs) {
@@ -133,35 +128,54 @@ export async function executeCommitCmd({
133
128
  (b) => (b as B2tFSBranchIbGib_V1)?.data?.name === branchName,
134
129
  ) as B2tFSBranchIbGib_V1 || (branchIbGibs.at(-1) as B2tFSBranchIbGib_V1);
135
130
 
136
-
137
131
  if (currentBranchIbGib) {
138
132
  currentBranchAddr = getIbGibAddr({ ibGib: currentBranchIbGib });
139
133
  }
140
134
  }
141
135
 
142
- const currentRootAddr = currentBranchIbGib?.rel8ns?.b2tfs_item?.at(-1);
143
-
144
-
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 });
136
+ const currentRootAddr = currentBranchIbGib?.rel8ns?.b2tFS_item?.at(-1) || currentBranchIbGib?.rel8ns?.b2tfs_item?.at(-1);
154
137
 
155
- // 2. Evolve root folder item graph with staged items in activeSpace
138
+ // 1. Evolve root item graph with staged items and ingest co-located .ib.*.md comment files
156
139
  const repoName = path.basename(repoRoot) || 'root';
157
- const { newRootAddr } = await commitCmd_buildEvolvedRootItem({
140
+ let { newRootItem, newRootAddr, ingestedCommentCount } = await commitCmd_buildEvolvedRootItem({
158
141
  currentRootAddr,
159
142
  stagedPathsMap,
160
143
  space: activeSpace,
161
144
  metaspace,
162
145
  repoName,
146
+ worktreeDir: activeWorktreeDir,
163
147
  });
164
148
 
149
+ if ((!message || message.trim().length === 0) && ingestedCommentCount === 0) {
150
+ throw new Error(`Commit message is required (-m "message" or companion comment files) (E: 2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e)`);
151
+ }
152
+
153
+ // 2. If CLI message is provided, attach CommentIbGib to the root b2tfs item
154
+ let commentAddr: string | undefined;
155
+ if (message && message.trim().length > 0) {
156
+ const commentRes = await createCommentIbGib({
157
+ text: message.trim(),
158
+ saveInSpace: true,
159
+ space: activeSpace,
160
+ });
161
+ const commentIbGib = commentRes.newIbGib as CommentIbGib_V1;
162
+ commentAddr = getIbGibAddr({ ibGib: commentIbGib });
163
+ await metaspace.registerNewIbGib({ ibGib: commentIbGib, space: activeSpace });
164
+
165
+ const existingComments = newRootItem.rel8ns?.comment || [];
166
+ const evolveRootRes = await evolveB2tFSItemIbGib({
167
+ existingItem: newRootItem,
168
+ rel8nsToUpdate: {
169
+ comment: [...existingComments, commentAddr],
170
+ },
171
+ });
172
+ await persistTransformResult({ resTransform: evolveRootRes, space: activeSpace });
173
+ await metaspace.registerNewIbGib({ ibGib: evolveRootRes.newIbGib, space: activeSpace });
174
+
175
+ newRootItem = evolveRootRes.newIbGib as B2tFSItemIbGib_V1;
176
+ newRootAddr = getIbGibAddr({ ibGib: newRootItem });
177
+ }
178
+
165
179
  // 3. Advance B2tFSBranchIbGib_V1 node inside activeSpace
166
180
  const branchUuid = currentBranchIbGib?.data?.uuid || (await getUUID());
167
181
  const newBranchRes = await Factory_V1.firstGen({
@@ -173,10 +187,11 @@ export async function executeCommitCmd({
173
187
  uuid: branchUuid,
174
188
  classname: 'B2tFSBranchIbGib_V1',
175
189
  committedAt: new Date().toISOString(),
190
+ committedPaths,
176
191
  },
177
192
  rel8ns: {
178
193
  b2tfs_item: [newRootAddr],
179
- comment: [commentAddr],
194
+ ...(commentAddr ? { comment: [commentAddr] } : {}),
180
195
  },
181
196
  });
182
197
 
@@ -212,7 +227,8 @@ export async function executeCommitCmd({
212
227
 
213
228
  if (!quiet) {
214
229
  const shortGib = newBranchIbGib.gib ? newBranchIbGib.gib.slice(0, 7) : 'commit';
215
- console.log(`[${branchName} ${shortGib}] ${message.trim()} (I: 5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b)`);
230
+ const logMsg = message ? message.trim() : '(committed)';
231
+ console.log(`[${branchName} ${shortGib}] ${logMsg} (I: 5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b)`);
216
232
  console.log(` ${committedPaths.length} file(s) changed`);
217
233
  }
218
234
 
@@ -241,13 +257,16 @@ async function commitCmd_buildEvolvedRootItem({
241
257
  space,
242
258
  metaspace,
243
259
  repoName,
260
+ worktreeDir,
244
261
  }: {
245
262
  currentRootAddr?: string;
246
263
  stagedPathsMap: { [relPath: string]: B2tFSStagedItemRecord };
247
264
  space: any;
248
265
  metaspace: any;
249
266
  repoName: string;
250
- }): Promise<{ newRootItem: B2tFSItemIbGib_V1; newRootAddr: string }> {
267
+ worktreeDir: string;
268
+ }): Promise<{ newRootItem: B2tFSItemIbGib_V1; newRootAddr: string; ingestedCommentCount: number }> {
269
+ let ingestedCommentCount = 0;
251
270
  const itemsByRelPath = new Map<string, { itemAddr: string; fsType: 'file' | 'folder'; item?: B2tFSItemIbGib_V1 }>();
252
271
 
253
272
  if (currentRootAddr) {
@@ -276,6 +295,63 @@ async function commitCmd_buildEvolvedRootItem({
276
295
  });
277
296
  }
278
297
 
298
+ // 1. Ingest co-located .ib.<filename>.md comment files for staged files
299
+ for (const [relPath, itemRec] of itemsByRelPath.entries()) {
300
+ if (itemRec.fsType !== 'file') { continue; }
301
+
302
+ const dir = path.dirname(relPath);
303
+ const base = path.basename(relPath);
304
+ const commentDir = dir === '.' ? worktreeDir : path.join(worktreeDir, dir);
305
+ const commentFullPath = path.join(commentDir, `.ib.${base}.md`);
306
+
307
+ let commentText = '';
308
+ try {
309
+ commentText = await fs.readFile(commentFullPath, 'utf8');
310
+ } catch {
311
+ // No comment file for this file
312
+ continue;
313
+ }
314
+
315
+ if (commentText.trim().length > 0) {
316
+ const commentRes = await createCommentIbGib({
317
+ text: commentText.trim(),
318
+ saveInSpace: true,
319
+ space,
320
+ });
321
+ const commentIbGib = commentRes.newIbGib as CommentIbGib_V1;
322
+ const commentAddr = getIbGibAddr({ ibGib: commentIbGib });
323
+ await metaspace.registerNewIbGib({ ibGib: commentIbGib, space });
324
+ ingestedCommentCount++;
325
+
326
+ const currentItem = itemRec.item || (await getIbGibWithMetaspaceFallback({
327
+ addr: itemRec.itemAddr,
328
+ space,
329
+ metaspace,
330
+ }) as B2tFSItemIbGib_V1);
331
+
332
+ if (currentItem) {
333
+ const existingComments = currentItem.rel8ns?.comment || [];
334
+ const evolvedItemRes = await evolveB2tFSItemIbGib({
335
+ existingItem: currentItem,
336
+ rel8nsToUpdate: {
337
+ comment: [...existingComments, commentAddr],
338
+ },
339
+ });
340
+ await persistTransformResult({ resTransform: evolvedItemRes, space });
341
+ await metaspace.registerNewIbGib({ ibGib: evolvedItemRes.newIbGib, space });
342
+
343
+ itemRec.itemAddr = getIbGibAddr({ ibGib: evolvedItemRes.newIbGib });
344
+ itemRec.item = evolvedItemRes.newIbGib as B2tFSItemIbGib_V1;
345
+ }
346
+
347
+ try {
348
+ await fs.unlink(commentFullPath);
349
+ } catch {
350
+ // Delete failed / ignore
351
+ }
352
+ }
353
+ }
354
+
279
355
  // Ensure parent folder entries exist for all items
280
356
  const allRelPaths = Array.from(itemsByRelPath.keys());
281
357
  for (const relPath of allRelPaths) {
@@ -331,6 +407,46 @@ async function commitCmd_buildEvolvedRootItem({
331
407
  }
332
408
  }
333
409
 
410
+ // Check for folder-level .ib.<topic>.md files in this folder
411
+ const folderDiskPath = folderRelPath ? path.join(worktreeDir, folderRelPath) : worktreeDir;
412
+ const folderCommentAddrs: string[] = [];
413
+
414
+ try {
415
+ const dirents = await fs.readdir(folderDiskPath, { withFileTypes: true });
416
+ for (const dirent of dirents) {
417
+ if (dirent.isFile() && isCommentMetadataFilename(dirent.name)) {
418
+ const topicName = extractCommentTopicName(dirent.name);
419
+ const isCoLocatedFileComment = itemsByRelPath.has(folderRelPath ? `${folderRelPath}/${topicName}` : topicName);
420
+
421
+ if (!isCoLocatedFileComment) {
422
+ const fullCommentPath = path.join(folderDiskPath, dirent.name);
423
+ const commentContent = await fs.readFile(fullCommentPath, 'utf8');
424
+
425
+ if (commentContent.trim().length > 0) {
426
+ const commentRes = await createCommentIbGib({
427
+ text: commentContent.trim(),
428
+ saveInSpace: true,
429
+ space,
430
+ });
431
+ const commentIbGib = commentRes.newIbGib as CommentIbGib_V1;
432
+ const cAddr = getIbGibAddr({ ibGib: commentIbGib });
433
+ await metaspace.registerNewIbGib({ ibGib: commentIbGib, space });
434
+ folderCommentAddrs.push(cAddr);
435
+ ingestedCommentCount++;
436
+ }
437
+
438
+ try {
439
+ await fs.unlink(fullCommentPath);
440
+ } catch {
441
+ // ignore delete error
442
+ }
443
+ }
444
+ }
445
+ }
446
+ } catch {
447
+ // Folder may not exist on disk
448
+ }
449
+
334
450
  const folderName = folderRelPath ? path.basename(folderRelPath) : repoName;
335
451
  const nameHash = crypto.createHash('sha256').update(folderName).digest('hex');
336
452
 
@@ -341,13 +457,17 @@ async function commitCmd_buildEvolvedRootItem({
341
457
  if (existingFolderRec && existingFolderRec.item) {
342
458
  const oldChildAddrs = existingFolderRec.item.rel8ns?.[B2TFS_CHILD_ITEM_REL8N_NAME] || [];
343
459
  const toRemoveChildAddrs = oldChildAddrs.filter(a => !childAddrs.includes(a));
460
+ const existingComments = existingFolderRec.item.rel8ns?.comment || [];
344
461
 
345
462
  folderRes = await evolveB2tFSItemIbGib({
346
463
  existingItem: existingFolderRec.item,
347
464
  dataToUpdate: {
348
465
  nameHash,
349
466
  },
350
- rel8nsToUpdate: childAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: childAddrs } : undefined,
467
+ rel8nsToUpdate: {
468
+ ...(childAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: childAddrs } : {}),
469
+ ...(folderCommentAddrs.length > 0 ? { comment: [...existingComments, ...folderCommentAddrs] } : {}),
470
+ },
351
471
  rel8nsToRemove: toRemoveChildAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: toRemoveChildAddrs } : undefined,
352
472
  });
353
473
  } else {
@@ -357,7 +477,10 @@ async function commitCmd_buildEvolvedRootItem({
357
477
  data: {
358
478
  nameHash,
359
479
  },
360
- rel8ns: childAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: childAddrs } : undefined,
480
+ rel8ns: {
481
+ ...(childAddrs.length > 0 ? { [B2TFS_CHILD_ITEM_REL8N_NAME]: childAddrs } : {}),
482
+ ...(folderCommentAddrs.length > 0 ? { comment: folderCommentAddrs } : {}),
483
+ },
361
484
  });
362
485
  }
363
486
 
@@ -374,7 +497,7 @@ async function commitCmd_buildEvolvedRootItem({
374
497
  }
375
498
  }
376
499
 
377
- return { newRootItem: rootFolderItem!, newRootAddr: rootFolderAddr };
500
+ return { newRootItem: rootFolderItem!, newRootAddr: rootFolderAddr, ingestedCommentCount };
378
501
  }
379
502
 
380
503
  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
  }