@descix/cli 1.0.14 → 1.0.15

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.
@@ -744,6 +744,21 @@ export async function runCorpusSync(apiClient, options) {
744
744
  console.log(chalk.cyan(`\n [dry-run] Plan for ${kbName}:`));
745
745
  console.log(chalk.white(` Would upsert: ${allChunks.length} chunks from ${newOrChangedFiles.length} file(s)`));
746
746
  console.log(chalk.white(` Would delete (stale-file): ${fileIdsToDelete.length} blob SHA(s)`));
747
+ // NAME them, so the operator sees WHICH documents a real run removes, not just how many.
748
+ // A corpus file_id is a git blob SHA, so the corpus's own repositories can say where it lived.
749
+ if (fileIdsToDelete.length > 0) {
750
+ const { describeStaleBlobs } = await import('../core/staleBlobNames.js');
751
+ const { resolveOwningRepo } = await import('../core/ManifestLoader.js');
752
+ const repoRoots = [...new Set(files
753
+ .map((f) => resolveOwningRepo(f.absolute_path)?.repoRoot)
754
+ .filter(Boolean))];
755
+ for (const s of describeStaleBlobs(fileIdsToDelete, repoRoots)) {
756
+ const where = s.path
757
+ ? `${s.repo.split('/').pop()}:${s.path} (last seen in ${s.commit.substring(0, 8)})`
758
+ : 'not in any local history of this corpus — cannot be named';
759
+ console.log(chalk.gray(` - ${s.file_id.substring(0, 16)}… ${where}`));
760
+ }
761
+ }
747
762
  if (options.rebuild) {
748
763
  console.log(chalk.white(
749
764
  ` Would purge (rebuild): FULL scope ${communityId}/${appId}/${kbName} ` +
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Name the stale corpus files a sync would delete, so an operator sees WHICH documents go
3
+ * before a real run — not just how many. (GODSWORLD-DEV, 2026-09-18: a dry run reported
4
+ * "Would delete (stale-file): 3" on a KB where the three might matter, with no way to tell
5
+ * which three.)
6
+ *
7
+ * A `corpus:` file_id IS a git blob SHA, so the corpus's own repositories can say which path
8
+ * held that blob, and in which commit. No store round-trip and no server change. A blob that
9
+ * no local history holds is reported as such rather than guessed at.
10
+ */
11
+
12
+ import { execFileSync } from 'child_process';
13
+
14
+ /**
15
+ * Parse `git log --all --no-abbrev --format=%h --raw --find-object=<sha>` output: the most
16
+ * recent commit that added or removed that blob, and the path it lived at.
17
+ * @returns {{commit:string, path:string} | null}
18
+ */
19
+ export function parseFindObjectRaw(output, sha) {
20
+ let commit = null;
21
+ for (const line of String(output || '').split('\n')) {
22
+ if (/^[0-9a-f]{7,40}$/.test(line.trim())) { commit = line.trim(); continue; }
23
+ const m = line.match(/^:\d{6} \d{6} ([0-9a-f]{40}) ([0-9a-f]{40}) \S+\t(.+)$/);
24
+ if (m && commit && (m[1] === sha || m[2] === sha)) {
25
+ return { commit, path: m[3].split('\t').pop() };
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Resolve each stale `corpus:<sha>` id to where that blob lived, searching each repo root.
33
+ * @param {string[]} fileIds - `corpus:<sha>` ids
34
+ * @param {string[]} repoRoots - the repositories that own this corpus's sources
35
+ * @returns {Array<{file_id:string, repo:string|null, commit:string|null, path:string|null}>}
36
+ */
37
+ export function describeStaleBlobs(fileIds, repoRoots) {
38
+ return fileIds.map((file_id) => {
39
+ const sha = file_id.replace(/^corpus:/, '');
40
+ for (const repo of repoRoots) {
41
+ let out = '';
42
+ try {
43
+ out = execFileSync('git', ['log', '--all', '--no-abbrev', '--format=%h', '--raw', `--find-object=${sha}`],
44
+ { cwd: repo, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024 });
45
+ } catch {
46
+ continue;
47
+ }
48
+ const hit = parseFindObjectRaw(out, sha);
49
+ if (hit) return { file_id, repo, commit: hit.commit, path: hit.path };
50
+ }
51
+ return { file_id, repo: null, commit: null, path: null };
52
+ });
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descix/cli",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
5
5
  "repository": {
6
6
  "type": "git",