gitnexus 1.6.12-rc.2 → 1.6.12-rc.21
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/_shared/graph/types.d.ts +1 -1
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/language-detection.js +2 -0
- package/dist/_shared/language-detection.js.map +1 -1
- package/dist/_shared/languages.d.ts +1 -0
- package/dist/_shared/languages.d.ts.map +1 -1
- package/dist/_shared/languages.js +1 -0
- package/dist/_shared/languages.js.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +2 -0
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/scope-resolution/language-classification.d.ts +2 -1
- package/dist/_shared/scope-resolution/language-classification.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/language-classification.js +3 -1
- package/dist/_shared/scope-resolution/language-classification.js.map +1 -1
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts +3 -3
- package/dist/_shared/scope-resolution/registries/class-registry.js +3 -3
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/registries/context.js +2 -0
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -1
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/resolve-type-ref.js +2 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
- package/dist/cli/analyze-config.d.ts +2 -2
- package/dist/cli/analyze-config.js +10 -33
- package/dist/cli/doctor.js +6 -4
- package/dist/cli/eval-server.js +13 -1
- package/dist/cli/i18n/en.d.ts +4 -2
- package/dist/cli/i18n/en.js +4 -2
- package/dist/cli/i18n/resources.d.ts +6 -2
- package/dist/cli/i18n/zh-CN.d.ts +2 -0
- package/dist/cli/i18n/zh-CN.js +4 -2
- package/dist/core/analysis-feature-registry.d.ts +1 -1
- package/dist/core/analysis-feature-registry.js +2 -0
- package/dist/core/auto-sync/starter.js +2 -2
- package/dist/core/embeddings/ast-utils.js +8 -3
- package/dist/core/embeddings/chunker.js +67 -10
- package/dist/core/embeddings/embedding-pipeline.d.ts +1 -1
- package/dist/core/embeddings/embedding-pipeline.js +1 -1
- package/dist/core/embeddings/structural-extractor.js +2 -2
- package/dist/core/embeddings/types.d.ts +4 -2
- package/dist/core/embeddings/types.js +18 -0
- package/dist/core/git-ref.d.ts +22 -0
- package/dist/core/git-ref.js +114 -0
- package/dist/core/git-staleness.js +14 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +1 -1
- package/dist/core/group/extractors/graphql-extractor.js +149 -13
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +2 -2
- package/dist/core/ingestion/call-extractors/zig-static-gating.d.ts +5 -8
- package/dist/core/ingestion/content-language-classification.d.ts +10 -0
- package/dist/core/ingestion/content-language-classification.js +30 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +1 -0
- package/dist/core/ingestion/filesystem-walker.js +1 -1
- package/dist/core/ingestion/import-resolvers/node-workspace-packages.js +3 -2
- package/dist/core/ingestion/language-config.d.ts +82 -13
- package/dist/core/ingestion/language-config.js +217 -13
- package/dist/core/ingestion/language-provider.d.ts +88 -1
- package/dist/core/ingestion/languages/index.d.ts +7 -0
- package/dist/core/ingestion/languages/index.js +30 -1
- package/dist/core/ingestion/languages/objective-c/analysis-features.d.ts +10 -0
- package/dist/core/ingestion/languages/objective-c/analysis-features.js +19 -0
- package/dist/core/ingestion/languages/objective-c/compilation-unit-siblings.d.ts +13 -0
- package/dist/core/ingestion/languages/objective-c/compilation-unit-siblings.js +124 -0
- package/dist/core/ingestion/languages/objective-c/facts.d.ts +148 -0
- package/dist/core/ingestion/languages/objective-c/facts.js +1150 -0
- package/dist/core/ingestion/languages/objective-c/import-target.d.ts +11 -0
- package/dist/core/ingestion/languages/objective-c/import-target.js +122 -0
- package/dist/core/ingestion/languages/objective-c/macro-marker-preprocess.d.ts +24 -0
- package/dist/core/ingestion/languages/objective-c/macro-marker-preprocess.js +261 -0
- package/dist/core/ingestion/languages/objective-c/resolution-config.d.ts +41 -0
- package/dist/core/ingestion/languages/objective-c/resolution-config.js +282 -0
- package/dist/core/ingestion/languages/objective-c/scope-resolver.d.ts +2 -0
- package/dist/core/ingestion/languages/objective-c/scope-resolver.js +503 -0
- package/dist/core/ingestion/languages/objective-c.d.ts +2 -0
- package/dist/core/ingestion/languages/objective-c.js +300 -0
- package/dist/core/ingestion/languages/typescript/query.d.ts +31 -0
- package/dist/core/ingestion/languages/typescript/query.js +7 -1
- package/dist/core/ingestion/languages/typescript/tsconfig.js +3 -2
- package/dist/core/ingestion/languages/typescript.js +1 -1
- package/dist/core/ingestion/languages/zig/query.d.ts +26 -0
- package/dist/core/ingestion/languages/zig/query.js +61 -1
- package/dist/core/ingestion/languages/zig/range-binding.d.ts +8 -0
- package/dist/core/ingestion/languages/zig/range-binding.js +24 -0
- package/dist/core/ingestion/languages/zig/scope-resolver.d.ts +3 -2
- package/dist/core/ingestion/languages/zig/scope-resolver.js +18 -5
- package/dist/core/ingestion/languages/zig/this-alias-bindings.d.ts +58 -0
- package/dist/core/ingestion/languages/zig/this-alias-bindings.js +176 -0
- package/dist/core/ingestion/languages/zig/workspace-static-gating.d.ts +8 -0
- package/dist/core/ingestion/languages/zig/workspace-static-gating.js +82 -0
- package/dist/core/ingestion/model/registration-table.d.ts +3 -3
- package/dist/core/ingestion/model/registration-table.js +9 -5
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -1
- package/dist/core/ingestion/model/symbol-table.js +2 -0
- package/dist/core/ingestion/model/type-registry.d.ts +1 -1
- package/dist/core/ingestion/parsing-processor.d.ts +22 -6
- package/dist/core/ingestion/parsing-processor.js +39 -20
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +9 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +307 -96
- package/dist/core/ingestion/pipeline-phases/parse-round-budget.d.ts +42 -0
- package/dist/core/ingestion/pipeline-phases/parse-round-budget.js +50 -0
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +7 -1
- package/dist/core/ingestion/scope-extractor.js +4 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +13 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +2 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +2 -0
- package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +78 -5
- package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +262 -7
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +5 -2
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +10 -1
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +39 -3
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +82 -3
- package/dist/core/ingestion/scope-resolution/value-ref-edges.d.ts +14 -0
- package/dist/core/ingestion/scope-resolution/value-ref-edges.js +14 -0
- package/dist/core/ingestion/tree-sitter-queries.js +2 -0
- package/dist/core/ingestion/utils/symbol-labels.js +2 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +1 -1
- package/dist/core/ingestion/workers/parse-worker.js +63 -3
- package/dist/core/ingestion/workers/worker-pool.d.ts +56 -2
- package/dist/core/ingestion/workers/worker-pool.js +117 -26
- package/dist/core/lbug/csv-generator.js +2 -0
- package/dist/core/lbug/schema.d.ts +3 -1
- package/dist/core/lbug/schema.js +27 -0
- package/dist/core/search/fts-indexes.js +13 -4
- package/dist/core/search/fts-schema.js +2 -0
- package/dist/core/tree-sitter/parser-loader.js +10 -0
- package/dist/core/tree-sitter/vendored-grammars.d.ts +1 -1
- package/dist/core/tree-sitter/vendored-grammars.js +2 -1
- package/dist/mcp/local/local-backend.d.ts +33 -0
- package/dist/mcp/local/local-backend.js +166 -5
- package/dist/mcp/tools.js +6 -4
- package/dist/server/analyze-job.d.ts +19 -1
- package/dist/server/analyze-job.js +14 -3
- package/dist/server/analyze-launch.d.ts +14 -0
- package/dist/server/analyze-launch.js +59 -14
- package/dist/server/analyze-worker-ipc.d.ts +6 -5
- package/dist/server/analyze-worker-ipc.js +4 -0
- package/dist/server/api.js +68 -21
- package/dist/server/git-clone.d.ts +38 -5
- package/dist/server/git-clone.js +216 -38
- package/dist/server/repo-projection.d.ts +88 -0
- package/dist/server/repo-projection.js +53 -0
- package/dist/storage/file-lock.js +5 -3
- package/dist/storage/parse-cache.js +32 -1
- package/dist/storage/parsedfile-store.js +53 -1
- package/dist/storage/v8-sidecar.d.ts +11 -0
- package/dist/storage/v8-sidecar.js +15 -7
- package/dist/utils/process-identity.d.ts +13 -0
- package/dist/utils/process-identity.js +18 -0
- package/package.json +1 -1
- package/scripts/build-tree-sitter-grammars.cjs +4 -3
- package/vendor/tree-sitter-objc/LICENSE +21 -0
- package/vendor/tree-sitter-objc/README.md +18 -0
- package/vendor/tree-sitter-objc/binding.gyp +35 -0
- package/vendor/tree-sitter-objc/bindings/node/binding.cc +19 -0
- package/vendor/tree-sitter-objc/bindings/node/binding_test.js +9 -0
- package/vendor/tree-sitter-objc/bindings/node/index.d.ts +27 -0
- package/vendor/tree-sitter-objc/bindings/node/index.js +11 -0
- package/vendor/tree-sitter-objc/grammar.js +1286 -0
- package/vendor/tree-sitter-objc/package.json +56 -0
- package/vendor/tree-sitter-objc/prebuilds/SHA256SUMS +6 -0
- package/vendor/tree-sitter-objc/prebuilds/darwin-arm64/tree-sitter-objc.node +0 -0
- package/vendor/tree-sitter-objc/prebuilds/darwin-x64/tree-sitter-objc.node +0 -0
- package/vendor/tree-sitter-objc/prebuilds/linux-arm64/tree-sitter-objc.node +0 -0
- package/vendor/tree-sitter-objc/prebuilds/linux-x64/tree-sitter-objc.node +0 -0
- package/vendor/tree-sitter-objc/prebuilds/win32-arm64/tree-sitter-objc.node +0 -0
- package/vendor/tree-sitter-objc/prebuilds/win32-x64/tree-sitter-objc.node +0 -0
- package/vendor/tree-sitter-objc/queries/folds.scm +20 -0
- package/vendor/tree-sitter-objc/queries/highlights.scm +216 -0
- package/vendor/tree-sitter-objc/queries/indents.scm +1 -0
- package/vendor/tree-sitter-objc/queries/injections.scm +10 -0
- package/vendor/tree-sitter-objc/queries/locals.scm +1 -0
- package/vendor/tree-sitter-objc/src/grammar.json +16292 -0
- package/vendor/tree-sitter-objc/src/node-types.json +7524 -0
- package/vendor/tree-sitter-objc/src/parser.c +684023 -0
- package/vendor/tree-sitter-objc/src/tree_sitter/alloc.h +54 -0
- package/vendor/tree-sitter-objc/src/tree_sitter/array.h +291 -0
- package/vendor/tree-sitter-objc/src/tree_sitter/parser.h +266 -0
- package/vendor/tree-sitter-objc/tree-sitter-objc.wasm +0 -0
- package/vendor/tree-sitter-objc/tree-sitter.json +38 -0
- package/web/assets/{agent-BMRqXorA.js → agent-C2n33ANd.js} +1 -1
- package/web/assets/{index-Dj3vQOHK.js → index-CHJoW_P1.js} +18 -18
- package/web/assets/src-jc8Ffy3T.js +1 -0
- package/web/index.html +2 -2
- package/web/assets/src-Df5C1Nz4.js +0 -1
|
@@ -15,6 +15,7 @@ import { fork } from 'child_process';
|
|
|
15
15
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import { canonicalizePath, getStoragePath, INDEX_METADATA_FILE, listRegisteredRepos, registryPathEquals, } from '../storage/repo-manager.js';
|
|
18
|
+
import { BRANCHES_DIR, branchSlug } from '../storage/branch-index.js';
|
|
18
19
|
import { logger } from '../core/logger.js';
|
|
19
20
|
import { autoHeapCapMb } from '../core/ingestion/utils/effective-ram.js';
|
|
20
21
|
import { isTerminalJobStatus } from './analyze-job.js';
|
|
@@ -32,17 +33,6 @@ const MAX_WORKER_RETRIES = 2;
|
|
|
32
33
|
*/
|
|
33
34
|
const FINALIZE_SETTLE_TIMEOUT_MS = 60_000;
|
|
34
35
|
const FINALIZE_SETTLE_POLL_MS = 200;
|
|
35
|
-
/**
|
|
36
|
-
* Resolve once the analyzed repo's index is settled at `storagePath`: the
|
|
37
|
-
* LadybugDB file and metadata both exist AND were (re)written by THIS job
|
|
38
|
-
* (mtime >= jobStartMs — bare existence is not enough, a re-analysis leaves
|
|
39
|
-
* the previous index in place while it works), and no transient WAL/shadow/
|
|
40
|
-
* checkpoint sidecars remain (the worker's native close has finished).
|
|
41
|
-
*
|
|
42
|
-
* Never rejects. Timing out logs and proceeds (pre-gate behavior) rather
|
|
43
|
-
* than failing a job whose analysis genuinely succeeded — e.g. a no-op
|
|
44
|
-
* non-force analyze legitimately rewrites nothing.
|
|
45
|
-
*/
|
|
46
36
|
/**
|
|
47
37
|
* Look up the analyzed repo's registered storage path. The request's
|
|
48
38
|
* user-provided path is used only as a comparison key; the filesystem probes
|
|
@@ -56,7 +46,36 @@ const registeredStoragePath = async (targetPath) => {
|
|
|
56
46
|
const entry = entries.find((e) => registryPathEquals(canonicalizePath(e.path), target));
|
|
57
47
|
return entry?.storagePath ?? null;
|
|
58
48
|
};
|
|
59
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the directory this run's index actually landed in.
|
|
51
|
+
*
|
|
52
|
+
* `registerRepo` always records the FLAT `.gitnexus` as `entry.storagePath`,
|
|
53
|
+
* but a pinned `--branch` run whose label differs from the flat slot's owner
|
|
54
|
+
* writes `lbug`/`gitnexus.json` under `branches/<slug>/` instead. Probing the
|
|
55
|
+
* flat path for such a run watches files it never rewrote, so the gate below
|
|
56
|
+
* would spin to its timeout on a perfectly successful analysis (#3199 review).
|
|
57
|
+
*
|
|
58
|
+
* `isPrimaryBranch` is the worker's own report of `!placement.branch`, so this
|
|
59
|
+
* follows the placement core actually chose rather than recomputing it here
|
|
60
|
+
* (the flat slot's recorded owner can be adopted mid-run, which would make a
|
|
61
|
+
* recomputation race the thing it is trying to observe).
|
|
62
|
+
*/
|
|
63
|
+
const settleDirFor = (registryStoragePath, branch, isPrimaryBranch) => branch && isPrimaryBranch === false
|
|
64
|
+
? path.join(registryStoragePath, BRANCHES_DIR, branchSlug(branch))
|
|
65
|
+
: registryStoragePath;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve once the analyzed repo's index is settled at `storagePath`: the
|
|
68
|
+
* LadybugDB file and metadata both exist AND were (re)written by THIS job
|
|
69
|
+
* (mtime >= jobStartMs — bare existence is not enough, a re-analysis leaves
|
|
70
|
+
* the previous index in place while it works), and no transient WAL/shadow/
|
|
71
|
+
* checkpoint sidecars remain (the worker's native close has finished).
|
|
72
|
+
*
|
|
73
|
+
* Never rejects. Timing out logs and proceeds (pre-gate behavior) rather
|
|
74
|
+
* than failing a job whose analysis genuinely succeeded. The `alreadyUpToDate`
|
|
75
|
+
* fast path never rewrites `lbug` (see `run-analyze.ts`) and skips this wait
|
|
76
|
+
* at the `complete` handler so it does not hold the analyze slot for 60s.
|
|
77
|
+
*/
|
|
78
|
+
const waitForSettledIndex = async (targetPath, jobStartMs, branch, isPrimaryBranch) => {
|
|
60
79
|
const settled = (storagePath) => {
|
|
61
80
|
try {
|
|
62
81
|
const lbugStat = statSync(path.join(storagePath, 'lbug'));
|
|
@@ -74,7 +93,7 @@ const waitForSettledIndex = async (targetPath, jobStartMs) => {
|
|
|
74
93
|
// Re-resolved each round: the worker registers the repo as part of the
|
|
75
94
|
// finalization this gate is waiting out.
|
|
76
95
|
const storagePath = await registeredStoragePath(targetPath);
|
|
77
|
-
if (storagePath && settled(storagePath))
|
|
96
|
+
if (storagePath && settled(settleDirFor(storagePath, branch, isPrimaryBranch)))
|
|
78
97
|
return;
|
|
79
98
|
if (Date.now() > deadline) {
|
|
80
99
|
logger.warn({ targetPath }, 'analyze finalization not visible after timeout; completing job anyway');
|
|
@@ -121,6 +140,13 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
121
140
|
});
|
|
122
141
|
// Capture stderr for crash diagnostics
|
|
123
142
|
let stderrChunks = '';
|
|
143
|
+
// A terminal IPC message (`complete`/`error`) means the worker finished
|
|
144
|
+
// and is now winding down — it calls process.exit(0) ~500ms later. The
|
|
145
|
+
// job is deliberately still non-terminal at that point because the
|
|
146
|
+
// finalization gate is running, so without this flag the exit handler
|
|
147
|
+
// below reads that clean exit as a crash and retries a SUCCESSFUL
|
|
148
|
+
// analysis, three times, before failing it (#3199 review).
|
|
149
|
+
let terminalIpcSeen = false;
|
|
124
150
|
child.stderr?.on('data', (chunk) => {
|
|
125
151
|
stderrChunks += chunk.toString();
|
|
126
152
|
if (stderrChunks.length > 4096)
|
|
@@ -134,6 +160,8 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
134
160
|
const current = jobManager.getJob(job.id);
|
|
135
161
|
if (!current || isTerminalJobStatus(current.status))
|
|
136
162
|
return;
|
|
163
|
+
if (msg.type === 'complete' || msg.type === 'error')
|
|
164
|
+
terminalIpcSeen = true;
|
|
137
165
|
if (msg.type === 'progress') {
|
|
138
166
|
jobManager.updateJob(job.id, {
|
|
139
167
|
status: 'analyzing',
|
|
@@ -151,7 +179,16 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
151
179
|
// below true in practice: the repo is actually queryable when the
|
|
152
180
|
// client receives the SSE complete event, and an index this run knows
|
|
153
181
|
// to be incomplete is never published at all.
|
|
154
|
-
|
|
182
|
+
//
|
|
183
|
+
// alreadyUpToDate never opens LadybugDB and never rewrites `lbug`
|
|
184
|
+
// (run-analyze.ts early-return; CLI notes the same). The mtime gate
|
|
185
|
+
// would spin the full 60s and hold the single global analyze slot.
|
|
186
|
+
// ftsRepairedOnly DOES rewrite `lbug` (initLbug + createSearchFTSIndexes)
|
|
187
|
+
// so it still waits.
|
|
188
|
+
const settle = msg.result.alreadyUpToDate
|
|
189
|
+
? Promise.resolve()
|
|
190
|
+
: waitForSettledIndex(targetPath, jobStartMs, opts.branch, msg.result.isPrimaryBranch);
|
|
191
|
+
settle
|
|
155
192
|
.then(() => closeDbHandle())
|
|
156
193
|
.catch(() => { }) // best-effort: eviction failure must not fail the job
|
|
157
194
|
.then(() => {
|
|
@@ -243,6 +280,13 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
243
280
|
const j = jobManager.getJob(job.id);
|
|
244
281
|
if (!j || isTerminalJobStatus(j.status))
|
|
245
282
|
return;
|
|
283
|
+
// The worker already reported a terminal outcome; this exit is it
|
|
284
|
+
// winding down, not dying. The job is still non-terminal only because
|
|
285
|
+
// the finalization gate above has not resolved yet, and that gate owns
|
|
286
|
+
// the outcome — retrying here would fork a second worker over a
|
|
287
|
+
// finished, successful analysis.
|
|
288
|
+
if (terminalIpcSeen)
|
|
289
|
+
return;
|
|
246
290
|
// Worker crashed — attempt retry if under the limit
|
|
247
291
|
if (j.retryCount < MAX_WORKER_RETRIES) {
|
|
248
292
|
j.retryCount++;
|
|
@@ -283,6 +327,7 @@ export function createLaunchAnalysisWorker(deps) {
|
|
|
283
327
|
...(opts.springActuatorPath ? { springActuatorPath: opts.springActuatorPath } : {}),
|
|
284
328
|
...(opts.asyncApiSpecPath ? { asyncApiSpecPath: opts.asyncApiSpecPath } : {}),
|
|
285
329
|
...(opts.registryName ? { registryName: opts.registryName } : {}),
|
|
330
|
+
...(opts.branch ? { branch: opts.branch } : {}),
|
|
286
331
|
},
|
|
287
332
|
});
|
|
288
333
|
};
|
|
@@ -43,12 +43,13 @@ import type { AnalyzeResult } from '../core/run-analyze.js';
|
|
|
43
43
|
* ones (e.g. `isPrimaryBranch?`), so an optional non-serializable field could be
|
|
44
44
|
* advertised by the type yet silently dropped by the runtime allowlist.
|
|
45
45
|
*
|
|
46
|
-
* `isPrimaryBranch`
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* `isPrimaryBranch` IS on the wire, under exactly the rule this comment used to
|
|
47
|
+
* cite for excluding it: a server-side consumer now needs it. `analyze-launch.ts`
|
|
48
|
+
* settles the index the run actually wrote, and only the worker knows whether
|
|
49
|
+
* core chose the flat slot or a `branches/<slug>/` sub-slot. It is a boolean, so
|
|
50
|
+
* it is JSON-safe by construction.
|
|
50
51
|
*/
|
|
51
|
-
export type AnalyzeResultIpc = Pick<AnalyzeResult, 'repoName' | 'repoPath' | 'stats' | 'alreadyUpToDate' | 'ftsRepairedOnly' | 'ftsSkipped' | 'graphWriteCollapsed'>;
|
|
52
|
+
export type AnalyzeResultIpc = Pick<AnalyzeResult, 'repoName' | 'repoPath' | 'stats' | 'alreadyUpToDate' | 'ftsRepairedOnly' | 'ftsSkipped' | 'graphWriteCollapsed' | 'isPrimaryBranch'>;
|
|
52
53
|
/**
|
|
53
54
|
* Project an `AnalyzeResult` down to the JSON-safe fields the parent consumes,
|
|
54
55
|
* dropping `pipelineResult` (the live `KnowledgeGraph`) and any other field not
|
|
@@ -16,5 +16,9 @@ export function projectAnalyzeResultForIpc(result) {
|
|
|
16
16
|
// outcome the CLI does; without it the worker reports a clean `complete`
|
|
17
17
|
// for a run whose edges are mostly missing.
|
|
18
18
|
graphWriteCollapsed: result.graphWriteCollapsed,
|
|
19
|
+
// Tells the parent which slot this run wrote — the flat `.gitnexus` or a
|
|
20
|
+
// `branches/<slug>/` sub-slot — so its finalization gate watches the files
|
|
21
|
+
// this job actually rewrote (#3199 review).
|
|
22
|
+
isPrimaryBranch: result.isPrimaryBranch,
|
|
19
23
|
};
|
|
20
24
|
}
|
package/dist/server/api.js
CHANGED
|
@@ -29,8 +29,14 @@ import { measurePersistedEmbeddingCount, persistedEmbeddingCountOrUndefined, } f
|
|
|
29
29
|
import { assertString, BadRequestError, createRouteLimiter } from './validation.js';
|
|
30
30
|
import { parseGrepQuery, GREP_TIME_BUDGET_MS } from './grep-params.js';
|
|
31
31
|
import { runGrepScanInWorker } from './grep-scan.js';
|
|
32
|
-
import { extractWebRepoName, getCloneDir, cloneOrPull, warnIfInsecureAzureConfig, GITHUB_TOKEN_HOSTS, } from './git-clone.js';
|
|
32
|
+
import { analyzeCloneOptions, extractWebRepoName, getCloneDir, cloneOrPull, warnIfInsecureAzureConfig, GITHUB_TOKEN_HOSTS, } from './git-clone.js';
|
|
33
33
|
import { createAnalyzeUploadHandler } from './analyze-upload.js';
|
|
34
|
+
import { checkStalenessAsync } from '../core/git-staleness.js';
|
|
35
|
+
import { projectRepoDetail, projectRepoListEntry, resolveLastCommit } from './repo-projection.js';
|
|
36
|
+
// Shared with the CLI's `--branch` (via the analyze-config wrapper) so both
|
|
37
|
+
// entry points accept the same refs. Imported from core — not cli/ — so
|
|
38
|
+
// createServer does not close a cycle with cli/serve.ts.
|
|
39
|
+
import { InvalidBranchError, validateBranchName } from '../core/git-ref.js';
|
|
34
40
|
import { assertServeAuthForPublicOrigin, createPublicOriginMatcher, createWriteOriginGuard, logOriginPolicy, PUBLIC_ORIGIN_ENV, resolveTrustProxy, TRUST_PROXY_ENV, warnIfRateLimitKeysCollapse, } from './middleware.js';
|
|
35
41
|
import { createLaunchAnalysisWorker } from './analyze-launch.js';
|
|
36
42
|
import { UPLOAD_ROOT } from './upload-paths.js';
|
|
@@ -805,17 +811,19 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
805
811
|
res.json(buildServerInfo(updateController.snapshot()));
|
|
806
812
|
});
|
|
807
813
|
// List all registered repos
|
|
808
|
-
|
|
814
|
+
// Rate-limited (CodeQL js/missing-rate-limiting) because this route now spawns
|
|
815
|
+
// one `git rev-list` per registered repo to answer freshness: an unauthenticated
|
|
816
|
+
// GET that costs N subprocesses is worth the same 60 rpm/IP ceiling `/api/repo`
|
|
817
|
+
// already carries. Web callers hit this on connect/switch, never in a loop.
|
|
818
|
+
app.get('/api/repos', createRouteLimiter(), async (_req, res) => {
|
|
809
819
|
try {
|
|
810
820
|
const repos = await listRegisteredRepos();
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
stats: r.stats,
|
|
818
|
-
})));
|
|
821
|
+
// Checked in parallel, for the reason `list_repos` already does it that
|
|
822
|
+
// way: each check spawns an async `git rev-list`, and the sequential
|
|
823
|
+
// variant took ~50s across 200 repos (#1363). Projecting inside the map
|
|
824
|
+
// keeps the entry and its own check together — an index-matched second
|
|
825
|
+
// array is the shape that silently mispairs them if either is reordered.
|
|
826
|
+
res.json(await Promise.all(repos.map(async (r) => projectRepoListEntry(r, await checkStalenessAsync(r.path, r.lastCommit)))));
|
|
819
827
|
}
|
|
820
828
|
catch (err) {
|
|
821
829
|
res.status(500).json({ error: err.message || 'Failed to list repos' });
|
|
@@ -841,12 +849,8 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
841
849
|
return;
|
|
842
850
|
}
|
|
843
851
|
const meta = await loadMeta(entry.storagePath);
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
repoPath: entry.path,
|
|
847
|
-
indexedAt: meta?.indexedAt ?? entry.indexedAt,
|
|
848
|
-
stats: meta?.stats ?? entry.stats ?? {},
|
|
849
|
-
});
|
|
852
|
+
const staleness = await checkStalenessAsync(entry.path, resolveLastCommit(entry, meta));
|
|
853
|
+
res.json(projectRepoDetail(entry, meta, staleness));
|
|
850
854
|
}
|
|
851
855
|
catch (err) {
|
|
852
856
|
res.status(500).json({ error: err.message || 'Failed to get repo info' });
|
|
@@ -1264,7 +1268,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1264
1268
|
// POST /api/analyze — start a new analysis job
|
|
1265
1269
|
app.post('/api/analyze', createRouteLimiter({ limit: 10 }), requireTrustedOrigin, async (req, res) => {
|
|
1266
1270
|
try {
|
|
1267
|
-
const { url: repoUrl, path: repoLocalPath, force, embeddings, dropEmbeddings, springActuatorPath, asyncApiSpecPath, token: repoToken, } = req.body;
|
|
1271
|
+
const { url: repoUrl, path: repoLocalPath, force, embeddings, dropEmbeddings, springActuatorPath, asyncApiSpecPath, token: repoToken, branch: repoBranch, } = req.body;
|
|
1268
1272
|
// Input type validation
|
|
1269
1273
|
if (repoUrl !== undefined && typeof repoUrl !== 'string') {
|
|
1270
1274
|
res.status(400).json({ error: '"url" must be a string' });
|
|
@@ -1288,6 +1292,27 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1288
1292
|
res.status(400).json({ error: 'Provide "url" (git URL) or "path" (local path)' });
|
|
1289
1293
|
return;
|
|
1290
1294
|
}
|
|
1295
|
+
// Branch: optional index-branch selector, validated with the same rules
|
|
1296
|
+
// as the CLI's `--branch` so both entry points accept the same refs.
|
|
1297
|
+
// Rejecting here (rather than letting the clone fail) keeps a malformed
|
|
1298
|
+
// ref from ever reaching `git`.
|
|
1299
|
+
if (repoBranch !== undefined && typeof repoBranch !== 'string') {
|
|
1300
|
+
res.status(400).json({ error: '"branch" must be a string' });
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
let analyzeBranch;
|
|
1304
|
+
if (repoBranch !== undefined) {
|
|
1305
|
+
try {
|
|
1306
|
+
analyzeBranch = validateBranchName(repoBranch, '"branch"');
|
|
1307
|
+
}
|
|
1308
|
+
catch (err) {
|
|
1309
|
+
if (err instanceof InvalidBranchError) {
|
|
1310
|
+
res.status(400).json({ error: err.message });
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
throw err;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1291
1316
|
// Token: optional, restricted charset to prevent header smuggling
|
|
1292
1317
|
// (CRLF), bound length, and bound to github.com (see validateAnalyzeToken).
|
|
1293
1318
|
const tokenError = validateAnalyzeToken(repoToken, repoUrl);
|
|
@@ -1311,7 +1336,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1311
1336
|
res.status(400).json({ error: '"path" must be an absolute path' });
|
|
1312
1337
|
return;
|
|
1313
1338
|
}
|
|
1314
|
-
const job = jobManager.createJob({
|
|
1339
|
+
const job = jobManager.createJob({
|
|
1340
|
+
repoUrl,
|
|
1341
|
+
repoPath: repoLocalPath,
|
|
1342
|
+
branch: analyzeBranch,
|
|
1343
|
+
});
|
|
1315
1344
|
// If job was already running (dedup), just return its id. The token is
|
|
1316
1345
|
// not part of the dedup identity and is never stored on the job, so a
|
|
1317
1346
|
// token on THIS request had no effect — the existing job already
|
|
@@ -1337,17 +1366,21 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1337
1366
|
// Clone if URL provided
|
|
1338
1367
|
if (repoUrl && !repoLocalPath) {
|
|
1339
1368
|
const repoName = extractWebRepoName(repoUrl);
|
|
1340
|
-
|
|
1369
|
+
// Branch-pinned runs get their own clone dir, so they never share
|
|
1370
|
+
// a working tree with the unpinned one (see getCloneDir).
|
|
1371
|
+
targetPath = getCloneDir(repoName, analyzeBranch);
|
|
1341
1372
|
jobManager.updateJob(job.id, {
|
|
1342
1373
|
status: 'cloning',
|
|
1343
|
-
|
|
1374
|
+
// url+branch: same value as registryName (dir basename), not
|
|
1375
|
+
// the extractWebRepoName stem used only as getCloneDir's first arg.
|
|
1376
|
+
repoName: analyzeBranch ? path.basename(targetPath) : repoName,
|
|
1344
1377
|
progress: { phase: 'cloning', percent: 0, message: `Cloning ${repoUrl}...` },
|
|
1345
1378
|
});
|
|
1346
1379
|
await cloneOrPull(repoUrl, targetPath, (progress) => {
|
|
1347
1380
|
jobManager.updateJob(job.id, {
|
|
1348
1381
|
progress: { phase: progress.phase, percent: 5, message: progress.message },
|
|
1349
1382
|
});
|
|
1350
|
-
}, repoToken
|
|
1383
|
+
}, analyzeCloneOptions(repoToken, analyzeBranch));
|
|
1351
1384
|
}
|
|
1352
1385
|
if (!targetPath) {
|
|
1353
1386
|
throw new Error('No target path resolved');
|
|
@@ -1358,6 +1391,20 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1358
1391
|
dropEmbeddings,
|
|
1359
1392
|
springActuatorPath,
|
|
1360
1393
|
asyncApiSpecPath,
|
|
1394
|
+
branch: analyzeBranch,
|
|
1395
|
+
// Both clone dirs share an `origin`, so the name `registerRepo`
|
|
1396
|
+
// infers from the remote would be identical and the second one
|
|
1397
|
+
// would fail with RegistryNameCollisionError. Register the pinned
|
|
1398
|
+
// clone under its directory name instead: unique per branch, and
|
|
1399
|
+
// it re-derives through getCloneDir for DELETE /api/repo.
|
|
1400
|
+
//
|
|
1401
|
+
// Gated on the SAME condition as the clone above: when a caller
|
|
1402
|
+
// supplies both `url` and `path` nothing is cloned, and renaming
|
|
1403
|
+
// the operator's own local repo after its directory would be a
|
|
1404
|
+
// surprise unrelated to branch pinning.
|
|
1405
|
+
...(analyzeBranch && repoUrl && !repoLocalPath
|
|
1406
|
+
? { registryName: path.basename(targetPath) }
|
|
1407
|
+
: {}),
|
|
1361
1408
|
});
|
|
1362
1409
|
}
|
|
1363
1410
|
catch (err) {
|
|
@@ -25,12 +25,25 @@ export declare function extractRepoName(url: string): string;
|
|
|
25
25
|
* `extractRepoName()` strict for internal/security-sensitive callers.
|
|
26
26
|
*/
|
|
27
27
|
export declare function extractWebRepoName(url: string): string;
|
|
28
|
-
/** Get the clone target directory for a repo name. */
|
|
29
|
-
export declare function getCloneDir(repoName: string): string;
|
|
28
|
+
/** Get the clone target directory for a repo name, optionally pinned to a branch. */
|
|
29
|
+
export declare function getCloneDir(repoName: string, branch?: string): string;
|
|
30
30
|
export interface CloneProgress {
|
|
31
31
|
phase: 'cloning' | 'pulling';
|
|
32
32
|
message: string;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the `cloneOrPull` options for an `/api/analyze` request.
|
|
36
|
+
*
|
|
37
|
+
* Extracted from the route so the token/branch combination is unit-testable.
|
|
38
|
+
* Inline, the branch-only case was the one nothing asserted: every existing
|
|
39
|
+
* test still passed if `branch` were dropped whenever no token was supplied —
|
|
40
|
+
* i.e. silently cloning the default branch for every public URL, which is the
|
|
41
|
+
* exact behavior #3198 is about (#3199 review).
|
|
42
|
+
*
|
|
43
|
+
* Returns `undefined` rather than `{}` when neither is set, because that is
|
|
44
|
+
* what `cloneOrPull` treats as "no options" at its own call sites.
|
|
45
|
+
*/
|
|
46
|
+
export declare function analyzeCloneOptions(token?: string, branch?: string): Pick<CloneOrPullOptions, 'token' | 'branch'> | undefined;
|
|
34
47
|
export interface CloneOrPullOptions {
|
|
35
48
|
token?: string;
|
|
36
49
|
allowedCloneRoot?: string;
|
|
@@ -119,9 +132,29 @@ export declare function getRemoteOriginUrl(cwd: string, timeoutMs?: number): Pro
|
|
|
119
132
|
export declare function assertRemoteMatchesRequestedUrl(targetDir: string, requestedUrl: string, timeoutMs?: number): Promise<void>;
|
|
120
133
|
/**
|
|
121
134
|
* Clone or pull a git repository.
|
|
122
|
-
*
|
|
123
|
-
* If targetDir
|
|
124
|
-
*
|
|
135
|
+
*
|
|
136
|
+
* If targetDir doesn't exist: git clone --depth 1, adding `--branch <branch>`
|
|
137
|
+
* when one is requested.
|
|
138
|
+
*
|
|
139
|
+
* If targetDir exists with .git, its remote.origin is verified against the
|
|
140
|
+
* requested URL first, and then the branch decides the update:
|
|
141
|
+
* - no `options.branch`: git pull --ff-only, which updates the current
|
|
142
|
+
* branch in place via its configured upstream. Nothing moves, so no
|
|
143
|
+
* dirty-tree check applies.
|
|
144
|
+
* - a `options.branch` that is ALREADY the current named branch: restore
|
|
145
|
+
* GitNexus overlays, then fetch via
|
|
146
|
+
* `+refs/heads/<branch>:refs/remotes/origin/<branch>` and
|
|
147
|
+
* `checkout -B <branch> origin/<branch>` (shallow clones cannot
|
|
148
|
+
* `merge --ff-only` across a 2+ commit move). Never a raw
|
|
149
|
+
* `origin <branch>` pull refspec. No porcelain refuse.
|
|
150
|
+
* - a detached HEAD whose SHA already matches the requested ref (tag /
|
|
151
|
+
* SHA pin): restore overlays only. Do not fetch/merge — a same-named
|
|
152
|
+
* branch could otherwise fast-forward the pin past the tag.
|
|
153
|
+
* - a `options.branch` that DIFFERS from the current pin: fetch that ref,
|
|
154
|
+
* then `checkout -B <branch> origin/<branch>` — so the requested branch,
|
|
155
|
+
* not the one already checked out, is what ends up in the working tree.
|
|
156
|
+
* This is the switching case, and it refuses a dirty tree unless
|
|
157
|
+
* `overwriteLocalChanges` is set.
|
|
125
158
|
*
|
|
126
159
|
* Security:
|
|
127
160
|
* - targetDir must resolve inside CLONE_ROOT (~/.gitnexus/repos/). The
|