homegraph 1.2.0 → 1.5.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/README.md +48 -11
- package/dist/bin/homegraph.js +298 -72
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +117 -11
- package/dist/context/index.js.map +1 -1
- package/dist/db/index.d.ts +207 -6
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +461 -20
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +19 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +157 -3
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +426 -9
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +32 -1
- package/dist/db/sqlite-adapter.d.ts +24 -17
- package/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.js +292 -36
- package/dist/db/sqlite-adapter.js.map +1 -1
- package/dist/db/wal-valve.d.ts +119 -0
- package/dist/db/wal-valve.d.ts.map +1 -0
- package/dist/db/wal-valve.js +290 -0
- package/dist/db/wal-valve.js.map +1 -0
- package/dist/directory.d.ts +9 -5
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +210 -19
- package/dist/directory.js.map +1 -1
- package/dist/extraction/arkts-batch-worker.js +3 -2
- package/dist/extraction/arkts-batch-worker.js.map +1 -1
- package/dist/extraction/cfml-extractor.d.ts +107 -0
- package/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/dist/extraction/cfml-extractor.js +494 -0
- package/dist/extraction/cfml-extractor.js.map +1 -0
- package/dist/extraction/default-ignore.d.ts +39 -0
- package/dist/extraction/default-ignore.d.ts.map +1 -0
- package/dist/extraction/default-ignore.js +221 -0
- package/dist/extraction/default-ignore.js.map +1 -0
- package/dist/extraction/grammars.d.ts +25 -1
- package/dist/extraction/grammars.d.ts.map +1 -1
- package/dist/extraction/grammars.js +177 -24
- package/dist/extraction/grammars.js.map +1 -1
- package/dist/extraction/index.d.ts +52 -14
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +521 -314
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +90 -6
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +1579 -128
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +42 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +369 -4
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/cfquery.d.ts +12 -0
- package/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/dist/extraction/languages/cfquery.js +28 -0
- package/dist/extraction/languages/cfquery.js.map +1 -0
- package/dist/extraction/languages/cfscript.d.ts +3 -0
- package/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/dist/extraction/languages/cfscript.js +73 -0
- package/dist/extraction/languages/cfscript.js.map +1 -0
- package/dist/extraction/languages/cobol.d.ts +33 -0
- package/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/dist/extraction/languages/cobol.js +499 -0
- package/dist/extraction/languages/cobol.js.map +1 -0
- package/dist/extraction/languages/erlang.d.ts +3 -0
- package/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/dist/extraction/languages/erlang.js +350 -0
- package/dist/extraction/languages/erlang.js.map +1 -0
- package/dist/extraction/languages/index.d.ts.map +1 -1
- package/dist/extraction/languages/index.js +16 -0
- package/dist/extraction/languages/index.js.map +1 -1
- package/dist/extraction/languages/nix.d.ts +3 -0
- package/dist/extraction/languages/nix.d.ts.map +1 -0
- package/dist/extraction/languages/nix.js +294 -0
- package/dist/extraction/languages/nix.js.map +1 -0
- package/dist/extraction/languages/solidity.d.ts +3 -0
- package/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/dist/extraction/languages/solidity.js +293 -0
- package/dist/extraction/languages/solidity.js.map +1 -0
- package/dist/extraction/languages/terraform.d.ts +3 -0
- package/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/dist/extraction/languages/terraform.js +641 -0
- package/dist/extraction/languages/terraform.js.map +1 -0
- package/dist/extraction/languages/vbnet.d.ts +11 -0
- package/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/dist/extraction/languages/vbnet.js +141 -0
- package/dist/extraction/languages/vbnet.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +30 -10
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/dist/extraction/mybatis-extractor.js +140 -38
- package/dist/extraction/mybatis-extractor.js.map +1 -1
- package/dist/extraction/parse-pool.d.ts +27 -1
- package/dist/extraction/parse-pool.d.ts.map +1 -1
- package/dist/extraction/parse-pool.js +75 -7
- package/dist/extraction/parse-pool.js.map +1 -1
- package/dist/extraction/parse-worker.js +9 -2
- package/dist/extraction/parse-worker.js.map +1 -1
- package/dist/extraction/store-worker.d.ts +20 -0
- package/dist/extraction/store-worker.d.ts.map +1 -0
- package/dist/extraction/store-worker.js +102 -0
- package/dist/extraction/store-worker.js.map +1 -0
- package/dist/extraction/store-writer.d.ts +48 -0
- package/dist/extraction/store-writer.d.ts.map +1 -0
- package/dist/extraction/store-writer.js +167 -0
- package/dist/extraction/store-writer.js.map +1 -0
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/dist/extraction/tree-sitter-helpers.js +1 -0
- package/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/dist/extraction/tree-sitter-types.d.ts +3 -1
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +38 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +801 -10
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +14 -4
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/dist/extraction/wasm-runtime-flags.js +55 -7
- package/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +1 -1
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +74 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +436 -55
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts +42 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +97 -2
- package/dist/installer/index.js.map +1 -1
- package/dist/mcp/daemon.d.ts +25 -3
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +62 -8
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/dist/mcp/dynamic-boundaries.js +2 -1
- package/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/dist/mcp/early-ppid.d.ts +26 -0
- package/dist/mcp/early-ppid.d.ts.map +1 -0
- package/dist/mcp/early-ppid.js +29 -0
- package/dist/mcp/early-ppid.js.map +1 -0
- package/dist/mcp/engine.d.ts +4 -5
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +27 -1
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +41 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.d.ts +18 -1
- package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/dist/mcp/liveness-watchdog.js +73 -13
- package/dist/mcp/liveness-watchdog.js.map +1 -1
- package/dist/mcp/memory-budget.d.ts +30 -0
- package/dist/mcp/memory-budget.d.ts.map +1 -0
- package/dist/mcp/memory-budget.js +123 -0
- package/dist/mcp/memory-budget.js.map +1 -0
- package/dist/mcp/proxy.d.ts.map +1 -1
- package/dist/mcp/proxy.js +88 -4
- package/dist/mcp/proxy.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +35 -4
- package/dist/mcp/query-cache.d.ts.map +1 -1
- package/dist/mcp/query-cache.js +89 -19
- package/dist/mcp/query-cache.js.map +1 -1
- package/dist/mcp/query-pool.d.ts +46 -12
- package/dist/mcp/query-pool.d.ts.map +1 -1
- package/dist/mcp/query-pool.js +100 -23
- package/dist/mcp/query-pool.js.map +1 -1
- package/dist/mcp/query-worker.d.ts +2 -2
- package/dist/mcp/query-worker.js +2 -2
- package/dist/mcp/server-instructions.d.ts +4 -27
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +39 -84
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/session.d.ts +14 -0
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +28 -1
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/startup-handshake.d.ts +44 -0
- package/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/dist/mcp/startup-handshake.js +73 -0
- package/dist/mcp/startup-handshake.js.map +1 -0
- package/dist/mcp/tools.d.ts +104 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +2432 -217
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/transport.js.map +1 -1
- package/dist/project-config.d.ts +38 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +101 -2
- package/dist/project-config.js.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +178 -146
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +54 -10
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +1299 -257
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/cooperative-yield.d.ts +10 -2
- package/dist/resolution/cooperative-yield.d.ts.map +1 -1
- package/dist/resolution/cooperative-yield.js +6 -4
- package/dist/resolution/cooperative-yield.js.map +1 -1
- package/dist/resolution/frameworks/cics.d.ts +20 -0
- package/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/dist/resolution/frameworks/cics.js +90 -0
- package/dist/resolution/frameworks/cics.js.map +1 -0
- package/dist/resolution/frameworks/go.d.ts.map +1 -1
- package/dist/resolution/frameworks/go.js +29 -6
- package/dist/resolution/frameworks/go.js.map +1 -1
- package/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/dist/resolution/frameworks/index.js +3 -0
- package/dist/resolution/frameworks/index.js.map +1 -1
- package/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/dist/resolution/frameworks/java.js +14 -6
- package/dist/resolution/frameworks/java.js.map +1 -1
- package/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/dist/resolution/frameworks/terraform.js +277 -0
- package/dist/resolution/frameworks/terraform.js.map +1 -0
- package/dist/resolution/goframe-synthesizer.d.ts +2 -1
- package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
- package/dist/resolution/goframe-synthesizer.js +8 -3
- package/dist/resolution/goframe-synthesizer.js.map +1 -1
- package/dist/resolution/import-resolver.d.ts +9 -3
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +295 -11
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +146 -8
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +1089 -131
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/memory-budget.d.ts +48 -0
- package/dist/resolution/memory-budget.d.ts.map +1 -0
- package/dist/resolution/memory-budget.js +162 -0
- package/dist/resolution/memory-budget.js.map +1 -0
- package/dist/resolution/name-matcher.d.ts +12 -4
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +793 -130
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/resolver-pool.d.ts +106 -0
- package/dist/resolution/resolver-pool.d.ts.map +1 -0
- package/dist/resolution/resolver-pool.js +344 -0
- package/dist/resolution/resolver-pool.js.map +1 -0
- package/dist/resolution/resolver-worker.d.ts +17 -0
- package/dist/resolution/resolver-worker.d.ts.map +1 -0
- package/dist/resolution/resolver-worker.js +141 -0
- package/dist/resolution/resolver-worker.js.map +1 -0
- package/dist/resolution/strip-comments.d.ts +1 -1
- package/dist/resolution/strip-comments.d.ts.map +1 -1
- package/dist/resolution/strip-comments.js +49 -0
- package/dist/resolution/strip-comments.js.map +1 -1
- package/dist/resolution/types.d.ts +32 -0
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.d.ts +10 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.js +142 -4
- package/dist/resolution/workspace-packages.js.map +1 -1
- package/dist/search/identifier-segments.d.ts +60 -0
- package/dist/search/identifier-segments.d.ts.map +1 -0
- package/dist/search/identifier-segments.js +176 -0
- package/dist/search/identifier-segments.js.map +1 -0
- package/dist/search/query-utils.d.ts +201 -0
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +919 -2
- package/dist/search/query-utils.js.map +1 -1
- package/dist/spec/build/diff-parser.d.ts +55 -0
- package/dist/spec/build/diff-parser.d.ts.map +1 -0
- package/dist/spec/build/diff-parser.js +205 -0
- package/dist/spec/build/diff-parser.js.map +1 -0
- package/dist/spec/build/git-scanner.d.ts +93 -0
- package/dist/spec/build/git-scanner.d.ts.map +1 -0
- package/dist/spec/build/git-scanner.js +254 -0
- package/dist/spec/build/git-scanner.js.map +1 -0
- package/dist/spec/build/pipeline.d.ts +54 -0
- package/dist/spec/build/pipeline.d.ts.map +1 -0
- package/dist/spec/build/pipeline.js +170 -0
- package/dist/spec/build/pipeline.js.map +1 -0
- package/dist/spec/build/scan.d.ts +48 -0
- package/dist/spec/build/scan.d.ts.map +1 -0
- package/dist/spec/build/scan.js +89 -0
- package/dist/spec/build/scan.js.map +1 -0
- package/dist/spec/build/scope-resolver.d.ts +50 -0
- package/dist/spec/build/scope-resolver.d.ts.map +1 -0
- package/dist/spec/build/scope-resolver.js +105 -0
- package/dist/spec/build/scope-resolver.js.map +1 -0
- package/dist/spec/build/spec-extractor.d.ts +74 -0
- package/dist/spec/build/spec-extractor.d.ts.map +1 -0
- package/dist/spec/build/spec-extractor.js +344 -0
- package/dist/spec/build/spec-extractor.js.map +1 -0
- package/dist/spec/config.d.ts +35 -1
- package/dist/spec/config.d.ts.map +1 -1
- package/dist/spec/config.js +30 -5
- package/dist/spec/config.js.map +1 -1
- package/dist/spec/db/fts.d.ts.map +1 -1
- package/dist/spec/db/fts.js +19 -50
- package/dist/spec/db/fts.js.map +1 -1
- package/dist/spec/db/index.d.ts +5 -3
- package/dist/spec/db/index.d.ts.map +1 -1
- package/dist/spec/db/index.js +16 -1
- package/dist/spec/db/index.js.map +1 -1
- package/dist/spec/db/persist.d.ts +45 -0
- package/dist/spec/db/persist.d.ts.map +1 -0
- package/dist/spec/db/persist.js +66 -0
- package/dist/spec/db/persist.js.map +1 -0
- package/dist/spec/db/relations.d.ts +80 -1
- package/dist/spec/db/relations.d.ts.map +1 -1
- package/dist/spec/db/relations.js +175 -4
- package/dist/spec/db/relations.js.map +1 -1
- package/dist/spec/db/schema.d.ts +4 -0
- package/dist/spec/db/schema.d.ts.map +1 -1
- package/dist/spec/db/schema.js +4 -0
- package/dist/spec/db/schema.js.map +1 -1
- package/dist/spec/db/schema.sql +5 -1
- package/dist/spec/db/spec-node.d.ts +5 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -1
- package/dist/spec/db/spec-node.js +11 -5
- package/dist/spec/db/spec-node.js.map +1 -1
- package/dist/spec/db/sql-utils.d.ts +11 -0
- package/dist/spec/db/sql-utils.d.ts.map +1 -0
- package/dist/spec/db/sql-utils.js +19 -0
- package/dist/spec/db/sql-utils.js.map +1 -0
- package/dist/spec/evolve/cluster-context.d.ts +30 -0
- package/dist/spec/evolve/cluster-context.d.ts.map +1 -0
- package/dist/spec/evolve/cluster-context.js +78 -0
- package/dist/spec/evolve/cluster-context.js.map +1 -0
- package/dist/spec/evolve/commit-spec-analyzer.d.ts +51 -0
- package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +1 -0
- package/dist/spec/evolve/commit-spec-analyzer.js +88 -0
- package/dist/spec/evolve/commit-spec-analyzer.js.map +1 -0
- package/dist/spec/evolve/commit-spec-persister.d.ts +53 -0
- package/dist/spec/evolve/commit-spec-persister.d.ts.map +1 -0
- package/dist/spec/evolve/commit-spec-persister.js +93 -0
- package/dist/spec/evolve/commit-spec-persister.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +23 -7
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -1
- package/dist/spec/evolve/impact-locator.js +58 -14
- package/dist/spec/evolve/impact-locator.js.map +1 -1
- package/dist/spec/evolve/pipeline.d.ts +62 -25
- package/dist/spec/evolve/pipeline.d.ts.map +1 -1
- package/dist/spec/evolve/pipeline.js +436 -457
- package/dist/spec/evolve/pipeline.js.map +1 -1
- package/dist/spec/evolve/spec-rewriter.d.ts +13 -13
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
- package/dist/spec/evolve/spec-rewriter.js +45 -45
- package/dist/spec/evolve/spec-rewriter.js.map +1 -1
- package/dist/spec/git/commits.d.ts +85 -0
- package/dist/spec/git/commits.d.ts.map +1 -0
- package/dist/spec/git/commits.js +218 -0
- package/dist/spec/git/commits.js.map +1 -0
- package/dist/spec/git/exec.d.ts +13 -0
- package/dist/spec/git/exec.d.ts.map +1 -0
- package/dist/spec/git/exec.js +19 -0
- package/dist/spec/git/exec.js.map +1 -0
- package/dist/spec/git/index.d.ts +9 -0
- package/dist/spec/git/index.d.ts.map +1 -0
- package/dist/spec/git/index.js +22 -0
- package/dist/spec/git/index.js.map +1 -0
- package/dist/spec/git-utils.d.ts +8 -0
- package/dist/spec/git-utils.d.ts.map +1 -0
- package/dist/spec/git-utils.js +14 -0
- package/dist/spec/git-utils.js.map +1 -0
- package/dist/spec/graph/index.d.ts +8 -0
- package/dist/spec/graph/index.d.ts.map +1 -0
- package/dist/spec/graph/index.js +17 -0
- package/dist/spec/graph/index.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +6 -35
- package/dist/spec/graph/queries.d.ts.map +1 -1
- package/dist/spec/graph/queries.js +71 -248
- package/dist/spec/graph/queries.js.map +1 -1
- package/dist/spec/llm/agent-client.d.ts +59 -0
- package/dist/spec/llm/agent-client.d.ts.map +1 -0
- package/dist/spec/llm/agent-client.js +213 -0
- package/dist/spec/llm/agent-client.js.map +1 -0
- package/dist/spec/llm/agents/claude-code.d.ts +32 -0
- package/dist/spec/llm/agents/claude-code.d.ts.map +1 -0
- package/dist/spec/llm/agents/claude-code.js +125 -0
- package/dist/spec/llm/agents/claude-code.js.map +1 -0
- package/dist/spec/llm/agents/codex.d.ts +32 -0
- package/dist/spec/llm/agents/codex.d.ts.map +1 -0
- package/dist/spec/llm/agents/codex.js +145 -0
- package/dist/spec/llm/agents/codex.js.map +1 -0
- package/dist/spec/llm/agents/detect-utils.d.ts +28 -0
- package/dist/spec/llm/agents/detect-utils.d.ts.map +1 -0
- package/dist/spec/llm/agents/detect-utils.js +91 -0
- package/dist/spec/llm/agents/detect-utils.js.map +1 -0
- package/dist/spec/llm/agents/deveco-code.d.ts +33 -0
- package/dist/spec/llm/agents/deveco-code.d.ts.map +1 -0
- package/dist/spec/llm/agents/deveco-code.js +165 -0
- package/dist/spec/llm/agents/deveco-code.js.map +1 -0
- package/dist/spec/llm/agents/index.d.ts +31 -0
- package/dist/spec/llm/agents/index.d.ts.map +1 -0
- package/dist/spec/llm/agents/index.js +65 -0
- package/dist/spec/llm/agents/index.js.map +1 -0
- package/dist/spec/llm/agents/types.d.ts +76 -0
- package/dist/spec/llm/agents/types.d.ts.map +1 -0
- package/dist/spec/llm/agents/types.js +15 -0
- package/dist/spec/llm/agents/types.js.map +1 -0
- package/dist/spec/llm/client.d.ts +31 -11
- package/dist/spec/llm/client.d.ts.map +1 -1
- package/dist/spec/llm/client.js +143 -81
- package/dist/spec/llm/client.js.map +1 -1
- package/dist/spec/llm/factory.d.ts +41 -0
- package/dist/spec/llm/factory.d.ts.map +1 -0
- package/dist/spec/llm/factory.js +80 -0
- package/dist/spec/llm/factory.js.map +1 -0
- package/dist/spec/llm/prompts.d.ts +24 -8
- package/dist/spec/llm/prompts.d.ts.map +1 -1
- package/dist/spec/llm/prompts.js +100 -50
- package/dist/spec/llm/prompts.js.map +1 -1
- package/dist/spec/llm/retry.d.ts +53 -0
- package/dist/spec/llm/retry.d.ts.map +1 -0
- package/dist/spec/llm/retry.js +153 -0
- package/dist/spec/llm/retry.js.map +1 -0
- package/dist/spec/mine/clusterer.d.ts +63 -0
- package/dist/spec/mine/clusterer.d.ts.map +1 -0
- package/dist/spec/mine/clusterer.js +904 -0
- package/dist/spec/mine/clusterer.js.map +1 -0
- package/dist/spec/mine/clustering/features.d.ts +38 -0
- package/dist/spec/mine/clustering/features.d.ts.map +1 -0
- package/dist/spec/mine/clustering/features.js +108 -0
- package/dist/spec/mine/clustering/features.js.map +1 -0
- package/dist/spec/mine/clustering/index.d.ts +62 -0
- package/dist/spec/mine/clustering/index.d.ts.map +1 -0
- package/dist/spec/mine/clustering/index.js +259 -0
- package/dist/spec/mine/clustering/index.js.map +1 -0
- package/dist/spec/mine/clustering/leiden.d.ts +61 -0
- package/dist/spec/mine/clustering/leiden.d.ts.map +1 -0
- package/dist/spec/mine/clustering/leiden.js +489 -0
- package/dist/spec/mine/clustering/leiden.js.map +1 -0
- package/dist/spec/mine/clustering/text-similarity.d.ts +22 -0
- package/dist/spec/mine/clustering/text-similarity.d.ts.map +1 -0
- package/dist/spec/mine/clustering/text-similarity.js +98 -0
- package/dist/spec/mine/clustering/text-similarity.js.map +1 -0
- package/dist/spec/mine/generator.d.ts +54 -0
- package/dist/spec/mine/generator.d.ts.map +1 -0
- package/dist/spec/mine/generator.js +267 -0
- package/dist/spec/mine/generator.js.map +1 -0
- package/dist/spec/mine/persist.d.ts +37 -0
- package/dist/spec/mine/persist.d.ts.map +1 -0
- package/dist/spec/mine/persist.js +170 -0
- package/dist/spec/mine/persist.js.map +1 -0
- package/dist/spec/mine/pipeline.d.ts +41 -0
- package/dist/spec/mine/pipeline.d.ts.map +1 -0
- package/dist/spec/mine/pipeline.js +236 -0
- package/dist/spec/mine/pipeline.js.map +1 -0
- package/dist/spec/mine/progress-handler.d.ts +22 -0
- package/dist/spec/mine/progress-handler.d.ts.map +1 -0
- package/dist/spec/mine/progress-handler.js +108 -0
- package/dist/spec/mine/progress-handler.js.map +1 -0
- package/dist/spec/mine/progress.d.ts +23 -0
- package/dist/spec/mine/progress.d.ts.map +1 -0
- package/dist/spec/mine/progress.js +12 -0
- package/dist/spec/mine/progress.js.map +1 -0
- package/dist/spec/mine/scanner.d.ts +59 -0
- package/dist/spec/mine/scanner.d.ts.map +1 -0
- package/dist/spec/mine/scanner.js +383 -0
- package/dist/spec/mine/scanner.js.map +1 -0
- package/dist/spec/types.d.ts +32 -0
- package/dist/spec/types.d.ts.map +1 -1
- package/dist/spec/ui/index.d.ts +9 -0
- package/dist/spec/ui/index.d.ts.map +1 -0
- package/dist/spec/ui/index.js +16 -0
- package/dist/spec/ui/index.js.map +1 -0
- package/dist/spec/ui/progress-handler.d.ts +33 -0
- package/dist/spec/ui/progress-handler.d.ts.map +1 -0
- package/dist/spec/ui/progress-handler.js +132 -0
- package/dist/spec/ui/progress-handler.js.map +1 -0
- package/dist/spec/ui/progress.d.ts +24 -0
- package/dist/spec/ui/progress.d.ts.map +1 -0
- package/dist/spec/ui/progress.js +13 -0
- package/dist/spec/ui/progress.js.map +1 -0
- package/dist/spec/utils/fs.d.ts +45 -0
- package/dist/spec/utils/fs.d.ts.map +1 -0
- package/dist/spec/utils/fs.js +163 -0
- package/dist/spec/utils/fs.js.map +1 -0
- package/dist/spec/utils/index.d.ts +12 -0
- package/dist/spec/utils/index.d.ts.map +1 -0
- package/dist/spec/utils/index.js +26 -0
- package/dist/spec/utils/index.js.map +1 -0
- package/dist/spec/utils/meta.d.ts +32 -0
- package/dist/spec/utils/meta.d.ts.map +1 -0
- package/dist/spec/utils/meta.js +122 -0
- package/dist/spec/utils/meta.js.map +1 -0
- package/dist/spec/utils/truncate.d.ts +86 -0
- package/dist/spec/utils/truncate.d.ts.map +1 -0
- package/dist/spec/utils/truncate.js +172 -0
- package/dist/spec/utils/truncate.js.map +1 -0
- package/dist/spec/utils.d.ts +4 -16
- package/dist/spec/utils.d.ts.map +1 -1
- package/dist/spec/utils.js +6 -58
- package/dist/spec/utils.js.map +1 -1
- package/dist/sync/git-hooks.d.ts.map +1 -1
- package/dist/sync/git-hooks.js +2 -0
- package/dist/sync/git-hooks.js.map +1 -1
- package/dist/sync/watcher.d.ts +35 -6
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +127 -17
- package/dist/sync/watcher.js.map +1 -1
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +5 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/telemetry/index.js +3 -3
- package/dist/telemetry/index.js.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/dist/upgrade/index.d.ts +32 -0
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +162 -14
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/remove-binary.d.ts +87 -0
- package/dist/upgrade/remove-binary.d.ts.map +1 -0
- package/dist/upgrade/remove-binary.js +289 -0
- package/dist/upgrade/remove-binary.js.map +1 -0
- package/dist/upgrade/update-check.d.ts +92 -0
- package/dist/upgrade/update-check.d.ts.map +1 -0
- package/dist/upgrade/update-check.js +258 -0
- package/dist/upgrade/update-check.js.map +1 -0
- package/package.json +6 -2
- package/scripts/agent-eval/run-all.sh +6 -0
- package/scripts/build-bundle.sh +5 -0
- package/scripts/npm-shim.js +8 -1
- package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/data/agents.json +0 -109
- package/scripts/exp_boundary_eval/data/experiments.json +0 -140
- package/scripts/qa_eval/README.md +0 -407
- package/scripts/qa_eval/_test_deveco_probe.py +0 -41
- package/scripts/qa_eval/agent_runner.py +0 -526
- package/scripts/qa_eval/data/.gitignore +0 -4
- package/scripts/qa_eval/data/test-set.jsonl +0 -2
- package/scripts/qa_eval/eval_metrics.py +0 -274
- package/scripts/qa_eval/external_agent.py +0 -976
- package/scripts/qa_eval/llm_config.py +0 -92
- package/scripts/qa_eval/memory_monitor.py +0 -132
- package/scripts/qa_eval/my_answer_accuracy.py +0 -187
- package/scripts/qa_eval/requirements.txt +0 -2
- package/scripts/qa_eval/run_pipeline.py +0 -804
- package/scripts/qa_eval/stats_efficiency.py +0 -279
- package/scripts/qa_eval/stats_scores.py +0 -207
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Commit Clusterer — multi-signal graph clustering for the `spec mine` pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Groups commits by structural (AST symbol overlap), file-path, temporal,
|
|
6
|
+
* and commit-message similarity. Pure TypeScript implementation — no native
|
|
7
|
+
* dependencies required.
|
|
8
|
+
*
|
|
9
|
+
* Algorithm: Leiden community detection on a weighted similarity graph.
|
|
10
|
+
* Replaces Louvain and connected-components (single-linkage) to avoid chaining
|
|
11
|
+
* all commits into one giant cluster and to guarantee well-connected communities
|
|
12
|
+
* via the refinement phase. A resolution parameter γ is auto-tuned via
|
|
13
|
+
* multiplicative search so the output community count stays close to maxClusters.
|
|
14
|
+
*
|
|
15
|
+
* @module spec/mine/clusterer
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.clusterCommits = clusterCommits;
|
|
19
|
+
const errors_1 = require("../../errors");
|
|
20
|
+
const query_utils_1 = require("../../search/query-utils");
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/** Tokenize a string into lowercase words (3+ chars). */
|
|
25
|
+
function tokenize(text) {
|
|
26
|
+
return text
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.split(/[^a-z0-9]+/)
|
|
29
|
+
.filter((t) => t.length >= 3);
|
|
30
|
+
}
|
|
31
|
+
/** Jaccard similarity between two sets of strings. */
|
|
32
|
+
function jaccard(a, b) {
|
|
33
|
+
if (a.length === 0 && b.length === 0)
|
|
34
|
+
return 0;
|
|
35
|
+
const setA = new Set(a);
|
|
36
|
+
const setB = new Set(b);
|
|
37
|
+
let intersection = 0;
|
|
38
|
+
for (const x of setA) {
|
|
39
|
+
if (setB.has(x))
|
|
40
|
+
intersection++;
|
|
41
|
+
}
|
|
42
|
+
const union = setA.size + setB.size - intersection;
|
|
43
|
+
return union === 0 ? 0 : intersection / union;
|
|
44
|
+
}
|
|
45
|
+
/** Cosine similarity between two equal-length numeric vectors. */
|
|
46
|
+
function cosineSimilarity(a, b) {
|
|
47
|
+
if (a.length !== b.length || a.length === 0)
|
|
48
|
+
return 0;
|
|
49
|
+
let dot = 0;
|
|
50
|
+
let magA = 0;
|
|
51
|
+
let magB = 0;
|
|
52
|
+
for (let i = 0; i < a.length; i++) {
|
|
53
|
+
dot += a[i] * b[i];
|
|
54
|
+
magA += a[i] * a[i];
|
|
55
|
+
magB += b[i] * b[i];
|
|
56
|
+
}
|
|
57
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
58
|
+
return denom === 0 ? 0 : dot / denom;
|
|
59
|
+
}
|
|
60
|
+
/** Collect all symbol names across all file changes in a commit. */
|
|
61
|
+
function collectSymbolNames(change) {
|
|
62
|
+
const names = new Set();
|
|
63
|
+
for (const fc of change.fileChanges) {
|
|
64
|
+
for (const s of fc.addedSymbols)
|
|
65
|
+
names.add(s.name);
|
|
66
|
+
for (const s of fc.removedSymbols)
|
|
67
|
+
names.add(s.name);
|
|
68
|
+
for (const m of fc.modifiedSymbols) {
|
|
69
|
+
names.add(m.old.name);
|
|
70
|
+
names.add(m.new.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return Array.from(names);
|
|
74
|
+
}
|
|
75
|
+
/** Collect all file paths changed in a commit. */
|
|
76
|
+
function collectFilePaths(change) {
|
|
77
|
+
const paths = new Set();
|
|
78
|
+
for (const fc of change.fileChanges) {
|
|
79
|
+
paths.add(fc.filePath);
|
|
80
|
+
}
|
|
81
|
+
return Array.from(paths);
|
|
82
|
+
}
|
|
83
|
+
/** Collect directory prefixes (first 2 path segments) for module proximity.
|
|
84
|
+
* For single-segment paths (root-level files) the filename itself is used as
|
|
85
|
+
* the key — this prevents unrelated root-level files from falsely sharing an
|
|
86
|
+
* empty "directory" and being grouped together. */
|
|
87
|
+
function collectDirectoryPrefixes(change) {
|
|
88
|
+
const dirs = new Set();
|
|
89
|
+
for (const fc of change.fileChanges) {
|
|
90
|
+
const parts = fc.filePath.split('/');
|
|
91
|
+
if (parts.length === 1) {
|
|
92
|
+
dirs.add(parts[0]);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
dirs.add(parts.slice(0, 2).join('/'));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return Array.from(dirs);
|
|
99
|
+
}
|
|
100
|
+
/** Minimum number of changed symbols (added + removed + modified) required
|
|
101
|
+
* for a solo commit to be promoted into its own cluster. Commits below
|
|
102
|
+
* this threshold go to `unclustered` — they are too small to warrant a
|
|
103
|
+
* standalone spec document. */
|
|
104
|
+
const MIN_SYMBOLS_FOR_SOLO_CLUSTER = 2;
|
|
105
|
+
/** Count total changed symbols across all file changes in a commit. */
|
|
106
|
+
function countChangedSymbols(change) {
|
|
107
|
+
let count = 0;
|
|
108
|
+
for (const fc of change.fileChanges) {
|
|
109
|
+
count += fc.addedSymbols.length;
|
|
110
|
+
count += fc.removedSymbols.length;
|
|
111
|
+
count += fc.modifiedSymbols.length;
|
|
112
|
+
}
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
115
|
+
/** Compute the cohesion ratio for a commit — symbols changed per file touched.
|
|
116
|
+
* Low cohesion (< 1.0) suggests a mechanical refactor across many files;
|
|
117
|
+
* high cohesion (≥ 2.0) suggests concentrated feature work. */
|
|
118
|
+
function computeCohesion(change) {
|
|
119
|
+
const totalSymbols = countChangedSymbols(change);
|
|
120
|
+
const filesTouched = change.fileChanges.length || 1;
|
|
121
|
+
return totalSymbols / filesTouched;
|
|
122
|
+
}
|
|
123
|
+
/** Extract ticket references (e.g., PROJ-123, #456) from a commit message. */
|
|
124
|
+
function extractTicketRefs(message) {
|
|
125
|
+
const refs = [];
|
|
126
|
+
const jiraRe = /[A-Z]+-\d+/g;
|
|
127
|
+
const ghRe = /#\d+/g;
|
|
128
|
+
let m;
|
|
129
|
+
while ((m = jiraRe.exec(message)) !== null)
|
|
130
|
+
refs.push(m[0]);
|
|
131
|
+
while ((m = ghRe.exec(message)) !== null)
|
|
132
|
+
refs.push(m[0]);
|
|
133
|
+
return Array.from(new Set(refs));
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// TF-IDF Vectorizer (message-level)
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
class TfidfVectorizer {
|
|
139
|
+
vocabulary = new Map();
|
|
140
|
+
idf = [];
|
|
141
|
+
/** Build vocabulary and compute IDF from document corpus. */
|
|
142
|
+
fit(documents) {
|
|
143
|
+
const docFreq = new Map();
|
|
144
|
+
const N = documents.length;
|
|
145
|
+
for (const doc of documents) {
|
|
146
|
+
const seen = new Set();
|
|
147
|
+
for (const term of doc) {
|
|
148
|
+
if (!seen.has(term)) {
|
|
149
|
+
seen.add(term);
|
|
150
|
+
docFreq.set(term, (docFreq.get(term) || 0) + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Build vocabulary sorted for deterministic output
|
|
155
|
+
const sorted = Array.from(docFreq.keys()).sort();
|
|
156
|
+
this.vocabulary.clear();
|
|
157
|
+
this.idf = [];
|
|
158
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
159
|
+
const term = sorted[i];
|
|
160
|
+
this.vocabulary.set(term, i);
|
|
161
|
+
// IDF = log((N + 1) / (df + 1)) + 1 (smooth)
|
|
162
|
+
const df = docFreq.get(term);
|
|
163
|
+
this.idf.push(Math.log((N + 1) / (df + 1)) + 1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Transform a document into a TF-IDF vector. */
|
|
167
|
+
transform(document) {
|
|
168
|
+
const vec = new Array(this.vocabulary.size).fill(0);
|
|
169
|
+
const tf = new Map();
|
|
170
|
+
for (const term of document) {
|
|
171
|
+
tf.set(term, (tf.get(term) || 0) + 1);
|
|
172
|
+
}
|
|
173
|
+
for (const [term, count] of tf) {
|
|
174
|
+
const idx = this.vocabulary.get(term);
|
|
175
|
+
if (idx !== undefined) {
|
|
176
|
+
vec[idx] = (count / document.length) * this.idf[idx];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return vec;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Similarity Computation
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
/**
|
|
186
|
+
* Compute multi-signal similarity between two commits.
|
|
187
|
+
*
|
|
188
|
+
* Weights:
|
|
189
|
+
* - Symbol overlap (Jaccard): 0.40
|
|
190
|
+
* - Spatial proximity (Jaccard): 0.15 (max of file-path and dir-prefix)
|
|
191
|
+
* - Message TF-IDF cosine: 0.25
|
|
192
|
+
* - Ticket reference overlap: 0.10
|
|
193
|
+
* - Temporal proximity: 0.10 (half-life 3 days)
|
|
194
|
+
*
|
|
195
|
+
* A cohesion penalty is applied as a multiplier when one commit has low
|
|
196
|
+
* symbol-per-file density (< 2.0), which is characteristic of mechanical
|
|
197
|
+
* refactors that should not bridge unrelated feature clusters.
|
|
198
|
+
*/
|
|
199
|
+
function computeSimilarity(a, b, aSymbols, bSymbols, aFiles, bFiles, aDirs, bDirs, aTickets, bTickets, msgTfidfMatrix, aIdx, bIdx) {
|
|
200
|
+
// Symbol overlap — highest weight because AST-level diff is precise
|
|
201
|
+
const symbolSim = jaccard(aSymbols, bSymbols);
|
|
202
|
+
// Spatial proximity — max of exact file-path and module-level dir-prefix
|
|
203
|
+
// overlap, so same-file edits get full credit and same-module edits also
|
|
204
|
+
// connect, without double-counting the same spatial dimension.
|
|
205
|
+
const spatialSim = Math.max(jaccard(aFiles, bFiles), jaccard(aDirs, bDirs));
|
|
206
|
+
// Message TF-IDF cosine
|
|
207
|
+
const msgSim = cosineSimilarity(msgTfidfMatrix[aIdx], msgTfidfMatrix[bIdx]);
|
|
208
|
+
// Ticket reference overlap
|
|
209
|
+
const ticketSim = jaccard(aTickets, bTickets);
|
|
210
|
+
// Temporal proximity — half-life 3 days (259200 seconds)
|
|
211
|
+
const halfLife = 3 * 24 * 60 * 60 * 1000;
|
|
212
|
+
const maxTimeSpan = Math.abs(a.timestamp - b.timestamp);
|
|
213
|
+
const timeSim = Math.exp(-maxTimeSpan / halfLife);
|
|
214
|
+
// Cohesion penalty: low cohesion (many files, few symbols) = refactor-like.
|
|
215
|
+
// Penalty multiplier: 0.6 at cohesion 0 → 1.0 at cohesion ≥ 2.0
|
|
216
|
+
const minCohesion = Math.min(computeCohesion(a), computeCohesion(b));
|
|
217
|
+
const cohesionPenalty = minCohesion >= 2.0
|
|
218
|
+
? 1.0
|
|
219
|
+
: 0.6 + 0.4 * (minCohesion / 2.0);
|
|
220
|
+
const rawScore = 0.40 * symbolSim +
|
|
221
|
+
0.15 * spatialSim +
|
|
222
|
+
0.25 * msgSim +
|
|
223
|
+
0.10 * ticketSim +
|
|
224
|
+
0.10 * timeSim;
|
|
225
|
+
return rawScore * cohesionPenalty;
|
|
226
|
+
}
|
|
227
|
+
/** Compute weighted degree for every node. */
|
|
228
|
+
function computeDegrees(graph) {
|
|
229
|
+
const n = graph.length;
|
|
230
|
+
const degrees = new Array(n).fill(0);
|
|
231
|
+
for (let i = 0; i < n; i++) {
|
|
232
|
+
for (let j = 0; j < n; j++) {
|
|
233
|
+
degrees[i] += graph[i][j];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return degrees;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Shuffle an array in place (Fisher-Yates). Returns the same array.
|
|
240
|
+
*/
|
|
241
|
+
function shuffle(arr) {
|
|
242
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
243
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
244
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
245
|
+
}
|
|
246
|
+
return arr;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Create an array [0, 1, ..., n-1].
|
|
250
|
+
*/
|
|
251
|
+
function range(n) {
|
|
252
|
+
return Array.from({ length: n }, (_, i) => i);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Modularity gain from moving node i into community C.
|
|
256
|
+
*
|
|
257
|
+
* Assumes node i has already been *removed* from its current community.
|
|
258
|
+
*
|
|
259
|
+
* @param k_i_in - Sum of edge weights from node i to community C.
|
|
260
|
+
* @param k_i - Weighted degree of node i.
|
|
261
|
+
* @param sigmaTotC - Total weighted degree of community C BEFORE adding i.
|
|
262
|
+
* @param m2 - Twice the total edge weight of the graph (2m).
|
|
263
|
+
* @param gamma - Resolution parameter (higher = more communities).
|
|
264
|
+
*/
|
|
265
|
+
function modularityGain(k_i_in, k_i, sigmaTotC, m2, gamma) {
|
|
266
|
+
if (m2 === 0)
|
|
267
|
+
return 0;
|
|
268
|
+
return k_i_in - gamma * sigmaTotC * k_i / m2;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Refinement phase of the Leiden algorithm.
|
|
272
|
+
*
|
|
273
|
+
* For each Phase-1 community, runs a restricted local-moving pass that starts
|
|
274
|
+
* from singleton sub-communities and only allows moves within the same community.
|
|
275
|
+
* This guarantees that sub-communities are internally well-connected before they
|
|
276
|
+
* are aggregated — the key advantage of Leiden over Louvain.
|
|
277
|
+
*
|
|
278
|
+
* @param graph - Full weighted graph.
|
|
279
|
+
* @param communities - Phase-1 partition (list of node-index arrays).
|
|
280
|
+
* @param degrees - Pre-computed weighted degrees for every node.
|
|
281
|
+
* @param m2 - Twice the total edge weight of the full graph.
|
|
282
|
+
* @param gamma - Resolution parameter.
|
|
283
|
+
* @returns Refined partition — flat list of sub-community node-index arrays.
|
|
284
|
+
*/
|
|
285
|
+
function refine(graph, communities, degrees, m2, gamma) {
|
|
286
|
+
const refined = [];
|
|
287
|
+
for (const community of communities) {
|
|
288
|
+
if (community.length <= 1) {
|
|
289
|
+
refined.push([...community]);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const nodes = community;
|
|
293
|
+
const nodeCount = nodes.length;
|
|
294
|
+
// Each node starts in its own singleton sub-community.
|
|
295
|
+
const nodeToSub = range(nodeCount);
|
|
296
|
+
const subSigmaTot = nodes.map((n) => degrees[n]);
|
|
297
|
+
// Local moving restricted to within this community only.
|
|
298
|
+
const MAX_PASSES = 20;
|
|
299
|
+
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
300
|
+
let moved = false;
|
|
301
|
+
const order = shuffle(range(nodeCount));
|
|
302
|
+
for (const localIdx of order) {
|
|
303
|
+
const globalNode = nodes[localIdx];
|
|
304
|
+
const currentSub = nodeToSub[localIdx];
|
|
305
|
+
const k_i = degrees[globalNode];
|
|
306
|
+
// Collect edge weights to neighbor sub-communities (within this community only).
|
|
307
|
+
const neighborSubs = new Map();
|
|
308
|
+
for (let j = 0; j < nodeCount; j++) {
|
|
309
|
+
if (j === localIdx)
|
|
310
|
+
continue;
|
|
311
|
+
const w = graph[globalNode][nodes[j]];
|
|
312
|
+
if (w > 0) {
|
|
313
|
+
const subJ = nodeToSub[j];
|
|
314
|
+
neighborSubs.set(subJ, (neighborSubs.get(subJ) || 0) + w);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// ΔQ for removing from current sub-community.
|
|
318
|
+
const k_i_in_current = neighborSubs.get(currentSub) || 0;
|
|
319
|
+
const sigmaTotCurrent = subSigmaTot[currentSub];
|
|
320
|
+
const sigmaTotCurrentWithout = sigmaTotCurrent - k_i;
|
|
321
|
+
const deltaQRemove = k_i_in_current > 0 || sigmaTotCurrentWithout > 0
|
|
322
|
+
? -(k_i_in_current - gamma * sigmaTotCurrentWithout * k_i / m2)
|
|
323
|
+
: 0;
|
|
324
|
+
let bestSub = currentSub;
|
|
325
|
+
let bestDeltaQ = 0;
|
|
326
|
+
for (const [subId, k_i_in] of neighborSubs) {
|
|
327
|
+
if (subId === currentSub)
|
|
328
|
+
continue;
|
|
329
|
+
const sigmaTotTarget = subSigmaTot[subId];
|
|
330
|
+
if (sigmaTotTarget === 0)
|
|
331
|
+
continue;
|
|
332
|
+
const deltaQAdd = modularityGain(k_i_in, k_i, sigmaTotTarget, m2, gamma);
|
|
333
|
+
const totalDelta = deltaQRemove + deltaQAdd;
|
|
334
|
+
if (totalDelta > bestDeltaQ) {
|
|
335
|
+
bestDeltaQ = totalDelta;
|
|
336
|
+
bestSub = subId;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (bestSub !== currentSub) {
|
|
340
|
+
nodeToSub[localIdx] = bestSub;
|
|
341
|
+
subSigmaTot[currentSub] -= k_i;
|
|
342
|
+
subSigmaTot[bestSub] += k_i;
|
|
343
|
+
moved = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (!moved)
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
// Collect non-empty sub-communities from this community.
|
|
350
|
+
const subCommToNodes = new Map();
|
|
351
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
352
|
+
const subId = nodeToSub[i];
|
|
353
|
+
if (!subCommToNodes.has(subId)) {
|
|
354
|
+
subCommToNodes.set(subId, []);
|
|
355
|
+
}
|
|
356
|
+
subCommToNodes.get(subId).push(nodes[i]);
|
|
357
|
+
}
|
|
358
|
+
for (const [, members] of subCommToNodes) {
|
|
359
|
+
refined.push(members);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return refined;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Core Leiden algorithm: iterative local-moving + refinement + aggregation.
|
|
366
|
+
*
|
|
367
|
+
* Unlike Louvain (which feeds the Phase-1 partition directly into aggregation),
|
|
368
|
+
* Leiden inserts a refinement step that splits each Phase-1 community into
|
|
369
|
+
* well-connected sub-communities. Only the refined partition is aggregated.
|
|
370
|
+
* This guarantees internally-connected communities and avoids the
|
|
371
|
+
* "disconnected community" defect of Louvain.
|
|
372
|
+
*
|
|
373
|
+
* @param graph - Weighted undirected similarity graph.
|
|
374
|
+
* @param gamma - Resolution parameter (default 1.0).
|
|
375
|
+
* @param depth - Aggregation depth guard (internal use).
|
|
376
|
+
*/
|
|
377
|
+
function leiden(graph, gamma, depth = 0) {
|
|
378
|
+
const n = graph.length;
|
|
379
|
+
if (n <= 1) {
|
|
380
|
+
return n === 0 ? [] : [[0]];
|
|
381
|
+
}
|
|
382
|
+
const degrees = computeDegrees(graph);
|
|
383
|
+
const totalWeight = degrees.reduce((a, b) => a + b, 0);
|
|
384
|
+
const m2 = totalWeight; // 2m (sum of all degrees = 2 × sum of edge weights)
|
|
385
|
+
// Each node starts in its own community
|
|
386
|
+
const nodeToComm = range(n);
|
|
387
|
+
// Per-community stats
|
|
388
|
+
// sigmaTot[c] = sum of degrees of all nodes in community c
|
|
389
|
+
const sigmaTot = [...degrees];
|
|
390
|
+
// Phase 1: local moving
|
|
391
|
+
const MAX_PASSES = 20;
|
|
392
|
+
for (let pass = 0; pass < MAX_PASSES; pass++) {
|
|
393
|
+
let moved = false;
|
|
394
|
+
const order = shuffle(range(n));
|
|
395
|
+
for (const node of order) {
|
|
396
|
+
const currentComm = nodeToComm[node];
|
|
397
|
+
const k_i = degrees[node];
|
|
398
|
+
// Collect edge weights to each neighbor community
|
|
399
|
+
const neighborComms = new Map(); // commId → k_i_in
|
|
400
|
+
for (let j = 0; j < n; j++) {
|
|
401
|
+
const w = graph[node][j];
|
|
402
|
+
if (w > 0) {
|
|
403
|
+
const cj = nodeToComm[j];
|
|
404
|
+
neighborComms.set(cj, (neighborComms.get(cj) || 0) + w);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Compute ΔQ for removing node from its current community.
|
|
408
|
+
// k_i_in_current = edges from node to nodes in current community.
|
|
409
|
+
const k_i_in_current = neighborComms.get(currentComm) || 0;
|
|
410
|
+
const sigmaTotCurrent = sigmaTot[currentComm];
|
|
411
|
+
// ΔQ of removal (negative of the gain formula applied to current community
|
|
412
|
+
// with sigmaTot excluding self)
|
|
413
|
+
const sigmaTotCurrentWithout = sigmaTotCurrent - k_i;
|
|
414
|
+
const deltaQRemove = k_i_in_current > 0 || sigmaTotCurrentWithout > 0
|
|
415
|
+
? -(k_i_in_current - gamma * sigmaTotCurrentWithout * k_i / m2)
|
|
416
|
+
: 0;
|
|
417
|
+
// Find best community to move to
|
|
418
|
+
let bestComm = currentComm;
|
|
419
|
+
let bestDeltaQ = 0;
|
|
420
|
+
for (const [commId, k_i_in] of neighborComms) {
|
|
421
|
+
if (commId === currentComm)
|
|
422
|
+
continue;
|
|
423
|
+
const sigmaTotTarget = sigmaTot[commId];
|
|
424
|
+
if (sigmaTotTarget === 0)
|
|
425
|
+
continue;
|
|
426
|
+
const deltaQAdd = modularityGain(k_i_in, k_i, sigmaTotTarget, m2, gamma);
|
|
427
|
+
const totalDelta = deltaQRemove + deltaQAdd;
|
|
428
|
+
if (totalDelta > bestDeltaQ) {
|
|
429
|
+
bestDeltaQ = totalDelta;
|
|
430
|
+
bestComm = commId;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (bestComm !== currentComm) {
|
|
434
|
+
// Move node from currentComm to bestComm
|
|
435
|
+
nodeToComm[node] = bestComm;
|
|
436
|
+
sigmaTot[currentComm] -= k_i;
|
|
437
|
+
sigmaTot[bestComm] += k_i;
|
|
438
|
+
moved = true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (!moved)
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
// Collect non-empty communities from Phase 1
|
|
445
|
+
const commIdToMembers = new Map();
|
|
446
|
+
for (let i = 0; i < n; i++) {
|
|
447
|
+
const cid = nodeToComm[i];
|
|
448
|
+
if (!commIdToMembers.has(cid)) {
|
|
449
|
+
commIdToMembers.set(cid, []);
|
|
450
|
+
}
|
|
451
|
+
commIdToMembers.get(cid).push(i);
|
|
452
|
+
}
|
|
453
|
+
const communities = Array.from(commIdToMembers.values());
|
|
454
|
+
// Refinement: split each Phase-1 community into well-connected sub-communities.
|
|
455
|
+
const refinedCommunities = refine(graph, communities, degrees, m2, gamma);
|
|
456
|
+
// Phase 2: Aggregation using refined partition (Leiden key difference from Louvain).
|
|
457
|
+
const k = refinedCommunities.length;
|
|
458
|
+
if (k < n && k > 1 && depth < 5) {
|
|
459
|
+
const aggGraph = aggregateGraph(graph, refinedCommunities);
|
|
460
|
+
const subCommunities = leiden(aggGraph, gamma, depth + 1);
|
|
461
|
+
return mapBackCommunities(subCommunities, refinedCommunities);
|
|
462
|
+
}
|
|
463
|
+
return refinedCommunities;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Build an aggregated graph where each node represents a community from the
|
|
467
|
+
* previous level. Edge weights are summed across community boundaries.
|
|
468
|
+
*/
|
|
469
|
+
function aggregateGraph(graph, communities) {
|
|
470
|
+
const k = communities.length;
|
|
471
|
+
const agg = Array.from({ length: k }, () => new Array(k).fill(0));
|
|
472
|
+
// Build reverse map: original node → community index
|
|
473
|
+
const nodeToCommIdx = new Map();
|
|
474
|
+
for (let ci = 0; ci < k; ci++) {
|
|
475
|
+
for (const node of communities[ci]) {
|
|
476
|
+
nodeToCommIdx.set(node, ci);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// Aggregate edge weights
|
|
480
|
+
for (let i = 0; i < graph.length; i++) {
|
|
481
|
+
const ci = nodeToCommIdx.get(i);
|
|
482
|
+
for (let j = i + 1; j < graph.length; j++) {
|
|
483
|
+
const w = graph[i][j];
|
|
484
|
+
if (w > 0) {
|
|
485
|
+
const cj = nodeToCommIdx.get(j);
|
|
486
|
+
if (ci === cj) {
|
|
487
|
+
// Self-loop: double weight to preserve total degree
|
|
488
|
+
// (each internal edge contributes to degrees of both endpoints).
|
|
489
|
+
agg[ci][ci] += 2 * w;
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
agg[ci][cj] += w;
|
|
493
|
+
agg[cj][ci] += w;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return agg;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Map aggregated-level community indices back to original node indices.
|
|
502
|
+
*/
|
|
503
|
+
function mapBackCommunities(subCommunities, originalCommunities) {
|
|
504
|
+
return subCommunities.map((aggComm) => {
|
|
505
|
+
const members = [];
|
|
506
|
+
for (const aggIdx of aggComm) {
|
|
507
|
+
for (const node of originalCommunities[aggIdx]) {
|
|
508
|
+
members.push(node);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return members;
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Extract a sub-graph containing only the specified nodes and edges between them.
|
|
516
|
+
*
|
|
517
|
+
* @param graph - Full weighted graph.
|
|
518
|
+
* @param indices - Node indices to include in the sub-graph.
|
|
519
|
+
* @returns New WeightedGraph with local indexing; subGraph[i][j] == graph[indices[i]][indices[j]].
|
|
520
|
+
*/
|
|
521
|
+
function extractSubGraph(graph, indices) {
|
|
522
|
+
const m = indices.length;
|
|
523
|
+
const subGraph = Array.from({ length: m }, () => new Array(m).fill(0));
|
|
524
|
+
for (let i = 0; i < m; i++) {
|
|
525
|
+
for (let j = i + 1; j < m; j++) {
|
|
526
|
+
const w = graph[indices[i]][indices[j]];
|
|
527
|
+
subGraph[i][j] = w;
|
|
528
|
+
subGraph[j][i] = w;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return subGraph;
|
|
532
|
+
}
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
// Resolution Auto-Tuning
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
/** Maximum iterations for resolution parameter search. */
|
|
537
|
+
const MAX_TUNING_ITER = 15;
|
|
538
|
+
/** Minimum cluster size to consider for recursive sub-splitting. */
|
|
539
|
+
const MIN_SUBSPLIT_SIZE = 4;
|
|
540
|
+
/** Maximum recursion depth for sub-splitting. */
|
|
541
|
+
const MAX_SUBSPLIT_DEPTH = 3;
|
|
542
|
+
/**
|
|
543
|
+
* Recursively split large clusters by running Leiden on their sub-graphs.
|
|
544
|
+
*
|
|
545
|
+
* For each cluster with size > MIN_SUBSPLIT_SIZE, extracts its sub-graph,
|
|
546
|
+
* runs Leiden with auto-tuned γ (targeting up to min(5, floor(size/2))),
|
|
547
|
+
* and recursively splits further if sub-communities still have structure.
|
|
548
|
+
*
|
|
549
|
+
* Stop conditions:
|
|
550
|
+
* - Cluster size ≤ MIN_SUBSPLIT_SIZE
|
|
551
|
+
* - Sub-graph Leiden returns 1 community (no further structure)
|
|
552
|
+
* - Max recursion depth reached (MAX_SUBSPLIT_DEPTH)
|
|
553
|
+
*
|
|
554
|
+
* @param graph - Full original weighted graph.
|
|
555
|
+
* @param communities - Current partition (list of node-index arrays).
|
|
556
|
+
* @param depth - Current recursion depth (internal).
|
|
557
|
+
* @returns Flat list of leaf-community node-index arrays.
|
|
558
|
+
*/
|
|
559
|
+
function recursiveSubSplit(graph, communities, depth = 0) {
|
|
560
|
+
if (depth >= MAX_SUBSPLIT_DEPTH)
|
|
561
|
+
return communities;
|
|
562
|
+
const result = [];
|
|
563
|
+
for (const community of communities) {
|
|
564
|
+
if (community.length <= MIN_SUBSPLIT_SIZE) {
|
|
565
|
+
result.push(community);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
// Extract sub-graph and run Leiden with independent γ tuning.
|
|
569
|
+
const subGraph = extractSubGraph(graph, community);
|
|
570
|
+
const targetClusters = Math.min(5, Math.floor(community.length / 2));
|
|
571
|
+
const subComms = autoTuneResolution(subGraph, targetClusters);
|
|
572
|
+
if (subComms.length <= 1) {
|
|
573
|
+
// No further structure found — keep as-is.
|
|
574
|
+
result.push(community);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
// Map sub-community local indices back to original graph indices.
|
|
578
|
+
const mappedSubComms = subComms.map((subComm) => subComm.map((localIdx) => community[localIdx]));
|
|
579
|
+
// Recurse into sub-communities.
|
|
580
|
+
const splitResult = recursiveSubSplit(graph, mappedSubComms, depth + 1);
|
|
581
|
+
result.push(...splitResult);
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Adjust the resolution parameter γ so that Leiden produces approximately
|
|
587
|
+
* `targetClusters` communities.
|
|
588
|
+
*
|
|
589
|
+
* Uses multiplicative search: higher γ → more communities, lower γ → fewer.
|
|
590
|
+
*/
|
|
591
|
+
function autoTuneResolution(graph, maxClusters) {
|
|
592
|
+
const n = graph.length;
|
|
593
|
+
if (n <= maxClusters) {
|
|
594
|
+
// Can't have more communities than nodes. Run once with default gamma
|
|
595
|
+
// and let post-processing handle it.
|
|
596
|
+
return leiden(graph, 1.0);
|
|
597
|
+
}
|
|
598
|
+
let gamma = 1.0;
|
|
599
|
+
let communities = leiden(graph, gamma);
|
|
600
|
+
let count = communities.length;
|
|
601
|
+
// If already in range (±30% tolerance with upper cap at maxClusters), accept.
|
|
602
|
+
if (count >= Math.ceil(maxClusters * 0.5) && count <= maxClusters) {
|
|
603
|
+
return communities;
|
|
604
|
+
}
|
|
605
|
+
// Multiplicative search: keep adjusting gamma until we bracket the target.
|
|
606
|
+
let lowGamma = 0.05;
|
|
607
|
+
let highGamma = 50.0;
|
|
608
|
+
let lowCommunities = null;
|
|
609
|
+
let highCommunities = null;
|
|
610
|
+
for (let iter = 0; iter < MAX_TUNING_ITER; iter++) {
|
|
611
|
+
// Run Leiden at current gamma
|
|
612
|
+
communities = leiden(graph, gamma);
|
|
613
|
+
count = communities.length;
|
|
614
|
+
(0, errors_1.logDebug)('Clusterer: resolution tuning', {
|
|
615
|
+
iteration: iter,
|
|
616
|
+
gamma: Math.round(gamma * 1000) / 1000,
|
|
617
|
+
communities: count,
|
|
618
|
+
target: maxClusters,
|
|
619
|
+
});
|
|
620
|
+
if (count >= Math.ceil(maxClusters * 0.5) && count <= maxClusters) {
|
|
621
|
+
// In range — accept.
|
|
622
|
+
return communities;
|
|
623
|
+
}
|
|
624
|
+
if (count > maxClusters) {
|
|
625
|
+
// Too many communities → need lower gamma
|
|
626
|
+
highGamma = gamma;
|
|
627
|
+
highCommunities = communities;
|
|
628
|
+
gamma = Math.max(lowGamma, gamma * 0.65);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
// Too few communities → need higher gamma
|
|
632
|
+
lowGamma = gamma;
|
|
633
|
+
lowCommunities = communities;
|
|
634
|
+
gamma = Math.min(highGamma, gamma * 1.55);
|
|
635
|
+
}
|
|
636
|
+
// If search range collapsed, exit
|
|
637
|
+
if (highGamma - lowGamma < 0.01)
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
// Prefer the result with count closest to (but not exceeding) maxClusters.
|
|
641
|
+
// If both exceed, use the one with fewer communities.
|
|
642
|
+
if (lowCommunities && highCommunities) {
|
|
643
|
+
const lowCount = lowCommunities.length;
|
|
644
|
+
const highCount = highCommunities.length;
|
|
645
|
+
if (lowCount <= maxClusters && highCount > maxClusters) {
|
|
646
|
+
return lowCommunities;
|
|
647
|
+
}
|
|
648
|
+
if (lowCount > maxClusters && highCount > maxClusters) {
|
|
649
|
+
return lowCount <= highCount ? lowCommunities : highCommunities;
|
|
650
|
+
}
|
|
651
|
+
// Both <= maxClusters: prefer the one closer to target
|
|
652
|
+
const lowDist = maxClusters - lowCount;
|
|
653
|
+
const highDist = maxClusters - highCount;
|
|
654
|
+
return lowDist <= highDist ? lowCommunities : highCommunities;
|
|
655
|
+
}
|
|
656
|
+
return communities;
|
|
657
|
+
}
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
// Greedy Merge (fallback when too many communities)
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
/**
|
|
662
|
+
* Greedily merge communities until count ≤ maxClusters, by repeatedly
|
|
663
|
+
* merging the pair with the highest average inter-community similarity.
|
|
664
|
+
*/
|
|
665
|
+
function greedyMerge(graph, communities, maxClusters) {
|
|
666
|
+
if (communities.length <= maxClusters)
|
|
667
|
+
return communities;
|
|
668
|
+
let current = communities.map((c) => [...c]);
|
|
669
|
+
while (current.length > maxClusters) {
|
|
670
|
+
let bestPair = null;
|
|
671
|
+
let bestSim = -Infinity;
|
|
672
|
+
for (let i = 0; i < current.length; i++) {
|
|
673
|
+
for (let j = i + 1; j < current.length; j++) {
|
|
674
|
+
let sum = 0;
|
|
675
|
+
let count = 0;
|
|
676
|
+
for (const a of current[i]) {
|
|
677
|
+
for (const b of current[j]) {
|
|
678
|
+
sum += graph[a][b];
|
|
679
|
+
count++;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const avgSim = count > 0 ? sum / count : 0;
|
|
683
|
+
if (avgSim > bestSim) {
|
|
684
|
+
bestSim = avgSim;
|
|
685
|
+
bestPair = [i, j];
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (!bestPair)
|
|
690
|
+
break;
|
|
691
|
+
const [a, b] = bestPair;
|
|
692
|
+
current[a] = [...current[a], ...current[b]];
|
|
693
|
+
current.splice(b, 1);
|
|
694
|
+
}
|
|
695
|
+
return current;
|
|
696
|
+
}
|
|
697
|
+
// ---------------------------------------------------------------------------
|
|
698
|
+
// Public API
|
|
699
|
+
// ---------------------------------------------------------------------------
|
|
700
|
+
/**
|
|
701
|
+
* Fallback quality gate: true when a commit has at least one new file that
|
|
702
|
+
* is not a test file. Used when the symbol-level change count is below the
|
|
703
|
+
* solo-cluster threshold — a commit that adds non-test source files carries
|
|
704
|
+
* structural intent even when the extractor could not produce symbols
|
|
705
|
+
* (e.g. ArkTS files without an active extraction context).
|
|
706
|
+
*/
|
|
707
|
+
function hasNewNonTestFiles(change) {
|
|
708
|
+
for (const fc of change.fileChanges) {
|
|
709
|
+
if (fc.isNewFile && !(0, query_utils_1.isTestFile)(fc.filePath)) {
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Cluster commits using Leiden community detection on a multi-signal
|
|
717
|
+
* similarity graph, with recursive sub-splitting for large clusters.
|
|
718
|
+
*
|
|
719
|
+
* The pipeline:
|
|
720
|
+
* 1. Build a weighted similarity graph from AST symbol, file-path,
|
|
721
|
+
* commit-message TF-IDF, ticket-reference, and temporal signals.
|
|
722
|
+
* 2. Run Leiden community detection with auto-tuned resolution γ.
|
|
723
|
+
* 3. Greedy-merge if still too many communities.
|
|
724
|
+
* 4. Recursively split large clusters (≥ 4 commits) by running
|
|
725
|
+
* Leiden on their sub-graphs.
|
|
726
|
+
*
|
|
727
|
+
* Replaces the previous Louvain (no refinement) and connected-components
|
|
728
|
+
* (single-linkage chaining) approaches.
|
|
729
|
+
*
|
|
730
|
+
* @param changes - AST change data for each commit.
|
|
731
|
+
* @param threshold - Minimum similarity (0-1) for considering an edge.
|
|
732
|
+
* @param maxClusters - Desired maximum number of top-level clusters.
|
|
733
|
+
* The resolution parameter γ is auto-tuned to approach
|
|
734
|
+
* this target. Recursive splitting may produce more
|
|
735
|
+
* clusters at finer granularities.
|
|
736
|
+
* @returns Clustered result with stats.
|
|
737
|
+
*/
|
|
738
|
+
function clusterCommits(changes, threshold, maxClusters) {
|
|
739
|
+
const n = changes.length;
|
|
740
|
+
// Edge cases: 0 or 1 commit — avoid Leiden overhead.
|
|
741
|
+
if (n === 0) {
|
|
742
|
+
return {
|
|
743
|
+
clusters: [],
|
|
744
|
+
unclustered: [],
|
|
745
|
+
stats: { totalCommits: 0, clusteredCommits: 0, clusterCount: 0 },
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (n === 1) {
|
|
749
|
+
const c = changes[0];
|
|
750
|
+
const totalSymbols = countChangedSymbols(c);
|
|
751
|
+
if (totalSymbols < MIN_SYMBOLS_FOR_SOLO_CLUSTER && !hasNewNonTestFiles(c)) {
|
|
752
|
+
(0, errors_1.logDebug)('Clusterer: single commit below quality threshold', {
|
|
753
|
+
hash: c.commitHash.slice(0, 7),
|
|
754
|
+
changedSymbols: totalSymbols,
|
|
755
|
+
minRequired: MIN_SYMBOLS_FOR_SOLO_CLUSTER,
|
|
756
|
+
});
|
|
757
|
+
return {
|
|
758
|
+
clusters: [],
|
|
759
|
+
unclustered: [c],
|
|
760
|
+
stats: { totalCommits: 1, clusteredCommits: 0, clusterCount: 0 },
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
const syms = collectSymbolNames(c);
|
|
764
|
+
const files = collectFilePaths(c);
|
|
765
|
+
return {
|
|
766
|
+
clusters: [
|
|
767
|
+
{
|
|
768
|
+
id: 0,
|
|
769
|
+
commits: [c],
|
|
770
|
+
primaryFiles: files,
|
|
771
|
+
primarySymbols: syms,
|
|
772
|
+
summary: `1 commit: ${c.commitMessage}`,
|
|
773
|
+
timeRange: { start: c.timestamp, end: c.timestamp },
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
unclustered: [],
|
|
777
|
+
stats: { totalCommits: 1, clusteredCommits: 1, clusterCount: 1 },
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
// n >= 2: full pipeline from here.
|
|
781
|
+
// --- Pre-compute per-commit feature vectors ---
|
|
782
|
+
const messages = [];
|
|
783
|
+
const allSymbols = [];
|
|
784
|
+
const allFiles = [];
|
|
785
|
+
const allDirs = [];
|
|
786
|
+
const allTickets = [];
|
|
787
|
+
for (const change of changes) {
|
|
788
|
+
messages.push(tokenize(change.commitMessage));
|
|
789
|
+
allSymbols.push(collectSymbolNames(change));
|
|
790
|
+
allFiles.push(collectFilePaths(change));
|
|
791
|
+
allDirs.push(collectDirectoryPrefixes(change));
|
|
792
|
+
allTickets.push(extractTicketRefs(change.commitMessage));
|
|
793
|
+
}
|
|
794
|
+
// --- Build TF-IDF vectors for commit messages ---
|
|
795
|
+
const tfidf = new TfidfVectorizer();
|
|
796
|
+
tfidf.fit(messages);
|
|
797
|
+
const msgTfidfMatrix = messages.map((msg) => tfidf.transform(msg));
|
|
798
|
+
// --- Build weighted similarity graph ---
|
|
799
|
+
const graph = Array.from({ length: n }, () => new Array(n).fill(0));
|
|
800
|
+
for (let i = 0; i < n; i++) {
|
|
801
|
+
for (let j = i + 1; j < n; j++) {
|
|
802
|
+
const sim = computeSimilarity(changes[i], changes[j], allSymbols[i], allSymbols[j], allFiles[i], allFiles[j], allDirs[i], allDirs[j], allTickets[i], allTickets[j], msgTfidfMatrix, i, j);
|
|
803
|
+
if (sim >= threshold) {
|
|
804
|
+
graph[i][j] = sim;
|
|
805
|
+
graph[j][i] = sim;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// --- Leiden community detection with auto-tuned resolution ---
|
|
810
|
+
let components = autoTuneResolution(graph, maxClusters);
|
|
811
|
+
// --- Greedy merge if still too many communities ---
|
|
812
|
+
if (components.length > maxClusters) {
|
|
813
|
+
(0, errors_1.logDebug)('Clusterer: post-Leiden greedy merge', {
|
|
814
|
+
beforeMerge: components.length,
|
|
815
|
+
maxClusters,
|
|
816
|
+
});
|
|
817
|
+
components = greedyMerge(graph, components, maxClusters);
|
|
818
|
+
}
|
|
819
|
+
// --- Recursive sub-splitting for large clusters ---
|
|
820
|
+
components = recursiveSubSplit(graph, components);
|
|
821
|
+
// --- Sort by size descending for deterministic output ---
|
|
822
|
+
components.sort((a, b) => b.length - a.length);
|
|
823
|
+
// --- Filter: size-1 communities go to unclustered if < MIN_SYMBOLS ---
|
|
824
|
+
const validComponents = [];
|
|
825
|
+
const unclusteredIndices = [];
|
|
826
|
+
for (const comp of components) {
|
|
827
|
+
if (comp.length === 1) {
|
|
828
|
+
const idx = comp[0];
|
|
829
|
+
const c = changes[idx];
|
|
830
|
+
if (countChangedSymbols(c) < MIN_SYMBOLS_FOR_SOLO_CLUSTER && !hasNewNonTestFiles(c)) {
|
|
831
|
+
unclusteredIndices.push(idx);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
validComponents.push(comp);
|
|
836
|
+
}
|
|
837
|
+
// --- Build cluster objects ---
|
|
838
|
+
const clustered = new Set();
|
|
839
|
+
const clusters = [];
|
|
840
|
+
for (let i = 0; i < validComponents.length; i++) {
|
|
841
|
+
const comp = validComponents[i];
|
|
842
|
+
const clusterCommits = comp.map((idx) => {
|
|
843
|
+
clustered.add(idx);
|
|
844
|
+
return changes[idx];
|
|
845
|
+
});
|
|
846
|
+
// Compute primary files (appear in >50% of cluster commits)
|
|
847
|
+
const fileCounts = new Map();
|
|
848
|
+
const symbolCounts = new Map();
|
|
849
|
+
const allTimes = clusterCommits
|
|
850
|
+
.map((c) => c.timestamp)
|
|
851
|
+
.filter((t) => t > 0);
|
|
852
|
+
for (const c of clusterCommits) {
|
|
853
|
+
for (const fp of collectFilePaths(c)) {
|
|
854
|
+
fileCounts.set(fp, (fileCounts.get(fp) || 0) + 1);
|
|
855
|
+
}
|
|
856
|
+
for (const s of collectSymbolNames(c)) {
|
|
857
|
+
symbolCounts.set(s, (symbolCounts.get(s) || 0) + 1);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const half = clusterCommits.length / 2;
|
|
861
|
+
const primaryFiles = Array.from(fileCounts.entries())
|
|
862
|
+
.filter(([, count]) => count >= half)
|
|
863
|
+
.sort((a, b) => b[1] - a[1])
|
|
864
|
+
.map(([fp]) => fp);
|
|
865
|
+
const primarySymbols = Array.from(symbolCounts.entries())
|
|
866
|
+
.sort((a, b) => b[1] - a[1])
|
|
867
|
+
.slice(0, 10)
|
|
868
|
+
.map(([s]) => s);
|
|
869
|
+
// Build summary
|
|
870
|
+
const fileSummary = primaryFiles.length > 0
|
|
871
|
+
? `changing ${primaryFiles.slice(0, 3).join(', ')}`
|
|
872
|
+
: '';
|
|
873
|
+
const summary = `${clusterCommits.length} commits ${fileSummary}`.trim();
|
|
874
|
+
clusters.push({
|
|
875
|
+
id: i,
|
|
876
|
+
commits: clusterCommits,
|
|
877
|
+
primaryFiles,
|
|
878
|
+
primarySymbols,
|
|
879
|
+
summary,
|
|
880
|
+
timeRange: {
|
|
881
|
+
start: allTimes.length > 0 ? Math.min(...allTimes) : 0,
|
|
882
|
+
end: allTimes.length > 0 ? Math.max(...allTimes) : 0,
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
// --- Unclustered commits ---
|
|
887
|
+
const unclustered = changes.filter((_, i) => !clustered.has(i));
|
|
888
|
+
(0, errors_1.logDebug)('Clusterer result', {
|
|
889
|
+
totalCommits: n,
|
|
890
|
+
clusterCount: clusters.length,
|
|
891
|
+
clusteredCommits: n - unclustered.length,
|
|
892
|
+
unclustered: unclustered.length,
|
|
893
|
+
});
|
|
894
|
+
return {
|
|
895
|
+
clusters,
|
|
896
|
+
unclustered,
|
|
897
|
+
stats: {
|
|
898
|
+
totalCommits: n,
|
|
899
|
+
clusteredCommits: n - unclustered.length,
|
|
900
|
+
clusterCount: clusters.length,
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
//# sourceMappingURL=clusterer.js.map
|