@ryanstark24/sfgraph-cli 1.2.1 → 1.6.0
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/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +8 -3
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/diagnose.d.ts +139 -0
- package/dist/commands/diagnose.d.ts.map +1 -0
- package/dist/commands/diagnose.js +508 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/ingest.d.ts +73 -0
- package/dist/commands/ingest.d.ts.map +1 -1
- package/dist/commands/ingest.js +362 -19
- package/dist/commands/ingest.js.map +1 -1
- package/dist/commands/rebuild-bindings.d.ts.map +1 -1
- package/dist/commands/rebuild-bindings.js +2 -2
- package/dist/commands/rebuild-bindings.js.map +1 -1
- package/dist/commands/reset-elemid-map.d.ts.map +1 -1
- package/dist/commands/reset-elemid-map.js.map +1 -1
- package/dist/commands/serve.d.ts +7 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +59 -1
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/skip-labels.d.ts +11 -0
- package/dist/commands/skip-labels.d.ts.map +1 -0
- package/dist/commands/skip-labels.js +15 -0
- package/dist/commands/skip-labels.js.map +1 -0
- package/dist/commands/telemetry.js +1 -1
- package/dist/commands/telemetry.js.map +1 -1
- package/dist/heap.d.ts +24 -0
- package/dist/heap.d.ts.map +1 -0
- package/dist/heap.js +45 -0
- package/dist/heap.js.map +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -2
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/dist/__tests__/doctor.test.d.ts +0 -2
- package/dist/__tests__/doctor.test.d.ts.map +0 -1
- package/dist/__tests__/doctor.test.js +0 -64
- package/dist/__tests__/doctor.test.js.map +0 -1
- package/dist/__tests__/ingest-db-guard.test.d.ts +0 -2
- package/dist/__tests__/ingest-db-guard.test.d.ts.map +0 -1
- package/dist/__tests__/ingest-db-guard.test.js +0 -110
- package/dist/__tests__/ingest-db-guard.test.js.map +0 -1
- package/dist/__tests__/ingest-multi.test.d.ts +0 -2
- package/dist/__tests__/ingest-multi.test.d.ts.map +0 -1
- package/dist/__tests__/ingest-multi.test.js +0 -51
- package/dist/__tests__/ingest-multi.test.js.map +0 -1
- package/dist/__tests__/install-dry-run.test.d.ts +0 -2
- package/dist/__tests__/install-dry-run.test.d.ts.map +0 -1
- package/dist/__tests__/install-dry-run.test.js +0 -29
- package/dist/__tests__/install-dry-run.test.js.map +0 -1
- package/dist/__tests__/install-e2e.test.d.ts +0 -2
- package/dist/__tests__/install-e2e.test.d.ts.map +0 -1
- package/dist/__tests__/install-e2e.test.js +0 -33
- package/dist/__tests__/install-e2e.test.js.map +0 -1
- package/dist/__tests__/mcp-config.test.d.ts +0 -2
- package/dist/__tests__/mcp-config.test.d.ts.map +0 -1
- package/dist/__tests__/mcp-config.test.js +0 -104
- package/dist/__tests__/mcp-config.test.js.map +0 -1
- package/dist/__tests__/rebuild.test.d.ts +0 -2
- package/dist/__tests__/rebuild.test.d.ts.map +0 -1
- package/dist/__tests__/rebuild.test.js +0 -180
- package/dist/__tests__/rebuild.test.js.map +0 -1
- package/dist/__tests__/serve-guard.test.d.ts +0 -2
- package/dist/__tests__/serve-guard.test.d.ts.map +0 -1
- package/dist/__tests__/serve-guard.test.js +0 -82
- package/dist/__tests__/serve-guard.test.js.map +0 -1
- package/dist/__tests__/snapshot.test.d.ts +0 -2
- package/dist/__tests__/snapshot.test.d.ts.map +0 -1
- package/dist/__tests__/snapshot.test.js +0 -139
- package/dist/__tests__/snapshot.test.js.map +0 -1
- package/dist/__tests__/telemetry.test.d.ts +0 -2
- package/dist/__tests__/telemetry.test.d.ts.map +0 -1
- package/dist/__tests__/telemetry.test.js +0 -53
- package/dist/__tests__/telemetry.test.js.map +0 -1
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import type { LiveIngestOpts } from "@ryanstark24/sfgraph-core";
|
|
2
|
+
import { ConsoleLogger } from "@ryanstark24/sfgraph-shared";
|
|
1
3
|
export interface IngestOpts {
|
|
4
|
+
/** Offline ingest of an SFDX checkout; --org then only sets the alias. */
|
|
5
|
+
project?: string | undefined;
|
|
6
|
+
/** With --project: re-ingest only the members that changed (Task 11c). */
|
|
7
|
+
delta?: boolean | undefined;
|
|
2
8
|
org?: string | undefined;
|
|
3
9
|
orgs?: string | undefined;
|
|
4
10
|
all?: boolean | undefined;
|
|
@@ -10,6 +16,9 @@ export interface IngestOpts {
|
|
|
10
16
|
embedModelDim?: number | undefined;
|
|
11
17
|
rebuild?: boolean | undefined;
|
|
12
18
|
noBackup?: boolean | undefined;
|
|
19
|
+
/** Return at "graph ready" instead of waiting for embeddings
|
|
20
|
+
* (`--no-wait-embeddings`); the next ingest fills the pending vectors. */
|
|
21
|
+
noWaitEmbeddings?: boolean | undefined;
|
|
13
22
|
detectDeletions?: boolean | undefined;
|
|
14
23
|
/** Comma-separated source labels to fetch (e.g. 'apex,generic:Profile').
|
|
15
24
|
* Forces mode=full and merges into the existing graph without rebuild. */
|
|
@@ -44,5 +53,69 @@ export interface IngestOpts {
|
|
|
44
53
|
* Env: SFGRAPH_SKIP_THRESHOLD. Default 5. */
|
|
45
54
|
skipThreshold?: number | undefined;
|
|
46
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The persistent embedding cache for the active model:
|
|
58
|
+
* `<dataDir>/embedding-cache/<modelId>@<sha16>[@<cap>].sqlite`, where sha16 is
|
|
59
|
+
* the first 16 hex chars of the model weights file's sha256, so new weights
|
|
60
|
+
* under the same model id never reuse old vectors. A non-default token cap
|
|
61
|
+
* changes the vectors too, so it gets its own file. Undefined (no cache) when
|
|
62
|
+
* the model file cannot be found: there is nothing trustworthy to key it by.
|
|
63
|
+
* The file lives outside the per-org graph DB, so `--rebuild` reuses it; it
|
|
64
|
+
* only grows.
|
|
65
|
+
*/
|
|
66
|
+
export declare function embeddingCacheFileFor(dataDir: string): Promise<string | undefined>;
|
|
67
|
+
/**
|
|
68
|
+
* Ruling (a): an embedding failure after graph ready is not an ingest failure.
|
|
69
|
+
* The graph is complete and the unembedded texts stay flagged pending, so the
|
|
70
|
+
* next ingest fills them (find_similar uses text search meanwhile). Warn and
|
|
71
|
+
* leave the exit code alone; live-ingest already printed the cause.
|
|
72
|
+
*/
|
|
73
|
+
export declare function reportEmbeddingOutcome(result: {
|
|
74
|
+
embeddings?: {
|
|
75
|
+
error?: string;
|
|
76
|
+
failed?: number;
|
|
77
|
+
pending?: number;
|
|
78
|
+
};
|
|
79
|
+
}): void;
|
|
80
|
+
/** Give every built ingest the same embedder (multi-org runs: one worker and
|
|
81
|
+
* one model in memory instead of one per org). The caller closes it. */
|
|
82
|
+
export declare function shareEmbedder(build: (alias: string) => Promise<LiveIngestOpts>, embedder: NonNullable<LiveIngestOpts["embedder"]>): (alias: string) => Promise<LiveIngestOpts>;
|
|
83
|
+
/** Offline ingest of an SFDX checkout (plus vlocity/datapacks when present).
|
|
84
|
+
* Reuses buildSingleIngestOpts with a resolver that never contacts an org. */
|
|
85
|
+
export declare function buildProjectIngestOpts(opts: IngestOpts, logger: ConsoleLogger, deps?: {
|
|
86
|
+
onResolved?: (orgId: string, alias: string) => void;
|
|
87
|
+
}): Promise<LiveIngestOpts>;
|
|
88
|
+
/**
|
|
89
|
+
* Run a move/delete of the graph file. On Windows a file another process has
|
|
90
|
+
* open (`sfgraph serve`, or an MCP client's server) cannot be renamed or
|
|
91
|
+
* deleted: report that plainly instead of a raw EBUSY/EPERM. Nothing has been
|
|
92
|
+
* moved when this throws, so the existing graph is untouched.
|
|
93
|
+
*/
|
|
94
|
+
export declare function onGraphFile<T>(dbPath: string, op: () => T): T;
|
|
95
|
+
/** Rebuild backups kept per org (each is a full copy of the graph file). */
|
|
96
|
+
export declare const REBUILD_BACKUPS_KEPT = 2;
|
|
97
|
+
/**
|
|
98
|
+
* Delete all but the newest {@link REBUILD_BACKUPS_KEPT} rebuild backups of
|
|
99
|
+
* one org (with their sidecars). The names carry an ISO stamp, so name order
|
|
100
|
+
* is time order. The newest backup that still holds snapshot history is always
|
|
101
|
+
* kept too: a rebuild whose snapshot copy failed leaves a backup without
|
|
102
|
+
* history, and two of those in a row must not push the real history out.
|
|
103
|
+
* Best-effort.
|
|
104
|
+
*/
|
|
105
|
+
export declare function pruneRebuildBackups(backupDir: string, orgId: string): Promise<string[]>;
|
|
106
|
+
/**
|
|
107
|
+
* Task 11c: one ingest per org at a time (`<dataDir>/<orgId>.ingest.lock`).
|
|
108
|
+
* Taken as soon as an org is resolved, before --rebuild moves its graph;
|
|
109
|
+
* released when the command ends. Re-taking a held org is a no-op.
|
|
110
|
+
* While a lock is held, SIGINT / SIGTERM release it and exit with 128 + signo
|
|
111
|
+
* (a stale lock is only detectable on the same host).
|
|
112
|
+
*/
|
|
113
|
+
export declare function ingestLocks(o?: {
|
|
114
|
+
dir?: string;
|
|
115
|
+
onSignal?: (sig: NodeJS.Signals) => void;
|
|
116
|
+
}): {
|
|
117
|
+
take: (orgId: string, alias: string) => void;
|
|
118
|
+
releaseAll: () => void;
|
|
119
|
+
};
|
|
47
120
|
export declare function ingestCmd(opts: IngestOpts): Promise<void>;
|
|
48
121
|
//# sourceMappingURL=ingest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/commands/ingest.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/commands/ingest.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/ingest.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { constants as osConstants } from "node:os";
|
|
1
3
|
import path from "node:path";
|
|
2
|
-
import { SqliteGraphStore, SqliteSnapshotStore, listAllAuthenticatedOrgs, liveIngest, multiOrgIngest, } from "@ryanstark24/sfgraph-core";
|
|
4
|
+
import { EmbeddingPool, SqliteGraphStore, SqliteSnapshotStore, SqliteVectorStore, acquireIngestLock, defaultEmbeddingWorkerUrl, embedMaxTokens, embedModelFingerprint, embeddingCachePath, listAllAuthenticatedOrgs, liveIngest, multiOrgIngest, resolveEmbedModelId, snapshotCountInFile, } from "@ryanstark24/sfgraph-core";
|
|
3
5
|
import { ConsoleLogger, SfgraphError, getSfgraphPaths, safeOrgDbPath, } from "@ryanstark24/sfgraph-shared";
|
|
6
|
+
import { retrySkippedLabels } from "./skip-labels.js";
|
|
4
7
|
/** Skip categories that are worth retrying. `insufficient_access` and
|
|
5
8
|
* `not_found` are stable conditions that won't fix themselves on a
|
|
6
9
|
* second pass; retrying them just wastes time. */
|
|
@@ -44,21 +47,68 @@ function validateDbPath(rawDbPath, dataDir) {
|
|
|
44
47
|
}
|
|
45
48
|
async function buildSingleIngestOpts(alias, opts, logger, deps) {
|
|
46
49
|
const resolved = await deps.resolveOrg(alias);
|
|
50
|
+
deps.onResolved?.(String(resolved.orgId), alias);
|
|
47
51
|
const dataDir = getSfgraphPaths().data;
|
|
48
52
|
const dbPath = opts.db
|
|
49
53
|
? validateDbPath(opts.db, dataDir)
|
|
50
54
|
: safeOrgDbPath(dataDir, String(resolved.orgId));
|
|
51
|
-
|
|
52
|
-
await applyRebuild(dbPath, String(resolved.orgId), Boolean(opts.noBackup), logger)
|
|
53
|
-
|
|
55
|
+
const backupPath = opts.rebuild
|
|
56
|
+
? await applyRebuild(dbPath, String(resolved.orgId), Boolean(opts.noBackup), logger)
|
|
57
|
+
: undefined;
|
|
54
58
|
const graphStore = new SqliteGraphStore({ dbPath });
|
|
55
59
|
await graphStore.init();
|
|
60
|
+
// Closed if anything below fails: the caller only closes what it gets back.
|
|
61
|
+
try {
|
|
62
|
+
return await buildOnStore(graphStore, {
|
|
63
|
+
alias,
|
|
64
|
+
opts,
|
|
65
|
+
logger,
|
|
66
|
+
dbPath,
|
|
67
|
+
dataDir,
|
|
68
|
+
resolved,
|
|
69
|
+
backupPath,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
await graphStore.close().catch(() => { });
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function buildOnStore(graphStore, c) {
|
|
78
|
+
const { alias, opts, logger, dbPath, dataDir, resolved, backupPath } = c;
|
|
56
79
|
const snapshotStore = new SqliteSnapshotStore({
|
|
57
80
|
dbPath,
|
|
58
81
|
db: graphStore.db,
|
|
59
82
|
skipMigrations: true,
|
|
60
83
|
});
|
|
61
84
|
await snapshotStore.init();
|
|
85
|
+
// A rebuild keeps the snapshot history (point_in_time_diff): copy it from
|
|
86
|
+
// the backup before the first snapshot of the new graph is taken.
|
|
87
|
+
const carried = backupPath
|
|
88
|
+
? carrySnapshots(snapshotStore, backupPath, String(resolved.orgId))
|
|
89
|
+
: 0;
|
|
90
|
+
// Wire a vector store so ingest GENERATES MiniLM embeddings — without this
|
|
91
|
+
// `opts.vectorStore` is undefined, live-ingest never builds its embedding
|
|
92
|
+
// queue, and the vec0 tables stay empty (semantic find_similar returns
|
|
93
|
+
// "vector index unavailable"). Reuses the graph store's SQLite handle and
|
|
94
|
+
// loads sqlite-vec on it. If the native vec extension or the embedding model
|
|
95
|
+
// is unavailable, degrade gracefully (null → structural ingest still works,
|
|
96
|
+
// just no vectors) instead of failing the whole ingest.
|
|
97
|
+
let vectorStore;
|
|
98
|
+
try {
|
|
99
|
+
const vs = new SqliteVectorStore({
|
|
100
|
+
dbPath,
|
|
101
|
+
db: graphStore.db,
|
|
102
|
+
skipMigrations: true,
|
|
103
|
+
});
|
|
104
|
+
await vs.init();
|
|
105
|
+
vectorStore = vs;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
logger.warn("ingest: vector store unavailable — embeddings disabled", {
|
|
109
|
+
err: e.message,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
62
112
|
// --rebuild and --only and --retry-skipped all force a full sync because
|
|
63
113
|
// incremental wouldn't pick up newly-permitted/unblocked types.
|
|
64
114
|
const forceFull = Boolean(opts.rebuild || opts.only || opts.retrySkipped);
|
|
@@ -80,9 +130,9 @@ async function buildSingleIngestOpts(alias, opts, logger, deps) {
|
|
|
80
130
|
}
|
|
81
131
|
try {
|
|
82
132
|
const raw = JSON.parse(readFileSync(skipReportPath, "utf8"));
|
|
83
|
-
const labels = (raw.skips ?? [])
|
|
133
|
+
const labels = retrySkippedLabels(raw.skips ?? []);
|
|
84
134
|
if (labels.length === 0) {
|
|
85
|
-
console.log("--retry-skipped: previous run had
|
|
135
|
+
console.log("--retry-skipped: previous run had no skips. Nothing to retry.");
|
|
86
136
|
// Return a no-op opts; the live-ingest will exit cleanly with no work.
|
|
87
137
|
onlyLabels = new Set();
|
|
88
138
|
}
|
|
@@ -95,22 +145,100 @@ async function buildSingleIngestOpts(alias, opts, logger, deps) {
|
|
|
95
145
|
throw new Error(`--retry-skipped: failed to read skip report at ${skipReportPath}: ${e.message}`);
|
|
96
146
|
}
|
|
97
147
|
}
|
|
148
|
+
const embeddingCacheFile = vectorStore ? await embeddingCacheFileFor(dataDir) : undefined;
|
|
98
149
|
return {
|
|
99
150
|
alias,
|
|
100
151
|
mode,
|
|
101
152
|
graphStore,
|
|
102
153
|
snapshotStore,
|
|
154
|
+
...(vectorStore ? { vectorStore, waitEmbeddings: !opts.noWaitEmbeddings } : {}),
|
|
155
|
+
...(embeddingCacheFile ? { embeddingCacheFile } : {}),
|
|
103
156
|
logger,
|
|
104
157
|
preResolved: resolved,
|
|
105
158
|
detectDeletions: Boolean(opts.detectDeletions),
|
|
106
|
-
|
|
159
|
+
// User-chosen labels (--only / --retry-skipped) are read in full: no
|
|
160
|
+
// early stop on identical refusals (review N1c).
|
|
161
|
+
...(onlyLabels ? { onlyLabels, explicitOnlyLabels: true } : {}),
|
|
107
162
|
skipReportPath,
|
|
108
163
|
disableCrossFlavor: Boolean(opts.noCrossFlavor),
|
|
109
164
|
disableArityResolve: Boolean(opts.noArityResolve),
|
|
110
165
|
disableFlowInvocableResolve: Boolean(opts.noFlowResolve),
|
|
111
166
|
disableAudit: Boolean(opts.noAudit),
|
|
167
|
+
// A snapshot of the still-empty rebuilt graph adds nothing to the carried
|
|
168
|
+
// history; the next ingest's snapshot records the rebuilt state.
|
|
169
|
+
...(carried > 0 ? { skipSnapshot: true } : {}),
|
|
170
|
+
// Populate the _sfgraph_findings / dead-code / governor / test-coverage
|
|
171
|
+
// tables after merge. Without this the analysis MCP tools (dead_code_audit,
|
|
172
|
+
// security_audit, governor_risk_check, export_sarif) fall back to weak
|
|
173
|
+
// live scans or return empty results — the tables existed but nothing
|
|
174
|
+
// ever wrote to them.
|
|
175
|
+
analysisDb: graphStore.db,
|
|
112
176
|
};
|
|
113
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* The persistent embedding cache for the active model:
|
|
180
|
+
* `<dataDir>/embedding-cache/<modelId>@<sha16>[@<cap>].sqlite`, where sha16 is
|
|
181
|
+
* the first 16 hex chars of the model weights file's sha256, so new weights
|
|
182
|
+
* under the same model id never reuse old vectors. A non-default token cap
|
|
183
|
+
* changes the vectors too, so it gets its own file. Undefined (no cache) when
|
|
184
|
+
* the model file cannot be found: there is nothing trustworthy to key it by.
|
|
185
|
+
* The file lives outside the per-org graph DB, so `--rebuild` reuses it; it
|
|
186
|
+
* only grows.
|
|
187
|
+
*/
|
|
188
|
+
export async function embeddingCacheFileFor(dataDir) {
|
|
189
|
+
const sha = await embedModelFingerprint();
|
|
190
|
+
if (!sha)
|
|
191
|
+
return undefined;
|
|
192
|
+
const cap = embedMaxTokens();
|
|
193
|
+
const key = `${resolveEmbedModelId()}@${sha.slice(0, 16)}`;
|
|
194
|
+
return embeddingCachePath(dataDir, cap === 256 ? key : `${key}@${cap}`);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Ruling (a): an embedding failure after graph ready is not an ingest failure.
|
|
198
|
+
* The graph is complete and the unembedded texts stay flagged pending, so the
|
|
199
|
+
* next ingest fills them (find_similar uses text search meanwhile). Warn and
|
|
200
|
+
* leave the exit code alone; live-ingest already printed the cause.
|
|
201
|
+
*/
|
|
202
|
+
export function reportEmbeddingOutcome(result) {
|
|
203
|
+
const e = result.embeddings;
|
|
204
|
+
if (!e?.error)
|
|
205
|
+
return;
|
|
206
|
+
const left = Math.max(e.pending ?? 0, e.failed ?? 0);
|
|
207
|
+
console.warn(`warning: the graph is complete, but ${left} texts were left unembedded (${e.error}); their vectors stay pending and the next ingest fills them`);
|
|
208
|
+
}
|
|
209
|
+
/** Give every built ingest the same embedder (multi-org runs: one worker and
|
|
210
|
+
* one model in memory instead of one per org). The caller closes it. */
|
|
211
|
+
export function shareEmbedder(build, embedder) {
|
|
212
|
+
return async (alias) => {
|
|
213
|
+
const o = await build(alias);
|
|
214
|
+
return o.vectorStore ? { ...o, embedder } : o;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** Offline ingest of an SFDX checkout (plus vlocity/datapacks when present).
|
|
218
|
+
* Reuses buildSingleIngestOpts with a resolver that never contacts an org. */
|
|
219
|
+
export async function buildProjectIngestOpts(opts, logger, deps = {}) {
|
|
220
|
+
const root = path.resolve(String(opts.project));
|
|
221
|
+
if (!existsSync(path.join(root, "sfdx-project.json"))) {
|
|
222
|
+
throw new Error(`--project: ${root} has no sfdx-project.json`);
|
|
223
|
+
}
|
|
224
|
+
const { offlineResolvedOrg, FilesystemMetadataSource } = await import("@ryanstark24/sfgraph-core");
|
|
225
|
+
const alias = opts.org ?? path.basename(root);
|
|
226
|
+
const built = await buildSingleIngestOpts(alias, opts, logger, {
|
|
227
|
+
resolveOrg: (async () => offlineResolvedOrg(root, alias)),
|
|
228
|
+
...(deps.onResolved ? { onResolved: deps.onResolved } : {}),
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
return {
|
|
232
|
+
...built,
|
|
233
|
+
mode: "full",
|
|
234
|
+
metadataSource: await FilesystemMetadataSource.fromProjectRoot(root),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
await built.graphStore.close().catch(() => { });
|
|
239
|
+
throw e;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
114
242
|
/**
|
|
115
243
|
* SQLite uses WAL by default in this codebase, which means alongside
|
|
116
244
|
* `<file>.sqlite` you also get `<file>.sqlite-wal` and `<file>.sqlite-shm`.
|
|
@@ -119,14 +247,91 @@ async function buildSingleIngestOpts(alias, opts, logger, deps) {
|
|
|
119
247
|
* SQLite later reads back into the new DB, corrupting state.
|
|
120
248
|
*/
|
|
121
249
|
const SQLITE_SIDECAR_SUFFIXES = ["-wal", "-shm", "-journal"];
|
|
250
|
+
/**
|
|
251
|
+
* Run a move/delete of the graph file. On Windows a file another process has
|
|
252
|
+
* open (`sfgraph serve`, or an MCP client's server) cannot be renamed or
|
|
253
|
+
* deleted: report that plainly instead of a raw EBUSY/EPERM. Nothing has been
|
|
254
|
+
* moved when this throws, so the existing graph is untouched.
|
|
255
|
+
*/
|
|
256
|
+
export function onGraphFile(dbPath, op) {
|
|
257
|
+
try {
|
|
258
|
+
return op();
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
const code = e.code;
|
|
262
|
+
if (code === "EBUSY" || code === "EPERM" || code === "EACCES") {
|
|
263
|
+
const hint = "Stop `sfgraph serve` and any MCP client using sfgraph (Claude Code, Cursor, ...), then re-run with --rebuild.";
|
|
264
|
+
throw new Error(`rebuild: the graph file is in use by another process (${code}): ${dbPath}. ${hint}`, { cause: e });
|
|
265
|
+
}
|
|
266
|
+
throw e;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Rebuild backups kept per org (each is a full copy of the graph file). */
|
|
270
|
+
export const REBUILD_BACKUPS_KEPT = 2;
|
|
271
|
+
/**
|
|
272
|
+
* Copy the backup's snapshot tables into the rebuilt graph. A failure leaves
|
|
273
|
+
* the new graph without history (the backup still has it) and warns.
|
|
274
|
+
*/
|
|
275
|
+
function carrySnapshots(store, backupPath, orgId) {
|
|
276
|
+
try {
|
|
277
|
+
const r = store.copySnapshotsFrom(backupPath);
|
|
278
|
+
if (r.skipped) {
|
|
279
|
+
console.warn(`REBUILD: snapshot history for ${orgId} not kept: ${r.skipped}`);
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
if (r.copied > 0) {
|
|
283
|
+
console.warn(`REBUILD: kept ${r.copied} snapshot${r.copied === 1 ? "" : "s"} for ${orgId}`);
|
|
284
|
+
}
|
|
285
|
+
return r.copied;
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
console.warn(`REBUILD: snapshot history for ${orgId} not kept (${e.message}); it is still in ${backupPath}`);
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Delete all but the newest {@link REBUILD_BACKUPS_KEPT} rebuild backups of
|
|
294
|
+
* one org (with their sidecars). The names carry an ISO stamp, so name order
|
|
295
|
+
* is time order. The newest backup that still holds snapshot history is always
|
|
296
|
+
* kept too: a rebuild whose snapshot copy failed leaves a backup without
|
|
297
|
+
* history, and two of those in a row must not push the real history out.
|
|
298
|
+
* Best-effort.
|
|
299
|
+
*/
|
|
300
|
+
export async function pruneRebuildBackups(backupDir, orgId) {
|
|
301
|
+
const { readdirSync, unlinkSync } = await import("node:fs");
|
|
302
|
+
const prefix = `${orgId}.rebuild-`;
|
|
303
|
+
let names;
|
|
304
|
+
try {
|
|
305
|
+
names = readdirSync(backupDir).filter((f) => f.startsWith(prefix) && f.endsWith(".sqlite"));
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
const newestFirst = names.sort().reverse();
|
|
311
|
+
// -1 (unreadable) counts as holding history.
|
|
312
|
+
const history = newestFirst.find((f) => snapshotCountInFile(path.join(backupDir, f)) !== 0);
|
|
313
|
+
const old = newestFirst.slice(REBUILD_BACKUPS_KEPT).filter((f) => f !== history);
|
|
314
|
+
for (const f of old) {
|
|
315
|
+
for (const suffix of ["", ...SQLITE_SIDECAR_SUFFIXES]) {
|
|
316
|
+
try {
|
|
317
|
+
unlinkSync(path.join(backupDir, `${f}${suffix}`));
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
/* best-effort */
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return old;
|
|
325
|
+
}
|
|
326
|
+
/** Move (or delete) the org's graph file; returns the backup's path when moved. */
|
|
122
327
|
async function applyRebuild(dbPath, orgId, noBackup, logger) {
|
|
123
328
|
const { existsSync, mkdirSync, renameSync, unlinkSync } = await import("node:fs");
|
|
124
329
|
if (!existsSync(dbPath)) {
|
|
125
330
|
logger.info(`rebuild: no existing graph at ${dbPath}; starting fresh`);
|
|
126
|
-
return;
|
|
331
|
+
return undefined;
|
|
127
332
|
}
|
|
128
333
|
if (noBackup) {
|
|
129
|
-
unlinkSync(dbPath);
|
|
334
|
+
onGraphFile(dbPath, () => unlinkSync(dbPath));
|
|
130
335
|
for (const suffix of SQLITE_SIDECAR_SUFFIXES) {
|
|
131
336
|
const sidecar = `${dbPath}${suffix}`;
|
|
132
337
|
if (existsSync(sidecar)) {
|
|
@@ -139,13 +344,13 @@ async function applyRebuild(dbPath, orgId, noBackup, logger) {
|
|
|
139
344
|
}
|
|
140
345
|
}
|
|
141
346
|
console.warn(`REBUILD: existing graph for ${orgId} DELETED (no-backup); running full sync`);
|
|
142
|
-
return;
|
|
347
|
+
return undefined;
|
|
143
348
|
}
|
|
144
349
|
const backupDir = path.join(getSfgraphPaths().data, "backups");
|
|
145
350
|
mkdirSync(backupDir, { recursive: true });
|
|
146
351
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
147
352
|
const backupPath = path.join(backupDir, `${orgId}.rebuild-${stamp}.sqlite`);
|
|
148
|
-
renameSync(dbPath, backupPath);
|
|
353
|
+
onGraphFile(dbPath, () => renameSync(dbPath, backupPath));
|
|
149
354
|
for (const suffix of SQLITE_SIDECAR_SUFFIXES) {
|
|
150
355
|
const sidecar = `${dbPath}${suffix}`;
|
|
151
356
|
if (existsSync(sidecar)) {
|
|
@@ -165,9 +370,54 @@ async function applyRebuild(dbPath, orgId, noBackup, logger) {
|
|
|
165
370
|
}
|
|
166
371
|
}
|
|
167
372
|
console.warn(`REBUILD: existing graph for ${orgId} moved to ${backupPath}; running full sync`);
|
|
373
|
+
const pruned = await pruneRebuildBackups(backupDir, orgId);
|
|
374
|
+
if (pruned.length > 0) {
|
|
375
|
+
console.warn(`REBUILD: removed ${pruned.length} older backup${pruned.length === 1 ? "" : "s"} for ${orgId} (keeping the newest ${REBUILD_BACKUPS_KEPT})`);
|
|
376
|
+
}
|
|
377
|
+
return backupPath;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Task 11c: one ingest per org at a time (`<dataDir>/<orgId>.ingest.lock`).
|
|
381
|
+
* Taken as soon as an org is resolved, before --rebuild moves its graph;
|
|
382
|
+
* released when the command ends. Re-taking a held org is a no-op.
|
|
383
|
+
* While a lock is held, SIGINT / SIGTERM release it and exit with 128 + signo
|
|
384
|
+
* (a stale lock is only detectable on the same host).
|
|
385
|
+
*/
|
|
386
|
+
export function ingestLocks(o = {}) {
|
|
387
|
+
const held = new Map();
|
|
388
|
+
const signals = ["SIGINT", "SIGTERM"];
|
|
389
|
+
const releaseAll = () => {
|
|
390
|
+
for (const l of held.values())
|
|
391
|
+
l.release();
|
|
392
|
+
held.clear();
|
|
393
|
+
for (const s of signals)
|
|
394
|
+
process.off(s, onSig);
|
|
395
|
+
};
|
|
396
|
+
const onSig = (sig) => {
|
|
397
|
+
releaseAll();
|
|
398
|
+
if (o.onSignal)
|
|
399
|
+
o.onSignal(sig);
|
|
400
|
+
// Exit explicitly: re-raising relies on the default action, which other
|
|
401
|
+
// SIGINT / SIGTERM listeners would suppress (the run would go on unlocked).
|
|
402
|
+
else
|
|
403
|
+
process.exit(128 + (osConstants.signals[sig] ?? 0));
|
|
404
|
+
};
|
|
405
|
+
return {
|
|
406
|
+
take(orgId, alias) {
|
|
407
|
+
if (held.has(orgId))
|
|
408
|
+
return;
|
|
409
|
+
const dir = o.dir ?? getSfgraphPaths().data;
|
|
410
|
+
held.set(orgId, acquireIngestLock({ dir, orgId, orgLabel: alias }));
|
|
411
|
+
if (held.size === 1)
|
|
412
|
+
for (const s of signals)
|
|
413
|
+
process.on(s, onSig);
|
|
414
|
+
},
|
|
415
|
+
releaseAll,
|
|
416
|
+
};
|
|
168
417
|
}
|
|
169
418
|
export async function ingestCmd(opts) {
|
|
170
419
|
const logger = new ConsoleLogger("info");
|
|
420
|
+
const locks = ingestLocks();
|
|
171
421
|
if (opts.embedModel)
|
|
172
422
|
process.env.SFGRAPH_EMBED_MODEL_PATH = opts.embedModel;
|
|
173
423
|
if (opts.embedModelId)
|
|
@@ -188,6 +438,33 @@ export async function ingestCmd(opts) {
|
|
|
188
438
|
poolOverrides.data = opts.dataPool;
|
|
189
439
|
const applied = await configureDefaultPools(poolOverrides);
|
|
190
440
|
logger.info(`ingest: pool concurrency tooling=${applied.tooling} metadata=${applied.metadata} data=${applied.data}`);
|
|
441
|
+
if (opts.delta && !opts.project)
|
|
442
|
+
throw new Error("--delta needs --project");
|
|
443
|
+
// An offline ingest reads every member of the checkout; a label filter has
|
|
444
|
+
// nothing to narrow (final review core M-3).
|
|
445
|
+
if (opts.project && (opts.only || opts.retrySkipped)) {
|
|
446
|
+
throw new Error(`${opts.only ? "--only" : "--retry-skipped"} does not work with --project: a project ingest always reads the whole checkout`);
|
|
447
|
+
}
|
|
448
|
+
if (opts.delta && opts.rebuild)
|
|
449
|
+
throw new Error("--delta and --rebuild are mutually exclusive");
|
|
450
|
+
if (opts.project && opts.delta) {
|
|
451
|
+
await projectDelta(opts, logger, locks.take);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (opts.project) {
|
|
455
|
+
const built = await buildProjectIngestOpts(opts, logger, { onResolved: locks.take });
|
|
456
|
+
const result = await liveIngest(built);
|
|
457
|
+
reportEmbeddingOutcome(result);
|
|
458
|
+
console.log(`ingest --project: ${result.membersProcessed} members, ${result.parseErrors} parse errors -> ${built.preResolved?.orgId}`);
|
|
459
|
+
const dp = built.metadataSource?.dataPackReport;
|
|
460
|
+
if (dp) {
|
|
461
|
+
console.log(`datapacks: ${JSON.stringify(dp.types)} skipped=${dp.skipped.length}`);
|
|
462
|
+
for (const s of dp.skipped.slice(0, 20))
|
|
463
|
+
console.log(` skipped ${s.path}: ${s.reason}`);
|
|
464
|
+
}
|
|
465
|
+
await built.graphStore.close();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
191
468
|
// Determine the alias list (multi-org modes).
|
|
192
469
|
let aliases = null;
|
|
193
470
|
if (opts.all) {
|
|
@@ -219,14 +496,27 @@ export async function ingestCmd(opts) {
|
|
|
219
496
|
}
|
|
220
497
|
if (aliases !== null && aliases.length > 0) {
|
|
221
498
|
const list = aliases;
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
499
|
+
const build = (alias) => buildSingleIngestOpts(alias, opts, logger, { resolveOrg, onResolved: locks.take });
|
|
500
|
+
// One embedding worker (one model, one ORT arena) shared by every org.
|
|
501
|
+
const workerUrl = defaultEmbeddingWorkerUrl();
|
|
502
|
+
const sharedPool = workerUrl ? new EmbeddingPool({ workerUrl }) : null;
|
|
503
|
+
let summary;
|
|
504
|
+
try {
|
|
505
|
+
summary = await multiOrgIngest({
|
|
506
|
+
aliases: list,
|
|
507
|
+
parallel: Boolean(opts.parallel),
|
|
508
|
+
logger,
|
|
509
|
+
buildOpts: sharedPool ? shareEmbedder(build, sharedPool) : build,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
await sharedPool?.close();
|
|
514
|
+
}
|
|
228
515
|
console.log(`\nMulti-org ingest complete (${summary.parallel ? "parallel" : "sequential"}, ${summary.totalElapsedMs}ms):`);
|
|
229
516
|
printResultsTable(summary.entries, opts.mode ?? "auto");
|
|
517
|
+
for (const e of summary.entries)
|
|
518
|
+
if (e.result)
|
|
519
|
+
reportEmbeddingOutcome(e.result);
|
|
230
520
|
const failed = summary.entries.filter((e) => e.status === "error").length;
|
|
231
521
|
if (failed > 0)
|
|
232
522
|
process.exitCode = 1;
|
|
@@ -247,10 +537,14 @@ export async function ingestCmd(opts) {
|
|
|
247
537
|
alias = detected;
|
|
248
538
|
logger.info(`ingest: using default org from sf config: ${alias}`);
|
|
249
539
|
}
|
|
250
|
-
const built = await buildSingleIngestOpts(alias, opts, logger, {
|
|
540
|
+
const built = await buildSingleIngestOpts(alias, opts, logger, {
|
|
541
|
+
resolveOrg,
|
|
542
|
+
onResolved: locks.take,
|
|
543
|
+
});
|
|
251
544
|
const startedAt = Date.now();
|
|
252
545
|
logger.info(`ingest: starting alias=${alias}`);
|
|
253
546
|
const result = await liveIngest(built);
|
|
547
|
+
reportEmbeddingOutcome(result);
|
|
254
548
|
logger.info("ingest: capabilities", result.capabilities);
|
|
255
549
|
logger.info(`ingest: complete mode=${result.mode} members=${result.membersProcessed} deletions=${result.deletions} parseErrors=${result.parseErrors} elapsed=${Date.now() - startedAt}ms`);
|
|
256
550
|
// Post-ingest auto-retry. If a substantial number of sources skipped
|
|
@@ -286,7 +580,12 @@ export async function ingestCmd(opts) {
|
|
|
286
580
|
console.log(" Disable with --no-auto-retry-skipped or SFGRAPH_NO_AUTO_RETRY=1.");
|
|
287
581
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
288
582
|
const retryStartedAt = Date.now();
|
|
289
|
-
|
|
583
|
+
// In-run auto-retry: not an explicit re-run, so the early stop stays on.
|
|
584
|
+
const retryResult = await liveIngest({
|
|
585
|
+
...built,
|
|
586
|
+
onlyLabels: labels,
|
|
587
|
+
explicitOnlyLabels: false,
|
|
588
|
+
});
|
|
290
589
|
logger.info(`ingest: auto-retry complete members=${retryResult.membersProcessed} parseErrors=${retryResult.parseErrors} elapsed=${Date.now() - retryStartedAt}ms`);
|
|
291
590
|
}
|
|
292
591
|
}
|
|
@@ -330,5 +629,49 @@ export async function ingestCmd(opts) {
|
|
|
330
629
|
}
|
|
331
630
|
process.exitCode = 1;
|
|
332
631
|
}
|
|
632
|
+
finally {
|
|
633
|
+
locks.releaseAll();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* `ingest --project <p> --delta`: re-derive only the changed members
|
|
638
|
+
* (core runDeltaIngest prints `delta: +A ~C -D (=U unchanged) in Ns`). A
|
|
639
|
+
* graph without member rows (older sfgraph, interrupted full ingest) gets a
|
|
640
|
+
* full rebuild instead. The caller's lock covers both.
|
|
641
|
+
*/
|
|
642
|
+
async function projectDelta(opts, logger, takeLock) {
|
|
643
|
+
const { runDeltaIngest } = await import("@ryanstark24/sfgraph-core");
|
|
644
|
+
const built = await buildProjectIngestOpts({ ...opts, rebuild: false }, logger, {
|
|
645
|
+
onResolved: takeLock,
|
|
646
|
+
});
|
|
647
|
+
const store = built.graphStore;
|
|
648
|
+
let open = true;
|
|
649
|
+
try {
|
|
650
|
+
const r = await runDeltaIngest({
|
|
651
|
+
...built,
|
|
652
|
+
graphStore: store,
|
|
653
|
+
preResolved: built.preResolved,
|
|
654
|
+
metadataSource: built.metadataSource,
|
|
655
|
+
fullRebuild: async () => {
|
|
656
|
+
await store.close();
|
|
657
|
+
open = false;
|
|
658
|
+
const full = await buildProjectIngestOpts({ ...opts, rebuild: true, delta: false }, logger);
|
|
659
|
+
try {
|
|
660
|
+
const result = await liveIngest(full);
|
|
661
|
+
reportEmbeddingOutcome(result);
|
|
662
|
+
console.log(`ingest --project: ${result.membersProcessed} members, ${result.parseErrors} parse errors -> ${full.preResolved?.orgId}`);
|
|
663
|
+
}
|
|
664
|
+
finally {
|
|
665
|
+
await full.graphStore.close();
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
if (r.result)
|
|
670
|
+
reportEmbeddingOutcome(r.result);
|
|
671
|
+
}
|
|
672
|
+
finally {
|
|
673
|
+
if (open)
|
|
674
|
+
await store.close();
|
|
675
|
+
}
|
|
333
676
|
}
|
|
334
677
|
//# sourceMappingURL=ingest.js.map
|