@descix/cli 1.0.5 ā 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -34
- package/agent-assets/guides/dynamic-site-pattern.md +10 -14
- package/agent-assets/guides/knowledge-only-pattern.md +11 -15
- package/agent-assets/guides/microservice-pattern.md +12 -14
- package/agent-assets/guides/static-site-pattern.md +12 -15
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/reference/app-creation.md +36 -101
- package/agent-assets/reference/architecture.md +24 -27
- package/agent-assets/reference/local-dev.md +0 -4
- package/agent-assets/reference/mcp-integration.md +14 -22
- package/agent-assets/reference/workspace-config.md +97 -340
- package/agent-assets/rules/descix_mcp.template.mdc +1 -1
- package/bin/descix.js +159 -98
- package/lib/api-client.js +30 -22
- package/lib/commands/airdrop.js +10 -0
- package/lib/commands/auth.js +33 -3
- package/lib/commands/briefer/index.js +8 -5
- package/lib/commands/config.js +13 -31
- package/lib/commands/corpus.js +161 -54
- package/lib/commands/health.js +116 -21
- package/lib/commands/init.js +25 -48
- package/lib/commands/kb.js +14 -4
- package/lib/commands/update.js +0 -1
- package/lib/core/Chunker.js +1 -1
- package/lib/core/CorpusWalker.js +238 -47
- package/lib/core/Hydrator.js +88 -11
- package/lib/core/ManifestLoader.js +68 -0
- package/lib/core/Syncer.js +147 -46
- package/lib/interactive.js +158 -0
- package/lib/origin.js +3 -3
- package/lib/wallet-file.js +36 -37
- package/lib/wizard/setup.js +104 -22
- package/lib/workspace-config.js +325 -167
- package/lib/workspace-identity.js +0 -1
- package/package.json +3 -3
- package/templates/agent-claude.md +0 -3
- package/templates/agent-cline.md +0 -3
- package/templates/agent-copilot.md +0 -3
- package/templates/agent-cursor.md +0 -3
- package/templates/drive/app/kb/General/README.md +1 -1
- package/templates/drive_templates/agent_app_template/kb/General/README.md +1 -1
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +3 -3
- package/templates/scaffolds/microservice/defaults-config.json +3 -1
- package/templates/scaffolds/microservice/manifest.json +9 -5
- package/templates/scaffolds/microservice/package.json +1 -1
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +2 -2
package/lib/commands/config.js
CHANGED
|
@@ -9,6 +9,8 @@ import chalk from 'chalk';
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { WorkspaceConfig } from '../workspace-config.js';
|
|
11
11
|
import { environmentNameFor } from '../environment-report.js';
|
|
12
|
+
import { resolveEffectiveOrigin } from '../api-client.js';
|
|
13
|
+
import { normalizeOrigin } from '../origin.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Show current configuration
|
|
@@ -20,15 +22,17 @@ export async function show() {
|
|
|
20
22
|
|
|
21
23
|
console.log(chalk.cyan('\nš DeSciX Workspace Configuration:\n'));
|
|
22
24
|
console.log(chalk.white(` Workspace: ${workspaceRoot}`));
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
console.log(chalk.white(` API URL: ${
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
// The origin every command in this shell will USE, and who chose it ā not the file's value
|
|
26
|
+
// alone. An inherited DESCIX_API_URL outranks the workspace pin; showing only the pin made
|
|
27
|
+
// `config show` say dev while commands ran against production.
|
|
28
|
+
const effective = await resolveEffectiveOrigin(workspaceConfig.env?.apiUrl);
|
|
29
|
+
console.log(chalk.white(` API URL: ${effective.origin}`));
|
|
30
|
+
console.log(chalk.white(` Environment: ${environmentNameFor(effective.origin)}`));
|
|
31
|
+
console.log(chalk.white(` Chosen by: ${effective.source}`));
|
|
32
|
+
const pinned = workspaceConfig.env?.apiUrl;
|
|
33
|
+
if (pinned && normalizeOrigin(pinned) !== effective.origin) {
|
|
34
|
+
console.log(chalk.yellow(` ā This workspace pins ${normalizeOrigin(pinned)} (${environmentNameFor(pinned)}), but ${effective.source} overrides it for this shell.`));
|
|
35
|
+
}
|
|
32
36
|
|
|
33
37
|
// Show mapped apps (v2.1 env.products)
|
|
34
38
|
const platform = workspaceConfig.env?.platform;
|
|
@@ -49,28 +53,6 @@ export async function show() {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
/**
|
|
53
|
-
* Set API URL
|
|
54
|
-
*/
|
|
55
|
-
export async function setUrl(url, options = {}) {
|
|
56
|
-
try {
|
|
57
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
58
|
-
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
59
|
-
|
|
60
|
-
workspaceConfig.apiUrl = url;
|
|
61
|
-
workspaceConfig.environment = url.includes('localhost') ? 'development' : 'production';
|
|
62
|
-
const configPath = await workspaceConfig.save(workspaceRoot);
|
|
63
|
-
|
|
64
|
-
console.log(chalk.green('\nā
Configuration updated!\n'));
|
|
65
|
-
console.log(chalk.white(` API URL: ${url}`));
|
|
66
|
-
console.log(chalk.gray(` Saved to: ${configPath}\n`));
|
|
67
|
-
|
|
68
|
-
} catch (error) {
|
|
69
|
-
console.error(chalk.red('Error updating config:', error.message));
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
56
|
/**
|
|
75
57
|
* Initialize config for environment
|
|
76
58
|
*/
|
package/lib/commands/corpus.js
CHANGED
|
@@ -27,7 +27,10 @@ import { WorkspaceConfig } from '../workspace-config.js';
|
|
|
27
27
|
import { loadManifests } from '../core/ManifestLoader.js';
|
|
28
28
|
import { walkCorpus } from '../core/CorpusWalker.js';
|
|
29
29
|
import { chunkFile, categorizeFile } from '../core/Chunker.js';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
upsertChunks, deleteStaleChunks, deleteStaleChunksByFileId, listRemoteFileIds, purgeKbScope,
|
|
32
|
+
isReportedCount, accumulateReportedCount, UNREPORTED_COUNT
|
|
33
|
+
} from '../core/Syncer.js';
|
|
31
34
|
import { lintManifestForDenyClasses, assertNoViolations } from '../core/CorpusDenyLint.js';
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -151,6 +154,57 @@ async function chunkCorpusFile(fileEntry, context) {
|
|
|
151
154
|
}).filter(Boolean);
|
|
152
155
|
}
|
|
153
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Read the previously-synced chunk total out of sync state, for REPORTING only.
|
|
159
|
+
*
|
|
160
|
+
* Three states must not be conflated:
|
|
161
|
+
* no sync state at all -> 0, a fact about US (we have never synced), not the store
|
|
162
|
+
* total_chunks is a number -> that number, INCLUDING a legitimate 0
|
|
163
|
+
* total_chunks null/absent -> null: UNKNOWN, and said so. It is never coerced to 0.
|
|
164
|
+
*
|
|
165
|
+
* The file delta is computed from blob SHAs, never from this number, so an unknown
|
|
166
|
+
* previous total blocks nothing. The total is re-established on every clean sync by the
|
|
167
|
+
* store's own live read-back (`liveTotalFromReconcile`), never by arithmetic on it.
|
|
168
|
+
*
|
|
169
|
+
* @param {Object|null} previousState - the loaded sync state, or null on first sync
|
|
170
|
+
* @returns {number|null}
|
|
171
|
+
*/
|
|
172
|
+
export function readPreviousChunkCount(previousState) {
|
|
173
|
+
if (!previousState) return 0;
|
|
174
|
+
return Number.isFinite(previousState.total_chunks) ? previousState.total_chunks : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The chunk total to PERSIST: the store's MEASURED live count from the post-sync
|
|
179
|
+
* reconcile (`get_kb_rag_status {reconcile:true}` ā `reconcileAfter`), or null when the
|
|
180
|
+
* store reported none. This is the ONE owner of `total_chunks`.
|
|
181
|
+
*
|
|
182
|
+
* Why not previous + upserted ā deleted: the server's `kb_sync_chunks` answers
|
|
183
|
+
* `upserted_count: null` by ruling (Pinecone's upsert API returns no count) and publishes
|
|
184
|
+
* the measured `rag_vector_count` instead. An arithmetic total built on that null was null
|
|
185
|
+
* on every sync, including `--rebuild` (measured 2026-09-16: reconcile read 6882 live and
|
|
186
|
+
* the state still said unknown), so the "re-run with --rebuild" remedy was a loop.
|
|
187
|
+
*
|
|
188
|
+
* @param {Object|null|undefined} rec - the reconcile response's message
|
|
189
|
+
* @returns {number|null}
|
|
190
|
+
*/
|
|
191
|
+
export function liveTotalFromReconcile(rec) {
|
|
192
|
+
return Number.isFinite(rec?.reconcileAfter) ? rec.reconcileAfter : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Render a count for display: the number when the store reported it, otherwise a
|
|
197
|
+
* phrase that says so. Consumes the module's count owner rather than re-testing
|
|
198
|
+
* `>= 0` at each call site.
|
|
199
|
+
*
|
|
200
|
+
* @param {number} value
|
|
201
|
+
* @param {string} unknownText
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
export function renderCount(value, unknownText = 'an unreported number of') {
|
|
205
|
+
return isReportedCount(value) ? String(value) : unknownText;
|
|
206
|
+
}
|
|
207
|
+
|
|
154
208
|
/**
|
|
155
209
|
* Save sync state after a successful corpus sync.
|
|
156
210
|
*
|
|
@@ -268,15 +322,21 @@ function resolveRef(manifest, cliRef) {
|
|
|
268
322
|
* @returns {Promise<boolean>}
|
|
269
323
|
*/
|
|
270
324
|
async function confirmYesNo(prompt) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
});
|
|
325
|
+
// Gated at construction by the ONE owner of "may I prompt". Under a non-TTY this throws a
|
|
326
|
+
// loud NonInteractiveError instead of hanging (open pipe/FIFO) or resolving nothing (/dev/null).
|
|
327
|
+
const { createPromptSession } = await import('../interactive.js');
|
|
328
|
+
const rl = createPromptSession({
|
|
329
|
+
what: 'descix kb corpus',
|
|
330
|
+
destructive: true,
|
|
331
|
+
nonInteractiveForm: ['descix kb corpus ... --yes # skips this confirmation']
|
|
279
332
|
});
|
|
333
|
+
try {
|
|
334
|
+
const answer = await rl.askRaw(`${prompt} [y/N] `);
|
|
335
|
+
const a = (answer || '').trim().toLowerCase();
|
|
336
|
+
return a === 'y' || a === 'yes';
|
|
337
|
+
} finally {
|
|
338
|
+
rl.close();
|
|
339
|
+
}
|
|
280
340
|
}
|
|
281
341
|
|
|
282
342
|
/**
|
|
@@ -455,9 +515,18 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
455
515
|
|
|
456
516
|
// 3a. Walk corpus
|
|
457
517
|
const walkSpinner = ora(' Walking source directories...').start();
|
|
458
|
-
const { files, commitSha, provenance } = await walkCorpus(manifest, workspaceRoot);
|
|
518
|
+
const { files, commitSha, provenance, gateBoundary } = await walkCorpus(manifest, workspaceRoot);
|
|
459
519
|
walkSpinner.succeed(` Found ${files.length} files (commit: ${commitSha.substring(0, 8)})`);
|
|
460
520
|
|
|
521
|
+
// A gate prints its own COVERAGE BOUNDARY with its verdict ā on GREEN as well as
|
|
522
|
+
// RED ā so the reader of a pass can see where the pass stops without hunting for a
|
|
523
|
+
// report elsewhere. Every file above cleared the source gate.
|
|
524
|
+
console.log(chalk.gray(` [source-gate] PASS ā all ${files.length} file(s) tracked by git at their manifest ref.`));
|
|
525
|
+
console.log(chalk.gray(` [source-gate] compares: ${gateBoundary.compares}`));
|
|
526
|
+
console.log(chalk.gray(` [source-gate] catches: ${gateBoundary.catches}`));
|
|
527
|
+
console.log(chalk.gray(` [source-gate] does NOT read: ${gateBoundary.does_not_read}`));
|
|
528
|
+
console.log(chalk.gray(` [source-gate] runs automatically: ${gateBoundary.runs_automatically}`));
|
|
529
|
+
|
|
461
530
|
// āā Deliverable A: --show-walk prints the resolved ref + first 50 files
|
|
462
531
|
// BEFORE any Pinecone read/write so the operator can sanity-check the
|
|
463
532
|
// walk results without trusting downstream output.
|
|
@@ -507,9 +576,13 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
507
576
|
const remote = await listRemoteFileIds(apiClient, appId, kbName);
|
|
508
577
|
rebuildRemoteUniqueCount = remote.unique_count;
|
|
509
578
|
rebuildRemoteTotalChunks = remote.total_chunks;
|
|
579
|
+
// `Pinecone reports N` is an ATTRIBUTION to the store, so it may only carry
|
|
580
|
+
// numbers the store actually reported. The file_ids list is always real
|
|
581
|
+
// (listRemoteFileIds throws rather than fabricating it); the counts may not be.
|
|
510
582
|
rebuildSpinner.succeed(
|
|
511
|
-
` [rebuild] Pinecone
|
|
512
|
-
|
|
583
|
+
` [rebuild] Pinecone enumerated ${remote.file_ids.length} corpus-scheme file_id(s); ` +
|
|
584
|
+
`reports ${renderCount(remote.unique_count)} unique / ` +
|
|
585
|
+
`${renderCount(remote.total_chunks)} enumerable chunk(s) (legacy orphans may exceed this)`
|
|
513
586
|
);
|
|
514
587
|
|
|
515
588
|
if (options.dryRun) {
|
|
@@ -521,9 +594,13 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
521
594
|
));
|
|
522
595
|
} else {
|
|
523
596
|
// Interactive confirmation guard. --yes skips for scripting/CI.
|
|
597
|
+
// A destructive-action prompt must not overstate its own certainty: if the
|
|
598
|
+
// store reported no count, say so rather than printing "ā„-1".
|
|
524
599
|
console.log(chalk.yellow(
|
|
525
600
|
`\n About to FULL-SCOPE PURGE every vector in ${communityId}/${appId}/${kbName} ` +
|
|
526
|
-
|
|
601
|
+
(isReportedCount(rebuildRemoteTotalChunks)
|
|
602
|
+
? `(ā„${rebuildRemoteTotalChunks} enumerable chunk(s); legacy orphans included), `
|
|
603
|
+
: `(the store reported no chunk count; ALL vectors in scope will go), `) +
|
|
527
604
|
`then re-upsert the full local walk (${files.length} file(s)).`
|
|
528
605
|
));
|
|
529
606
|
|
|
@@ -544,9 +621,9 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
544
621
|
rebuildScopePurged = purgeResult.purged_scope;
|
|
545
622
|
// deleteRAG via filter-based deleteMany returns -1 (count not knowable
|
|
546
623
|
// from the SDK). Surface honestly rather than fabricating a number.
|
|
547
|
-
rebuildPurged = purgeResult.deleted
|
|
624
|
+
rebuildPurged = purgeResult.deleted;
|
|
548
625
|
purgeSpinner.succeed(
|
|
549
|
-
purgeResult.deleted
|
|
626
|
+
isReportedCount(purgeResult.deleted)
|
|
550
627
|
? ` [rebuild] Purged ${purgeResult.deleted} vector(s) (full scope) and reset KB doc rag fields`
|
|
551
628
|
: ` [rebuild] Purged full scope (filter-based delete; SDK reports no count) and reset KB doc rag fields`
|
|
552
629
|
);
|
|
@@ -574,10 +651,10 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
574
651
|
previousState = null;
|
|
575
652
|
}
|
|
576
653
|
const previousBlobShas = new Set(previousState?.synced_blob_shas || []);
|
|
577
|
-
const previousChunkCount = previousState
|
|
654
|
+
const previousChunkCount = readPreviousChunkCount(previousState);
|
|
578
655
|
|
|
579
656
|
if (previousBlobShas.size > 0) {
|
|
580
|
-
console.log(chalk.gray(` Previous sync: ${previousBlobShas.size} files, ${previousChunkCount} chunks`));
|
|
657
|
+
console.log(chalk.gray(` Previous sync: ${previousBlobShas.size} files, ${previousChunkCount ?? 'an unknown number of'} chunks`));
|
|
581
658
|
} else {
|
|
582
659
|
console.log(chalk.gray(' First sync (no previous state)'));
|
|
583
660
|
}
|
|
@@ -640,7 +717,12 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
640
717
|
console.log(chalk.white(` Would upsert: ${allChunks.length} chunks from ${newOrChangedFiles.length} file(s)`));
|
|
641
718
|
console.log(chalk.white(` Would delete (stale-file): ${fileIdsToDelete.length} blob SHA(s)`));
|
|
642
719
|
if (options.rebuild) {
|
|
643
|
-
console.log(chalk.white(
|
|
720
|
+
console.log(chalk.white(
|
|
721
|
+
` Would purge (rebuild): FULL scope ${communityId}/${appId}/${kbName} ` +
|
|
722
|
+
(isReportedCount(rebuildRemoteTotalChunks)
|
|
723
|
+
? `(ā„${rebuildRemoteTotalChunks} enumerable chunks; legacy orphans included)`
|
|
724
|
+
: `(the store reported no chunk count; all vectors in scope)`)
|
|
725
|
+
));
|
|
644
726
|
}
|
|
645
727
|
// Track for the summary exit-code computation at the end of the loop.
|
|
646
728
|
totalFilesProcessed += newOrChangedFiles.length;
|
|
@@ -648,7 +730,10 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
648
730
|
totalFilesSkipped += unchangedCount;
|
|
649
731
|
// Full-scope rebuild would purge ā„ the enumerable chunk count. Use it as the
|
|
650
732
|
// would-delete tally so dry-run reports drift whenever a purge would run.
|
|
651
|
-
|
|
733
|
+
// Only add a rebuild figure the store actually reported; an unknown count
|
|
734
|
+
// contributes nothing rather than silently subtracting one from the total.
|
|
735
|
+
totalFilesDeleted += fileIdsToDelete.length +
|
|
736
|
+
((options.rebuild && isReportedCount(rebuildRemoteTotalChunks)) ? rebuildRemoteTotalChunks : 0);
|
|
652
737
|
// Do NOT call upsertChunks, deleteStaleChunksByFileId, deleteStaleChunks,
|
|
653
738
|
// or saveSyncState in dry-run. Continue to next manifest.
|
|
654
739
|
continue;
|
|
@@ -675,7 +760,9 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
675
760
|
while (!success && retries < MAX_RETRIES) {
|
|
676
761
|
try {
|
|
677
762
|
const result = await upsertChunks(apiClient, communityId, appId, kbName, batch);
|
|
678
|
-
|
|
763
|
+
// Unknown-ness survives the running total: one unreportable batch makes
|
|
764
|
+
// the whole figure unstateable rather than quietly summing around it.
|
|
765
|
+
upserted = accumulateReportedCount(upserted, result.upserted);
|
|
679
766
|
success = true;
|
|
680
767
|
syncSpinner.text = ` Upserting chunks... ${i + batch.length}/${allChunks.length}`;
|
|
681
768
|
} catch (err) {
|
|
@@ -715,7 +802,7 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
715
802
|
if (!success) {
|
|
716
803
|
syncSpinner.warn(
|
|
717
804
|
` Batch ${i}-${i + batch.length} did not complete after ${MAX_RETRIES} retries. ` +
|
|
718
|
-
`Synced ${upserted} chunks so far. This run is BOUNDED (it did not hang) ā ` +
|
|
805
|
+
`Synced ${renderCount(upserted)} chunks so far. This run is BOUNDED (it did not hang) ā ` +
|
|
719
806
|
`re-run 'descix kb corpus sync -a ${appId} -k ${kbName}' to RESUME: id-keyed upserts ` +
|
|
720
807
|
`are idempotent so already-synced batches are cheap and the remainder continues.`
|
|
721
808
|
);
|
|
@@ -728,7 +815,7 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
728
815
|
}
|
|
729
816
|
}
|
|
730
817
|
|
|
731
|
-
syncSpinner.succeed(` Upserted: ${upserted} chunks`);
|
|
818
|
+
syncSpinner.succeed(` Upserted: ${renderCount(upserted)} chunks`);
|
|
732
819
|
|
|
733
820
|
// Write failure log to .descix if any batches failed
|
|
734
821
|
if (syncFailures.length > 0) {
|
|
@@ -752,59 +839,76 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
752
839
|
// (community_id, app_id, knowledgebase_name) ā guarantees namespace isolation.
|
|
753
840
|
const result = await deleteStaleChunksByFileId(apiClient, communityId, appId, kbName, fileIdsToDelete);
|
|
754
841
|
deleted = result.deleted;
|
|
755
|
-
deleteSpinner.succeed(
|
|
842
|
+
deleteSpinner.succeed(
|
|
843
|
+
` Purged: ${renderCount(deleted)} chunk(s) across ${fileIdsToDelete.length} stale blob SHA(s)`
|
|
844
|
+
);
|
|
756
845
|
} catch (err) {
|
|
757
|
-
|
|
846
|
+
// The purge ERRORED: how many were removed is unknown. Leaving `deleted` at 0
|
|
847
|
+
// would persist "nothing was deleted" into sync state as though it were a fact.
|
|
848
|
+
deleted = UNREPORTED_COUNT;
|
|
849
|
+
deleteSpinner.warn(` Delete failed (deleted count unknown): ${err.message}`);
|
|
758
850
|
}
|
|
759
851
|
}
|
|
760
852
|
|
|
761
|
-
// 3f.
|
|
762
|
-
const allSyncedBlobShas = [...localBlobShas]; // All current files
|
|
763
|
-
await saveSyncState(appRoot, kbName, {
|
|
764
|
-
last_sync_commit: commitSha,
|
|
765
|
-
// Per-source provenance: WHICH repo, at WHICH ref, and the RESOLVED COMMIT SHA actually
|
|
766
|
-
// synced. `ref` may be a mutable branch, so the ref alone is not a record of what was
|
|
767
|
-
// synced ā the resolved sha is, and it stays exact after the branch moves.
|
|
768
|
-
sources_provenance: provenance,
|
|
769
|
-
synced_files_count: files.length,
|
|
770
|
-
total_chunks: previousChunkCount + upserted - deleted,
|
|
771
|
-
timestamp: new Date().toISOString(),
|
|
772
|
-
files_upserted: newOrChangedFiles.length,
|
|
773
|
-
files_unchanged: unchangedCount,
|
|
774
|
-
chunks_upserted: upserted,
|
|
775
|
-
chunks_deleted: deleted,
|
|
776
|
-
synced_blob_shas: allSyncedBlobShas
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
// 3g. AUTO-RECONCILE the cached rag_vector_count to the TRUE live Pinecone
|
|
853
|
+
// 3f. AUTO-RECONCILE the cached rag_vector_count to the TRUE live Pinecone
|
|
780
854
|
// count after a COMPLETED, clean sync (no batch failures). This keeps the
|
|
781
855
|
// doctor's fast cached read truthful after every clean run, so drift only
|
|
782
856
|
// ever appears mid-flight (on interruption). Skipped if any batch failed
|
|
783
857
|
// (the run is partial ā reconciling would bake in the partial state and the
|
|
784
858
|
// re-run will reconcile once complete). Non-fatal: a reconcile hiccup must
|
|
785
|
-
// not fail an otherwise-successful sync
|
|
859
|
+
// not fail an otherwise-successful sync ā but then the persisted total is
|
|
860
|
+
// null, honestly, because nothing measured it.
|
|
861
|
+
let liveTotal = null;
|
|
786
862
|
if (syncFailures.length === 0) {
|
|
787
863
|
try {
|
|
788
864
|
const recRes = await apiClient.invoke('get_kb_rag_status', {
|
|
789
865
|
app_id: appId, kb_id: kbName, reconcile: true
|
|
790
866
|
}, { allowGuest: false });
|
|
791
867
|
const rec = recRes.message || recRes;
|
|
792
|
-
|
|
868
|
+
liveTotal = liveTotalFromReconcile(rec);
|
|
869
|
+
if (liveTotal !== null) {
|
|
793
870
|
const d = rec.reconcileAfter - rec.reconcileBefore;
|
|
794
871
|
console.log(chalk.gray(
|
|
795
872
|
` ā³ Reconciled live count: cached ${rec.reconcileBefore} ā live ${rec.reconcileAfter}` +
|
|
796
873
|
(d === 0 ? ' (already truthful)' : ` (${d >= 0 ? '+' : ''}${d})`)
|
|
797
874
|
));
|
|
875
|
+
} else {
|
|
876
|
+
console.log(chalk.yellow(' ā Reconcile reported no live count; total_chunks stays unknown'));
|
|
798
877
|
}
|
|
799
878
|
} catch (recErr) {
|
|
800
879
|
console.log(chalk.yellow(` ā Auto-reconcile skipped (non-fatal): ${recErr.message}`));
|
|
801
880
|
}
|
|
802
881
|
}
|
|
803
882
|
|
|
883
|
+
// 3g. Save sync state with blob SHAs for future delta computation
|
|
884
|
+
const allSyncedBlobShas = [...localBlobShas]; // All current files
|
|
885
|
+
await saveSyncState(appRoot, kbName, {
|
|
886
|
+
last_sync_commit: commitSha,
|
|
887
|
+
// Per-source provenance: WHICH repo, at WHICH ref, and the RESOLVED COMMIT SHA actually
|
|
888
|
+
// synced. `ref` may be a mutable branch, so the ref alone is not a record of what was
|
|
889
|
+
// synced ā the resolved sha is, and it stays exact after the branch moves.
|
|
890
|
+
sources_provenance: provenance,
|
|
891
|
+
synced_files_count: files.length,
|
|
892
|
+
// The store's measured live count, or null ā never a fabricated integer: this file
|
|
893
|
+
// is read by later runs, so a wrong number here outlives the console line.
|
|
894
|
+
total_chunks: liveTotal,
|
|
895
|
+
timestamp: new Date().toISOString(),
|
|
896
|
+
files_upserted: newOrChangedFiles.length,
|
|
897
|
+
files_unchanged: unchangedCount,
|
|
898
|
+
chunks_upserted: isReportedCount(upserted) ? upserted : null,
|
|
899
|
+
chunks_deleted: isReportedCount(deleted) ? deleted : null,
|
|
900
|
+
synced_blob_shas: allSyncedBlobShas
|
|
901
|
+
});
|
|
902
|
+
|
|
804
903
|
totalFilesProcessed += newOrChangedFiles.length;
|
|
805
|
-
totalChunksCreated += upserted;
|
|
806
904
|
totalFilesSkipped += unchangedCount;
|
|
807
|
-
|
|
905
|
+
// A run total is summed by the SAME owner the per-batch total uses (:754), so an
|
|
906
|
+
// unreported leg leaves the total unstateable instead of quietly summing around it.
|
|
907
|
+
// Contributing 0 keeps -1 out of the printed total by fabricating a number in its
|
|
908
|
+
// place ā the same untruth, told where nobody can see it.
|
|
909
|
+
totalChunksCreated = accumulateReportedCount(totalChunksCreated, upserted);
|
|
910
|
+
totalFilesDeleted = accumulateReportedCount(totalFilesDeleted, deleted);
|
|
911
|
+
totalFilesDeleted = accumulateReportedCount(totalFilesDeleted, rebuildPurged);
|
|
808
912
|
}
|
|
809
913
|
|
|
810
914
|
// Summary
|
|
@@ -827,13 +931,12 @@ export async function runCorpusSync(apiClient, options) {
|
|
|
827
931
|
|
|
828
932
|
console.log(chalk.green('\nCorpus sync complete:'));
|
|
829
933
|
console.log(chalk.white(` Files synced: ${totalFilesProcessed}`));
|
|
830
|
-
console.log(chalk.white(` Chunks created: ${totalChunksCreated}`));
|
|
934
|
+
console.log(chalk.white(` Chunks created: ${renderCount(totalChunksCreated)}`));
|
|
831
935
|
console.log(chalk.white(` Files unchanged: ${totalFilesSkipped}`));
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
936
|
+
// Only a zero the store actually REPORTED is dimmed as a no-op; an unreported total
|
|
937
|
+
// is stated as unknown in full white, never collapsed into the "nothing happened" line.
|
|
938
|
+
const deletedLine = ` Chunks deleted: ${renderCount(totalFilesDeleted)}`;
|
|
939
|
+
console.log(totalFilesDeleted === 0 ? chalk.gray(deletedLine) : chalk.white(deletedLine));
|
|
837
940
|
console.log('');
|
|
838
941
|
return { dryRun: false };
|
|
839
942
|
|
|
@@ -917,7 +1020,11 @@ export async function runCorpusStatus(apiClient, options) {
|
|
|
917
1020
|
console.log(chalk.white(` Last sync: ${syncState.timestamp}`));
|
|
918
1021
|
console.log(chalk.white(` Commit: ${syncState.last_sync_commit?.substring(0, 8) || 'unknown'}`));
|
|
919
1022
|
console.log(chalk.white(` Files tracked: ${syncState.synced_files_count}`));
|
|
920
|
-
console.log(chalk.white(
|
|
1023
|
+
console.log(chalk.white(
|
|
1024
|
+
` Chunks in store: ${Number.isFinite(syncState.total_chunks)
|
|
1025
|
+
? syncState.total_chunks
|
|
1026
|
+
: 'unknown (the last sync reported no live count)'}`
|
|
1027
|
+
));
|
|
921
1028
|
|
|
922
1029
|
// Quick change detection: walk and compare blob SHAs
|
|
923
1030
|
if (options.verbose) {
|