gitnexus 1.6.8-rc.2 → 1.6.8-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/README.md +20 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/cli/ai-context.js +1 -0
- package/dist/cli/analyze-config.js +40 -0
- package/dist/cli/analyze.d.ts +22 -0
- package/dist/cli/analyze.js +36 -6
- package/dist/cli/clean.d.ts +1 -0
- package/dist/cli/clean.js +43 -1
- package/dist/cli/eval-server.js +43 -0
- package/dist/cli/help-i18n.js +11 -0
- package/dist/cli/i18n/en.d.ts +13 -0
- package/dist/cli/i18n/en.js +13 -0
- package/dist/cli/i18n/resources.d.ts +26 -0
- package/dist/cli/i18n/zh-CN.d.ts +13 -0
- package/dist/cli/i18n/zh-CN.js +13 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/list.js +12 -0
- package/dist/cli/optional-grammars.d.ts +6 -8
- package/dist/cli/optional-grammars.js +8 -11
- package/dist/cli/skill-gen.js +1 -0
- package/dist/cli/status.js +26 -5
- package/dist/cli/tool.d.ts +12 -1
- package/dist/cli/tool.js +41 -1
- package/dist/core/embeddings/embedder.js +4 -0
- package/dist/core/embeddings/embedding-pipeline.js +27 -16
- package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
- package/dist/core/graph/import-cycles.d.ts +10 -0
- package/dist/core/graph/import-cycles.js +103 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
- package/dist/core/group/extractors/http-patterns/java.js +3 -50
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
- package/dist/core/group/extractors/include-extractor.js +7 -7
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +64 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +129 -0
- package/dist/core/ingestion/cfg/collect.d.ts +30 -0
- package/dist/core/ingestion/cfg/collect.js +34 -0
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +97 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +113 -0
- package/dist/core/ingestion/cfg/emit.d.ts +144 -0
- package/dist/core/ingestion/cfg/emit.js +315 -0
- package/dist/core/ingestion/cfg/reaching-defs.d.ts +90 -0
- package/dist/core/ingestion/cfg/reaching-defs.js +364 -0
- package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
- package/dist/core/ingestion/cfg/traversal-result.js +2 -0
- package/dist/core/ingestion/cfg/types.d.ts +231 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +144 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1032 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +584 -0
- package/dist/core/ingestion/language-provider.d.ts +36 -2
- package/dist/core/ingestion/languages/c-cpp.js +11 -4
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
- package/dist/core/ingestion/languages/cpp/captures.js +24 -1
- package/dist/core/ingestion/languages/cpp/query.js +23 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +3 -2
- package/dist/core/ingestion/languages/kotlin.js +5 -1
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +2 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
- package/dist/core/ingestion/model/symbol-table.js +1 -0
- package/dist/core/ingestion/parsing-processor.js +22 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
- package/dist/core/ingestion/pipeline.d.ts +57 -0
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
- package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
- package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
- package/dist/core/ingestion/route-extractors/spring.js +136 -0
- package/dist/core/ingestion/scope-extractor.js +3 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +21 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +207 -0
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/taint/emit.d.ts +124 -0
- package/dist/core/ingestion/taint/emit.js +204 -0
- package/dist/core/ingestion/taint/match.d.ts +153 -0
- package/dist/core/ingestion/taint/match.js +278 -0
- package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
- package/dist/core/ingestion/taint/path-codec.js +190 -0
- package/dist/core/ingestion/taint/propagate.d.ts +216 -0
- package/dist/core/ingestion/taint/propagate.js +664 -0
- package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
- package/dist/core/ingestion/taint/site-safety.js +98 -0
- package/dist/core/ingestion/taint/source-sink-config.d.ts +94 -23
- package/dist/core/ingestion/taint/source-sink-config.js +11 -11
- package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
- package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
- package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
- package/dist/core/ingestion/taint/typescript-model.js +102 -0
- package/dist/core/ingestion/utils/method-props.js +1 -0
- package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
- package/dist/core/ingestion/workers/clone-safety.js +465 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
- package/dist/core/ingestion/workers/parse-worker.js +70 -53
- package/dist/core/ingestion/workers/post-result.d.ts +22 -0
- package/dist/core/ingestion/workers/post-result.js +87 -0
- package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
- package/dist/core/ingestion/workers/result-merge.js +43 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
- package/dist/core/ingestion/workers/worker-pool.js +33 -16
- package/dist/core/lbug/lbug-adapter.d.ts +19 -0
- package/dist/core/lbug/lbug-adapter.js +56 -1
- package/dist/core/run-analyze.d.ts +87 -0
- package/dist/core/run-analyze.js +280 -25
- package/dist/core/tree-sitter/parser-loader.js +5 -4
- package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
- package/dist/core/tree-sitter/vendored-grammars.js +57 -0
- package/dist/mcp/core/embedder.js +4 -0
- package/dist/mcp/local/local-backend.d.ts +86 -2
- package/dist/mcp/local/local-backend.js +657 -27
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +109 -0
- package/dist/server/analyze-launch.d.ts +29 -0
- package/dist/server/analyze-launch.js +137 -0
- package/dist/server/analyze-upload.d.ts +33 -0
- package/dist/server/analyze-upload.js +123 -0
- package/dist/server/analyze-worker-ipc.d.ts +58 -0
- package/dist/server/analyze-worker-ipc.js +16 -0
- package/dist/server/analyze-worker.d.ts +17 -1
- package/dist/server/analyze-worker.js +7 -1
- package/dist/server/api.js +46 -140
- package/dist/server/git-clone.d.ts +1 -0
- package/dist/server/git-clone.js +1 -1
- package/dist/server/middleware.d.ts +11 -0
- package/dist/server/middleware.js +27 -0
- package/dist/server/upload-ingest.d.ts +56 -0
- package/dist/server/upload-ingest.js +276 -0
- package/dist/server/upload-paths.d.ts +31 -0
- package/dist/server/upload-paths.js +51 -0
- package/dist/server/upload-sweep.d.ts +20 -0
- package/dist/server/upload-sweep.js +57 -0
- package/dist/storage/branch-index.d.ts +52 -0
- package/dist/storage/branch-index.js +65 -0
- package/dist/storage/git.d.ts +11 -0
- package/dist/storage/git.js +28 -0
- package/dist/storage/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +32 -10
- package/dist/storage/repo-manager.d.ts +122 -10
- package/dist/storage/repo-manager.js +162 -21
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +42 -8
- package/hooks/claude/gitnexus-hook.cjs +36 -8
- package/hooks/claude/hook-db-lock-probe.cjs +123 -2
- package/package.json +4 -2
- package/scripts/assert-publish-grammar-coverage.cjs +31 -0
- package/scripts/build-tree-sitter-grammars.cjs +16 -10
- package/skills/gitnexus-guide.md +11 -0
- package/vendor/tree-sitter-c/package.json +1 -1
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-kotlin/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -1
- package/web/assets/{agent-CKPMqImC.js → agent-ay4LD70X.js} +1 -1
- package/web/assets/{architectureDiagram-UL44E2DR-D1EXI0zA.js → architectureDiagram-UL44E2DR-Dc-viYhd.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-4rpojOyj.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-BnOy944n.js} +1 -1
- package/web/assets/{classDiagram-KGZ6W3CR-Bv93af_b.js → classDiagram-KGZ6W3CR-Bsgpy98Q.js} +1 -1
- package/web/assets/{classDiagram-v2-72OJOZXJ-CLfEqHUa.js → classDiagram-v2-72OJOZXJ-D5atDGjc.js} +1 -1
- package/web/assets/{diagram-3NCE3AQN-BqAtKUpW.js → diagram-3NCE3AQN-Cz1OEMVi.js} +1 -1
- package/web/assets/{diagram-GF46GFSD-CaBG6n6o.js → diagram-GF46GFSD-CWYwzfP8.js} +1 -1
- package/web/assets/{diagram-QXG6HAR7-CZ-O3rcV.js → diagram-QXG6HAR7-DtBObr8L.js} +1 -1
- package/web/assets/{diagram-WEQXMOUZ-P4lSL4GH.js → diagram-WEQXMOUZ-BUV44Ov_.js} +1 -1
- package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-Ds9s-sRF.js} +1 -1
- package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-CupStHQb.js} +1 -1
- package/web/assets/index-COMMmbxW.css +2 -0
- package/web/assets/{index-CG6q8eTs.js → index-Do8AE5yF.js} +85 -85
- package/web/assets/{infoDiagram-3YFTVSEB-DmLICZx1.js → infoDiagram-3YFTVSEB-DPTXSbgu.js} +1 -1
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CeK-Qhet.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-D4tx6QQ-.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-Bcto_kGa.js} +1 -1
- package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-DPIeBTG6.js} +1 -1
- package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-DDGXjVKO.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-BzDqpcVW.js} +1 -1
- package/web/assets/{stateDiagram-7D4R322I-CB2nABwH.js → stateDiagram-7D4R322I-Bx__als3.js} +1 -1
- package/web/assets/{stateDiagram-v2-36443NZ5-COBGd2RL.js → stateDiagram-v2-36443NZ5-QqOh2yO-.js} +1 -1
- package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-C1eogTOG.js} +1 -1
- package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-D20F4rSW.js} +1 -1
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-BeBHg7ST.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-CdOWeoNI.js} +1 -1
- package/web/index.html +2 -2
- package/scripts/materialize-vendor-grammars.cjs +0 -97
- package/web/assets/index-BKWA-m7o.css +0 -2
package/dist/mcp/resources.js
CHANGED
|
@@ -270,6 +270,7 @@ async function getContextResource(backend, repoName) {
|
|
|
270
270
|
lines.push(' - query: Process-grouped code intelligence (execution flows related to a concept)');
|
|
271
271
|
lines.push(' - context: 360-degree symbol view (categorized refs, process participation)');
|
|
272
272
|
lines.push(' - impact: Blast radius analysis (what breaks if you change a symbol)');
|
|
273
|
+
lines.push(' - explain: Persisted taint findings — source→sink data flows with per-hop variables (requires analyze --pdg)');
|
|
273
274
|
lines.push(' - detect_changes: Git-diff impact analysis (what do your changes affect)');
|
|
274
275
|
lines.push(' - rename: Multi-file coordinated rename with confidence tags');
|
|
275
276
|
lines.push(' - cypher: Raw graph queries');
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -36,4 +36,13 @@ export interface ToolDefinition {
|
|
|
36
36
|
*/
|
|
37
37
|
export declare const LIST_REPOS_DEFAULT_LIMIT = 50;
|
|
38
38
|
export declare const LIST_REPOS_MAX_LIMIT = 200;
|
|
39
|
+
/**
|
|
40
|
+
* Pagination bounds for the `explain` tool (#2083 M3 U6). Findings are sparse
|
|
41
|
+
* and capped per function at analyze time, but a large repo can still
|
|
42
|
+
* accumulate enough TAINTED rows to blow MCP/LLM token limits — the response
|
|
43
|
+
* is page-bounded like `list_repos`. Exported so the backend clamp
|
|
44
|
+
* (`local-backend.ts`) and the schema stay a single source of truth.
|
|
45
|
+
*/
|
|
46
|
+
export declare const EXPLAIN_DEFAULT_LIMIT = 50;
|
|
47
|
+
export declare const EXPLAIN_MAX_LIMIT = 200;
|
|
39
48
|
export declare const GITNEXUS_TOOLS: ToolDefinition[];
|
package/dist/mcp/tools.js
CHANGED
|
@@ -32,6 +32,15 @@ const DESTRUCTIVE_TOOL_ANNOTATIONS = {
|
|
|
32
32
|
*/
|
|
33
33
|
export const LIST_REPOS_DEFAULT_LIMIT = 50;
|
|
34
34
|
export const LIST_REPOS_MAX_LIMIT = 200;
|
|
35
|
+
/**
|
|
36
|
+
* Pagination bounds for the `explain` tool (#2083 M3 U6). Findings are sparse
|
|
37
|
+
* and capped per function at analyze time, but a large repo can still
|
|
38
|
+
* accumulate enough TAINTED rows to blow MCP/LLM token limits — the response
|
|
39
|
+
* is page-bounded like `list_repos`. Exported so the backend clamp
|
|
40
|
+
* (`local-backend.ts`) and the schema stay a single source of truth.
|
|
41
|
+
*/
|
|
42
|
+
export const EXPLAIN_DEFAULT_LIMIT = 50;
|
|
43
|
+
export const EXPLAIN_MAX_LIMIT = 200;
|
|
35
44
|
export const GITNEXUS_TOOLS = [
|
|
36
45
|
{
|
|
37
46
|
name: 'list_repos',
|
|
@@ -277,6 +286,29 @@ Returns: changed symbols, affected processes, and a risk summary.`,
|
|
|
277
286
|
required: [],
|
|
278
287
|
},
|
|
279
288
|
},
|
|
289
|
+
{
|
|
290
|
+
name: 'check',
|
|
291
|
+
description: `Run read-only structural checks against the indexed graph.
|
|
292
|
+
|
|
293
|
+
Currently detects directed cycles between File nodes connected by IMPORTS edges.
|
|
294
|
+
Returns deterministic cycle paths and a cycle count suitable for CI automation.`,
|
|
295
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: {
|
|
299
|
+
cycles: {
|
|
300
|
+
type: 'boolean',
|
|
301
|
+
description: 'Detect circular file imports (default: true).',
|
|
302
|
+
default: true,
|
|
303
|
+
},
|
|
304
|
+
repo: {
|
|
305
|
+
type: 'string',
|
|
306
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
required: [],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
280
312
|
{
|
|
281
313
|
name: 'rename',
|
|
282
314
|
description: `Multi-file coordinated rename using the knowledge graph + text search.
|
|
@@ -439,6 +471,49 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|
|
439
471
|
required: ['target', 'direction'],
|
|
440
472
|
},
|
|
441
473
|
},
|
|
474
|
+
{
|
|
475
|
+
name: 'explain',
|
|
476
|
+
description: `Explain persisted taint findings: intra-procedural source→sink data flows (TAINTED edges) recorded by \`gitnexus analyze --pdg\`.
|
|
477
|
+
|
|
478
|
+
Each finding carries the sink category (command-injection, code-injection, path-traversal, sql-injection, xss), the source/sink lines, and the ordered hop path with the variable carried on each hop (decoded from the persisted path encoding).
|
|
479
|
+
|
|
480
|
+
WHEN TO USE: Security review — "what taint findings exist in this repo / file / function?". Requires the repo to be indexed with \`gitnexus analyze --pdg\`; without that layer the tool returns a clear "no taint layer" note, not an error.
|
|
481
|
+
|
|
482
|
+
ANCHORLESS (no "target"): enumerates all persisted findings for the repo — bounded ("limit", deterministic order), with "totalFindings" and a "truncated" flag.
|
|
483
|
+
ANCHORED ("target" = file path or symbol/function name): full hop detail for that anchor. A file-ish target (contains "/" or an extension) filters by file; a symbol name resolves like context() — ambiguous names return ranked candidates, unknown names return not-found. Symbol anchoring is line-range granular (findings whose source block starts inside the symbol's span).
|
|
484
|
+
|
|
485
|
+
CONTRACT CAVEATS (intra-procedural M3 scope — absent flows are NOT proof of safety):
|
|
486
|
+
- Cross-function flows are not modeled (a flow through a helper function is invisible).
|
|
487
|
+
- Closure/callback flows are invisible in both directions (e.g. arr.forEach(() => sink(y))).
|
|
488
|
+
- Property/field flows are not tracked (obj.x = taint; sink(obj.y) has no chain).
|
|
489
|
+
- Guard-style sanitizers (if (isValid(x))) and implicit/control-dependence flows are not modeled.
|
|
490
|
+
- CommonJS aliasing is partially modeled (require('<literal>') joins resolve; dynamic requires do not).
|
|
491
|
+
- Exception-path over-approximation can produce false-positive noise.
|
|
492
|
+
|
|
493
|
+
Findings are deliberately NOT part of impact()'s traversal or the web schema — explain is the dedicated taint consumer. SANITIZES (kill) edges are queryable via cypher.`,
|
|
494
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
495
|
+
inputSchema: {
|
|
496
|
+
type: 'object',
|
|
497
|
+
properties: {
|
|
498
|
+
target: {
|
|
499
|
+
type: 'string',
|
|
500
|
+
description: 'Optional anchor: a file path (e.g. "src/handlers/run.ts" — suffix match accepted) or a symbol/function name (resolved like context()). Omit to enumerate all findings for the repo.',
|
|
501
|
+
},
|
|
502
|
+
limit: {
|
|
503
|
+
type: 'integer',
|
|
504
|
+
description: `Max findings returned (default: ${EXPLAIN_DEFAULT_LIMIT}, max: ${EXPLAIN_MAX_LIMIT}). "totalFindings" reports the full matched count; "truncated" is set when the page is smaller.`,
|
|
505
|
+
default: EXPLAIN_DEFAULT_LIMIT,
|
|
506
|
+
minimum: 1,
|
|
507
|
+
maximum: EXPLAIN_MAX_LIMIT,
|
|
508
|
+
},
|
|
509
|
+
repo: {
|
|
510
|
+
type: 'string',
|
|
511
|
+
description: 'Repository name or path. Omit if only one repo is indexed.',
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
required: [],
|
|
515
|
+
},
|
|
516
|
+
},
|
|
442
517
|
{
|
|
443
518
|
name: 'route_map',
|
|
444
519
|
description: `Show API route mappings: which components/hooks fetch which API endpoints, and which handler files serve them.
|
|
@@ -560,3 +635,37 @@ WHEN TO USE: After changing group.yaml or re-indexing member repos.`,
|
|
|
560
635
|
},
|
|
561
636
|
},
|
|
562
637
|
];
|
|
638
|
+
/**
|
|
639
|
+
* Per-repo tools that accept an optional `branch` scope (#2106). Single source
|
|
640
|
+
* of truth: the schema property is injected here so it cannot drift from the
|
|
641
|
+
* server-side default in `local-backend.ts` (`resolveRepo(repo, branch)`).
|
|
642
|
+
* `list_repos` and the `group_*` tools are intentionally excluded — they are
|
|
643
|
+
* not single-repo, single-branch operations.
|
|
644
|
+
*/
|
|
645
|
+
const BRANCH_SCOPED_TOOLS = new Set([
|
|
646
|
+
'query',
|
|
647
|
+
'cypher',
|
|
648
|
+
'context',
|
|
649
|
+
'detect_changes',
|
|
650
|
+
'explain',
|
|
651
|
+
'check',
|
|
652
|
+
'impact',
|
|
653
|
+
'rename',
|
|
654
|
+
'route_map',
|
|
655
|
+
'tool_map',
|
|
656
|
+
'shape_check',
|
|
657
|
+
'api_impact',
|
|
658
|
+
]);
|
|
659
|
+
for (const tool of GITNEXUS_TOOLS) {
|
|
660
|
+
if (!BRANCH_SCOPED_TOOLS.has(tool.name))
|
|
661
|
+
continue;
|
|
662
|
+
if (tool.inputSchema.properties.branch)
|
|
663
|
+
continue;
|
|
664
|
+
// Optional — `required` is left unchanged so omitting `branch` keeps today's
|
|
665
|
+
// default/primary-branch behavior. Ignored in group mode (repo starts "@").
|
|
666
|
+
tool.inputSchema.properties.branch = {
|
|
667
|
+
type: 'string',
|
|
668
|
+
description: 'Optional: scope to a specific branch index (multi-branch repos, #2106). ' +
|
|
669
|
+
'Omit for the default/primary branch. Ignored in group mode.',
|
|
670
|
+
};
|
|
671
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared analyze-worker launcher.
|
|
3
|
+
*
|
|
4
|
+
* Forks the analyze worker for an already-resolved repo directory and owns the
|
|
5
|
+
* lock + auto-retry + IPC machinery. Used by both the JSON `/api/analyze` route
|
|
6
|
+
* and the multipart `/api/analyze/upload` route. Dependency-injected (like
|
|
7
|
+
* createAnalyzeUploadHandler) so the seam is testable and api.ts stays smaller.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: this module must live alongside analyze-worker.{ts,js} — the worker
|
|
10
|
+
* path is resolved relative to `import.meta.url`.
|
|
11
|
+
*/
|
|
12
|
+
import type { JobManager } from './analyze-job.js';
|
|
13
|
+
export interface LaunchDeps {
|
|
14
|
+
jobManager: JobManager;
|
|
15
|
+
backend: {
|
|
16
|
+
init: () => Promise<unknown>;
|
|
17
|
+
};
|
|
18
|
+
acquireRepoLock: (key: string) => string | null;
|
|
19
|
+
releaseRepoLock: (key: string) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface LaunchOptions {
|
|
22
|
+
force?: boolean;
|
|
23
|
+
embeddings?: boolean;
|
|
24
|
+
dropEmbeddings?: boolean;
|
|
25
|
+
registryName?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function createLaunchAnalysisWorker(deps: LaunchDeps): (job: {
|
|
28
|
+
id: string;
|
|
29
|
+
}, targetPath: string, opts: LaunchOptions) => void;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared analyze-worker launcher.
|
|
3
|
+
*
|
|
4
|
+
* Forks the analyze worker for an already-resolved repo directory and owns the
|
|
5
|
+
* lock + auto-retry + IPC machinery. Used by both the JSON `/api/analyze` route
|
|
6
|
+
* and the multipart `/api/analyze/upload` route. Dependency-injected (like
|
|
7
|
+
* createAnalyzeUploadHandler) so the seam is testable and api.ts stays smaller.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: this module must live alongside analyze-worker.{ts,js} — the worker
|
|
10
|
+
* path is resolved relative to `import.meta.url`.
|
|
11
|
+
*/
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fork } from 'child_process';
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { getStoragePath } from '../storage/repo-manager.js';
|
|
17
|
+
import { logger } from '../core/logger.js';
|
|
18
|
+
const _require = createRequire(import.meta.url);
|
|
19
|
+
const MAX_WORKER_RETRIES = 2;
|
|
20
|
+
export function createLaunchAnalysisWorker(deps) {
|
|
21
|
+
const { jobManager, backend, acquireRepoLock, releaseRepoLock } = deps;
|
|
22
|
+
return function launchAnalysisWorker(job, targetPath, opts) {
|
|
23
|
+
// Acquire shared repo lock (keyed on storagePath to match embed handler)
|
|
24
|
+
const analyzeLockKey = getStoragePath(targetPath);
|
|
25
|
+
const lockErr = acquireRepoLock(analyzeLockKey);
|
|
26
|
+
if (lockErr) {
|
|
27
|
+
jobManager.updateJob(job.id, { status: 'failed', error: lockErr });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
jobManager.updateJob(job.id, { repoPath: targetPath, status: 'analyzing' });
|
|
31
|
+
// ── Worker fork with auto-retry ──────────────────────────────
|
|
32
|
+
const callerPath = fileURLToPath(import.meta.url);
|
|
33
|
+
const isDev = callerPath.endsWith('.ts');
|
|
34
|
+
const workerFile = isDev ? 'analyze-worker.ts' : 'analyze-worker.js';
|
|
35
|
+
const workerPath = path.join(path.dirname(callerPath), workerFile);
|
|
36
|
+
const tsxHookArgs = isDev
|
|
37
|
+
? ['--import', pathToFileURL(_require.resolve('tsx/esm')).href]
|
|
38
|
+
: [];
|
|
39
|
+
const forkWorker = () => {
|
|
40
|
+
const currentJob = jobManager.getJob(job.id);
|
|
41
|
+
if (!currentJob || currentJob.status === 'complete' || currentJob.status === 'failed')
|
|
42
|
+
return;
|
|
43
|
+
const child = fork(workerPath, [], {
|
|
44
|
+
execArgv: [...tsxHookArgs, '--max-old-space-size=8192'],
|
|
45
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
46
|
+
});
|
|
47
|
+
// Capture stderr for crash diagnostics
|
|
48
|
+
let stderrChunks = '';
|
|
49
|
+
child.stderr?.on('data', (chunk) => {
|
|
50
|
+
stderrChunks += chunk.toString();
|
|
51
|
+
if (stderrChunks.length > 4096)
|
|
52
|
+
stderrChunks = stderrChunks.slice(-4096);
|
|
53
|
+
});
|
|
54
|
+
child.on('message', (msg) => {
|
|
55
|
+
if (msg.type === 'progress') {
|
|
56
|
+
jobManager.updateJob(job.id, {
|
|
57
|
+
status: 'analyzing',
|
|
58
|
+
progress: { phase: msg.phase, percent: msg.percent, message: msg.message },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else if (msg.type === 'complete') {
|
|
62
|
+
releaseRepoLock(analyzeLockKey);
|
|
63
|
+
// Reinitialize backend BEFORE marking complete — ensures the new repo
|
|
64
|
+
// is queryable when the client receives the SSE complete event.
|
|
65
|
+
backend
|
|
66
|
+
.init()
|
|
67
|
+
.then(() => {
|
|
68
|
+
jobManager.updateJob(job.id, { status: 'complete', repoName: msg.result.repoName });
|
|
69
|
+
})
|
|
70
|
+
.catch((err) => {
|
|
71
|
+
logger.error({ err }, 'backend.init() failed after analyze:');
|
|
72
|
+
jobManager.updateJob(job.id, {
|
|
73
|
+
status: 'failed',
|
|
74
|
+
error: 'Server failed to reload after analysis. Try again.',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (msg.type === 'error') {
|
|
79
|
+
releaseRepoLock(analyzeLockKey);
|
|
80
|
+
jobManager.updateJob(job.id, { status: 'failed', error: msg.message });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
child.on('error', (err) => {
|
|
84
|
+
releaseRepoLock(analyzeLockKey);
|
|
85
|
+
jobManager.updateJob(job.id, {
|
|
86
|
+
status: 'failed',
|
|
87
|
+
error: `Worker process error: ${err.message}`,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
child.on('exit', (code) => {
|
|
91
|
+
const j = jobManager.getJob(job.id);
|
|
92
|
+
if (!j || j.status === 'complete' || j.status === 'failed')
|
|
93
|
+
return;
|
|
94
|
+
// Worker crashed — attempt retry if under the limit
|
|
95
|
+
if (j.retryCount < MAX_WORKER_RETRIES) {
|
|
96
|
+
j.retryCount++;
|
|
97
|
+
const delay = 1000 * Math.pow(2, j.retryCount - 1); // 1s, 2s
|
|
98
|
+
const lastErr = stderrChunks.trim().split('\n').pop() || '';
|
|
99
|
+
logger.warn(`Analyze worker crashed (code ${code}), retry ${j.retryCount}/${MAX_WORKER_RETRIES} in ${delay}ms` +
|
|
100
|
+
(lastErr ? `: ${lastErr}` : ''));
|
|
101
|
+
jobManager.updateJob(job.id, {
|
|
102
|
+
status: 'analyzing',
|
|
103
|
+
progress: {
|
|
104
|
+
phase: 'retrying',
|
|
105
|
+
percent: j.progress.percent,
|
|
106
|
+
message: `Worker crashed, retrying (${j.retryCount}/${MAX_WORKER_RETRIES})...`,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
stderrChunks = '';
|
|
110
|
+
setTimeout(forkWorker, delay);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Exhausted retries — permanent failure
|
|
114
|
+
releaseRepoLock(analyzeLockKey);
|
|
115
|
+
jobManager.updateJob(job.id, {
|
|
116
|
+
status: 'failed',
|
|
117
|
+
error: `Worker crashed ${MAX_WORKER_RETRIES + 1} times (code ${code})${stderrChunks ? ': ' + stderrChunks.trim().split('\n').pop() : ''}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Register child for cancellation + timeout tracking
|
|
122
|
+
jobManager.registerChild(job.id, child);
|
|
123
|
+
// Send start command to child
|
|
124
|
+
child.send({
|
|
125
|
+
type: 'start',
|
|
126
|
+
repoPath: targetPath,
|
|
127
|
+
options: {
|
|
128
|
+
force: !!opts.force,
|
|
129
|
+
embeddings: !!opts.embeddings,
|
|
130
|
+
dropEmbeddings: !!opts.dropEmbeddings,
|
|
131
|
+
...(opts.registryName ? { registryName: opts.registryName } : {}),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
forkWorker();
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/analyze/upload — analyze a browser folder upload.
|
|
3
|
+
*
|
|
4
|
+
* Securely ingests the multipart upload into a sandbox (upload-ingest.ts),
|
|
5
|
+
* promotes it to a persistent app-controlled directory, and analyzes it via
|
|
6
|
+
* the same job/worker machinery as a git clone — never returning a server
|
|
7
|
+
* path to the client. Factored as a dependency-injected handler so the job
|
|
8
|
+
* machinery (createJob + the worker launcher) can be mocked in unit tests.
|
|
9
|
+
*/
|
|
10
|
+
import type { Request, Response } from 'express';
|
|
11
|
+
import { ingestUpload } from './upload-ingest.js';
|
|
12
|
+
import type { AnalyzeJob } from './analyze-job.js';
|
|
13
|
+
/** Minimal job shape the handler needs (a subset of the real AnalyzeJob). */
|
|
14
|
+
export type UploadJobRef = Pick<AnalyzeJob, 'id' | 'status'>;
|
|
15
|
+
export interface AnalyzeUploadDeps {
|
|
16
|
+
/** Create (or throw on busy) an analysis job for the given upload dir. */
|
|
17
|
+
createJob: (params: {
|
|
18
|
+
repoPath: string;
|
|
19
|
+
}) => UploadJobRef;
|
|
20
|
+
/** Launch the analyze worker against an already-resolved repo directory. */
|
|
21
|
+
launch: (job: UploadJobRef, targetPath: string, opts: {
|
|
22
|
+
registryName: string;
|
|
23
|
+
}) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Mark a created job failed. The job occupies the single analysis slot from
|
|
26
|
+
* createJob onward, so ANY error before launch must release it — otherwise a
|
|
27
|
+
* leaked non-terminal job wedges all future analyses until restart.
|
|
28
|
+
*/
|
|
29
|
+
failJob: (jobId: string, error: string) => void;
|
|
30
|
+
/** Injectable for tests (defaults to the real ingestUpload). */
|
|
31
|
+
ingest?: typeof ingestUpload;
|
|
32
|
+
}
|
|
33
|
+
export declare function createAnalyzeUploadHandler(deps: AnalyzeUploadDeps): (req: Request, res: Response) => Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/analyze/upload — analyze a browser folder upload.
|
|
3
|
+
*
|
|
4
|
+
* Securely ingests the multipart upload into a sandbox (upload-ingest.ts),
|
|
5
|
+
* promotes it to a persistent app-controlled directory, and analyzes it via
|
|
6
|
+
* the same job/worker machinery as a git clone — never returning a server
|
|
7
|
+
* path to the client. Factored as a dependency-injected handler so the job
|
|
8
|
+
* machinery (createJob + the worker launcher) can be mocked in unit tests.
|
|
9
|
+
*/
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fsp from 'fs/promises';
|
|
12
|
+
import { ingestUpload } from './upload-ingest.js';
|
|
13
|
+
import { UPLOAD_ROOT, getUploadDir, deriveUploadName } from './upload-paths.js';
|
|
14
|
+
import { BadRequestError } from './validation.js';
|
|
15
|
+
/** Cap on collision-suffix attempts when allocating an upload dir name. */
|
|
16
|
+
const MAX_NAME_COLLISION_TRIES = 100;
|
|
17
|
+
/**
|
|
18
|
+
* Find an available upload directory name, appending `-2`, `-3`, … on
|
|
19
|
+
* collision with an existing upload. Bounded to avoid an unbounded scan.
|
|
20
|
+
*/
|
|
21
|
+
async function pickAvailableName(base) {
|
|
22
|
+
for (let i = 0; i < MAX_NAME_COLLISION_TRIES; i++) {
|
|
23
|
+
const name = i === 0 ? base : `${base}-${i + 1}`;
|
|
24
|
+
let dir;
|
|
25
|
+
try {
|
|
26
|
+
dir = getUploadDir(name);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await fsp.access(dir);
|
|
33
|
+
// exists → try the next suffix
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return name; // ENOENT → available
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new BadRequestError(`Could not allocate an upload directory after ${MAX_NAME_COLLISION_TRIES} attempts`, 409);
|
|
40
|
+
}
|
|
41
|
+
export function createAnalyzeUploadHandler(deps) {
|
|
42
|
+
const ingest = deps.ingest ?? ingestUpload;
|
|
43
|
+
return async function handleAnalyzeUploadRequest(req, res) {
|
|
44
|
+
let stageRoot;
|
|
45
|
+
let promotedDir;
|
|
46
|
+
let createdJobId;
|
|
47
|
+
let launched = false;
|
|
48
|
+
try {
|
|
49
|
+
const result = await ingest(req);
|
|
50
|
+
stageRoot = result.stageRoot;
|
|
51
|
+
const baseName = deriveUploadName(result.topLevelName);
|
|
52
|
+
if (!baseName) {
|
|
53
|
+
throw new BadRequestError('Uploaded folder has no usable name');
|
|
54
|
+
}
|
|
55
|
+
// webkitRelativePath prefixes every entry with the picked folder, so the
|
|
56
|
+
// real repo root is stageRoot/<topLevelName>. Validate it is a directory
|
|
57
|
+
// BEFORE taking the single analysis slot — a malformed (non-folder)
|
|
58
|
+
// upload must not be able to occupy the slot.
|
|
59
|
+
const innerRoot = path.join(result.stageRoot, result.topLevelName);
|
|
60
|
+
let innerIsDir = false;
|
|
61
|
+
try {
|
|
62
|
+
innerIsDir = (await fsp.stat(innerRoot)).isDirectory();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
innerIsDir = false;
|
|
66
|
+
}
|
|
67
|
+
if (!innerIsDir) {
|
|
68
|
+
throw new BadRequestError('Upload must be a folder');
|
|
69
|
+
}
|
|
70
|
+
const finalName = await pickAvailableName(baseName);
|
|
71
|
+
const finalDir = getUploadDir(finalName);
|
|
72
|
+
// createJob occupies the single analysis slot (throws 'already in
|
|
73
|
+
// progress' → 409). From here on, ANY error before launch MUST release
|
|
74
|
+
// the slot via failJob in the catch, or the server wedges all analyses.
|
|
75
|
+
let job;
|
|
76
|
+
try {
|
|
77
|
+
job = deps.createJob({ repoPath: finalDir });
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : '';
|
|
81
|
+
if (msg.includes('already in progress')) {
|
|
82
|
+
throw new BadRequestError(msg, 409);
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
createdJobId = job.id;
|
|
87
|
+
// Promote staging → persistent upload dir. Both live under UPLOAD_ROOT's
|
|
88
|
+
// filesystem, so this rename stays atomic (no EXDEV).
|
|
89
|
+
await fsp.mkdir(UPLOAD_ROOT, { recursive: true });
|
|
90
|
+
await fsp.rename(innerRoot, finalDir);
|
|
91
|
+
promotedDir = finalDir;
|
|
92
|
+
const oldStage = stageRoot;
|
|
93
|
+
stageRoot = undefined;
|
|
94
|
+
await fsp.rm(oldStage, { recursive: true, force: true }).catch(() => { });
|
|
95
|
+
// Drop any crafted index the upload may have carried (a `.gitnexus`
|
|
96
|
+
// segment passes containment); the worker will build a fresh one.
|
|
97
|
+
await fsp
|
|
98
|
+
.rm(path.join(finalDir, '.gitnexus'), { recursive: true, force: true })
|
|
99
|
+
.catch(() => { });
|
|
100
|
+
deps.launch(job, finalDir, { registryName: finalName });
|
|
101
|
+
launched = true;
|
|
102
|
+
res.status(202).json({ jobId: job.id, status: job.status });
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// Release the single analysis slot if a job was created but never
|
|
106
|
+
// launched — otherwise the leaked queued job blocks all future analyses.
|
|
107
|
+
if (createdJobId && !launched) {
|
|
108
|
+
deps.failJob(createdJobId, err instanceof Error ? err.message : 'Upload failed');
|
|
109
|
+
}
|
|
110
|
+
if (stageRoot) {
|
|
111
|
+
await fsp.rm(stageRoot, { recursive: true, force: true }).catch(() => { });
|
|
112
|
+
}
|
|
113
|
+
if (promotedDir && !launched) {
|
|
114
|
+
await fsp.rm(promotedDir, { recursive: true, force: true }).catch(() => { });
|
|
115
|
+
}
|
|
116
|
+
if (err instanceof BadRequestError) {
|
|
117
|
+
res.status(err.status).json({ error: err.message });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
res.status(500).json({ error: 'Upload failed' });
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-safe projection of `AnalyzeResult` for the analyze-worker → parent IPC
|
|
3
|
+
* boundary (#2112 boundary audit; #2135).
|
|
4
|
+
*
|
|
5
|
+
* The forked analyze worker (`analyze-worker.ts`) reports completion to the
|
|
6
|
+
* parent over `child_process` IPC, which uses Node's DEFAULT `'json'`
|
|
7
|
+
* serialization — `api.ts` forks the worker with no `serialization:` option, so
|
|
8
|
+
* the channel runs `JSON.stringify`/`JSON.parse`, NOT V8 structured clone.
|
|
9
|
+
*
|
|
10
|
+
* `AnalyzeResult.pipelineResult` is populated on every successful analysis
|
|
11
|
+
* (`run-analyze.ts`) and carries `pipelineResult.graph` — the live
|
|
12
|
+
* `KnowledgeGraph` closure object. Sending the raw result across this channel is
|
|
13
|
+
* wrong three ways:
|
|
14
|
+
* 1. Waste — the graph's `nodes`/`relationships` getters force-materialize the
|
|
15
|
+
* ENTIRE graph into two arrays, then JSON-stringify them, on every analyze.
|
|
16
|
+
* On a large repo (the #2112 scenario) that is a multi-hundred-MB
|
|
17
|
+
* stringify+parse whose result is immediately discarded.
|
|
18
|
+
* 2. Silent corruption — the graph's methods are own function properties;
|
|
19
|
+
* `JSON.stringify` drops them with no error, so a `pipelineResult.graph`
|
|
20
|
+
* that survived the wire is a data-only husk whose `forEachNode(...)` throws
|
|
21
|
+
* "is not a function" far from the cause.
|
|
22
|
+
* 3. Conditional crash — a BigInt or circular reference anywhere in the
|
|
23
|
+
* payload makes `process.send` throw `TypeError` synchronously; the throw is
|
|
24
|
+
* caught in the worker and re-sent as `{type:'error'}`, turning a
|
|
25
|
+
* SUCCESSFUL analysis (DB already written) into a reported FAILURE. This is
|
|
26
|
+
* the #2112 failure family on the server path, and — unlike the parse-worker
|
|
27
|
+
* result boundary — it has no clone-safety net.
|
|
28
|
+
*
|
|
29
|
+
* The parent (`api.ts`) reads only `result.repoName`; `pipelineResult`'s real
|
|
30
|
+
* consumers (CLI skill generation) call `runFullAnalysis` in-process and never
|
|
31
|
+
* cross this fork. So the worker sends an explicit allowlist of the scalar
|
|
32
|
+
* fields, JSON-safe by construction.
|
|
33
|
+
*/
|
|
34
|
+
import type { AnalyzeResult } from '../core/run-analyze.js';
|
|
35
|
+
/**
|
|
36
|
+
* The JSON-safe subset of `AnalyzeResult` that crosses the analyze-worker IPC
|
|
37
|
+
* boundary. A `Pick` allowlist — NOT `Omit<…, 'pipelineResult'>`. With `Pick`
|
|
38
|
+
* the allowlist IS the type, so the projection is exhaustive by construction:
|
|
39
|
+
* `projectAnalyzeResultForIpc`'s return literal must name exactly these keys
|
|
40
|
+
* (omitting one is a compile error), and a new field added to `AnalyzeResult`
|
|
41
|
+
* is simply absent from the wire until it is *deliberately* added here. `Omit`
|
|
42
|
+
* couldn't give that guarantee — it kept every other field, including OPTIONAL
|
|
43
|
+
* ones (e.g. `isPrimaryBranch?`), so an optional non-serializable field could be
|
|
44
|
+
* advertised by the type yet silently dropped by the runtime allowlist.
|
|
45
|
+
*
|
|
46
|
+
* `isPrimaryBranch` is intentionally excluded: the parent (`api.ts`) reads only
|
|
47
|
+
* `repoName`, and nothing consumes `isPrimaryBranch` across this fork (its CLI
|
|
48
|
+
* consumer calls `runFullAnalysis` in-process). Add a field here only when a
|
|
49
|
+
* server-side IPC consumer actually needs it — and only if it is JSON-safe.
|
|
50
|
+
*/
|
|
51
|
+
export type AnalyzeResultIpc = Pick<AnalyzeResult, 'repoName' | 'repoPath' | 'stats' | 'alreadyUpToDate' | 'ftsRepairedOnly' | 'ftsSkipped'>;
|
|
52
|
+
/**
|
|
53
|
+
* Project an `AnalyzeResult` down to the JSON-safe fields the parent consumes,
|
|
54
|
+
* dropping `pipelineResult` (the live `KnowledgeGraph`) and any other field not
|
|
55
|
+
* in the `AnalyzeResultIpc` allowlist. The return literal is exhaustive over
|
|
56
|
+
* `AnalyzeResultIpc` (a missing key is a compile error).
|
|
57
|
+
*/
|
|
58
|
+
export declare function projectAnalyzeResultForIpc(result: AnalyzeResult): AnalyzeResultIpc;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project an `AnalyzeResult` down to the JSON-safe fields the parent consumes,
|
|
3
|
+
* dropping `pipelineResult` (the live `KnowledgeGraph`) and any other field not
|
|
4
|
+
* in the `AnalyzeResultIpc` allowlist. The return literal is exhaustive over
|
|
5
|
+
* `AnalyzeResultIpc` (a missing key is a compile error).
|
|
6
|
+
*/
|
|
7
|
+
export function projectAnalyzeResultForIpc(result) {
|
|
8
|
+
return {
|
|
9
|
+
repoName: result.repoName,
|
|
10
|
+
repoPath: result.repoPath,
|
|
11
|
+
stats: result.stats,
|
|
12
|
+
alreadyUpToDate: result.alreadyUpToDate,
|
|
13
|
+
ftsRepairedOnly: result.ftsRepairedOnly,
|
|
14
|
+
ftsSkipped: result.ftsSkipped,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -10,4 +10,20 @@
|
|
|
10
10
|
* Child -> Parent: { type: 'complete', result: AnalyzeResult }
|
|
11
11
|
* Child -> Parent: { type: 'error', message: string }
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
import { type AnalyzeResultIpc } from './analyze-worker-ipc.js';
|
|
14
|
+
export interface ProgressMessage {
|
|
15
|
+
type: 'progress';
|
|
16
|
+
phase: string;
|
|
17
|
+
percent: number;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CompleteMessage {
|
|
21
|
+
type: 'complete';
|
|
22
|
+
result: AnalyzeResultIpc;
|
|
23
|
+
}
|
|
24
|
+
export interface ErrorMessage {
|
|
25
|
+
type: 'error';
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
/** Child → parent IPC messages. Shared with the parent-side launcher. */
|
|
29
|
+
export type WorkerMessage = ProgressMessage | CompleteMessage | ErrorMessage;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Child -> Parent: { type: 'error', message: string }
|
|
12
12
|
*/
|
|
13
13
|
import { runFullAnalysis } from '../core/run-analyze.js';
|
|
14
|
+
import { projectAnalyzeResultForIpc } from './analyze-worker-ipc.js';
|
|
14
15
|
import { closeLbug } from '../core/lbug/lbug-adapter.js';
|
|
15
16
|
function send(msg) {
|
|
16
17
|
process.send?.(msg);
|
|
@@ -48,7 +49,12 @@ process.on('message', async (msg) => {
|
|
|
48
49
|
send({ type: 'progress', phase: 'log', percent: -1, message });
|
|
49
50
|
},
|
|
50
51
|
});
|
|
51
|
-
|
|
52
|
+
// Send a JSON-safe projection, NOT the raw result: the IPC channel is
|
|
53
|
+
// default-JSON serialization and `result.pipelineResult` carries the live
|
|
54
|
+
// KnowledgeGraph (wasteful to materialize, silently corrupted by JSON, and
|
|
55
|
+
// a BigInt/circular value would throw and mis-report this success as a
|
|
56
|
+
// failure). See analyze-worker-ipc.ts.
|
|
57
|
+
send({ type: 'complete', result: projectAnalyzeResultForIpc(result) });
|
|
52
58
|
}
|
|
53
59
|
catch (err) {
|
|
54
60
|
send({ type: 'error', message: err?.message || 'Analysis failed' });
|