forge-server 0.1.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/.claude/hooks/worktree-create.sh +64 -0
- package/.claude/hooks/worktree-remove.sh +57 -0
- package/.claude/settings.local.json +29 -0
- package/.forge/knowledge/conventions.yaml +1 -0
- package/.forge/knowledge/decisions.yaml +1 -0
- package/.forge/knowledge/gotchas.yaml +1 -0
- package/.forge/knowledge/patterns.yaml +1 -0
- package/.forge/manifest.yaml +6 -0
- package/CLAUDE.md +144 -0
- package/bin/setup-forge.sh +132 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +553 -0
- package/dist/cli.js.map +1 -0
- package/dist/context/codebase.d.ts +57 -0
- package/dist/context/codebase.d.ts.map +1 -0
- package/dist/context/codebase.js +301 -0
- package/dist/context/codebase.js.map +1 -0
- package/dist/context/injector.d.ts +147 -0
- package/dist/context/injector.d.ts.map +1 -0
- package/dist/context/injector.js +533 -0
- package/dist/context/injector.js.map +1 -0
- package/dist/context/memory.d.ts +32 -0
- package/dist/context/memory.d.ts.map +1 -0
- package/dist/context/memory.js +140 -0
- package/dist/context/memory.js.map +1 -0
- package/dist/context/session-index.d.ts +54 -0
- package/dist/context/session-index.d.ts.map +1 -0
- package/dist/context/session-index.js +265 -0
- package/dist/context/session-index.js.map +1 -0
- package/dist/context/session.d.ts +42 -0
- package/dist/context/session.d.ts.map +1 -0
- package/dist/context/session.js +121 -0
- package/dist/context/session.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion/chunker.d.ts +19 -0
- package/dist/ingestion/chunker.d.ts.map +1 -0
- package/dist/ingestion/chunker.js +189 -0
- package/dist/ingestion/chunker.js.map +1 -0
- package/dist/ingestion/embedder.d.ts +45 -0
- package/dist/ingestion/embedder.d.ts.map +1 -0
- package/dist/ingestion/embedder.js +152 -0
- package/dist/ingestion/embedder.js.map +1 -0
- package/dist/ingestion/git-analyzer.d.ts +77 -0
- package/dist/ingestion/git-analyzer.d.ts.map +1 -0
- package/dist/ingestion/git-analyzer.js +437 -0
- package/dist/ingestion/git-analyzer.js.map +1 -0
- package/dist/ingestion/indexer.d.ts +79 -0
- package/dist/ingestion/indexer.d.ts.map +1 -0
- package/dist/ingestion/indexer.js +766 -0
- package/dist/ingestion/indexer.js.map +1 -0
- package/dist/ingestion/markdown-chunker.d.ts +19 -0
- package/dist/ingestion/markdown-chunker.d.ts.map +1 -0
- package/dist/ingestion/markdown-chunker.js +243 -0
- package/dist/ingestion/markdown-chunker.js.map +1 -0
- package/dist/ingestion/markdown-knowledge.d.ts +21 -0
- package/dist/ingestion/markdown-knowledge.d.ts.map +1 -0
- package/dist/ingestion/markdown-knowledge.js +129 -0
- package/dist/ingestion/markdown-knowledge.js.map +1 -0
- package/dist/ingestion/parser.d.ts +20 -0
- package/dist/ingestion/parser.d.ts.map +1 -0
- package/dist/ingestion/parser.js +429 -0
- package/dist/ingestion/parser.js.map +1 -0
- package/dist/ingestion/watcher.d.ts +28 -0
- package/dist/ingestion/watcher.d.ts.map +1 -0
- package/dist/ingestion/watcher.js +147 -0
- package/dist/ingestion/watcher.js.map +1 -0
- package/dist/knowledge/hydrator.d.ts +37 -0
- package/dist/knowledge/hydrator.d.ts.map +1 -0
- package/dist/knowledge/hydrator.js +220 -0
- package/dist/knowledge/hydrator.js.map +1 -0
- package/dist/knowledge/registry.d.ts +129 -0
- package/dist/knowledge/registry.d.ts.map +1 -0
- package/dist/knowledge/registry.js +361 -0
- package/dist/knowledge/registry.js.map +1 -0
- package/dist/knowledge/search.d.ts +114 -0
- package/dist/knowledge/search.d.ts.map +1 -0
- package/dist/knowledge/search.js +428 -0
- package/dist/knowledge/search.js.map +1 -0
- package/dist/knowledge/store.d.ts +76 -0
- package/dist/knowledge/store.d.ts.map +1 -0
- package/dist/knowledge/store.js +230 -0
- package/dist/knowledge/store.js.map +1 -0
- package/dist/learning/confidence.d.ts +30 -0
- package/dist/learning/confidence.d.ts.map +1 -0
- package/dist/learning/confidence.js +165 -0
- package/dist/learning/confidence.js.map +1 -0
- package/dist/learning/patterns.d.ts +52 -0
- package/dist/learning/patterns.d.ts.map +1 -0
- package/dist/learning/patterns.js +290 -0
- package/dist/learning/patterns.js.map +1 -0
- package/dist/learning/trajectory.d.ts +55 -0
- package/dist/learning/trajectory.d.ts.map +1 -0
- package/dist/learning/trajectory.js +200 -0
- package/dist/learning/trajectory.js.map +1 -0
- package/dist/memory/memory-compat.d.ts +100 -0
- package/dist/memory/memory-compat.d.ts.map +1 -0
- package/dist/memory/memory-compat.js +146 -0
- package/dist/memory/memory-compat.js.map +1 -0
- package/dist/memory/observation-store.d.ts +57 -0
- package/dist/memory/observation-store.d.ts.map +1 -0
- package/dist/memory/observation-store.js +154 -0
- package/dist/memory/observation-store.js.map +1 -0
- package/dist/memory/session-tracker.d.ts +81 -0
- package/dist/memory/session-tracker.d.ts.map +1 -0
- package/dist/memory/session-tracker.js +262 -0
- package/dist/memory/session-tracker.js.map +1 -0
- package/dist/pipeline/engine.d.ts +179 -0
- package/dist/pipeline/engine.d.ts.map +1 -0
- package/dist/pipeline/engine.js +691 -0
- package/dist/pipeline/engine.js.map +1 -0
- package/dist/pipeline/events.d.ts +54 -0
- package/dist/pipeline/events.d.ts.map +1 -0
- package/dist/pipeline/events.js +157 -0
- package/dist/pipeline/events.js.map +1 -0
- package/dist/pipeline/parallel.d.ts +83 -0
- package/dist/pipeline/parallel.d.ts.map +1 -0
- package/dist/pipeline/parallel.js +277 -0
- package/dist/pipeline/parallel.js.map +1 -0
- package/dist/pipeline/state-machine.d.ts +65 -0
- package/dist/pipeline/state-machine.d.ts.map +1 -0
- package/dist/pipeline/state-machine.js +176 -0
- package/dist/pipeline/state-machine.js.map +1 -0
- package/dist/query/graph-queries.d.ts +84 -0
- package/dist/query/graph-queries.d.ts.map +1 -0
- package/dist/query/graph-queries.js +216 -0
- package/dist/query/graph-queries.js.map +1 -0
- package/dist/query/hybrid-search.d.ts +34 -0
- package/dist/query/hybrid-search.d.ts.map +1 -0
- package/dist/query/hybrid-search.js +263 -0
- package/dist/query/hybrid-search.js.map +1 -0
- package/dist/query/intent-detector.d.ts +35 -0
- package/dist/query/intent-detector.d.ts.map +1 -0
- package/dist/query/intent-detector.js +115 -0
- package/dist/query/intent-detector.js.map +1 -0
- package/dist/query/ranking.d.ts +57 -0
- package/dist/query/ranking.d.ts.map +1 -0
- package/dist/query/ranking.js +109 -0
- package/dist/query/ranking.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +291 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/falkordb-store.d.ts +73 -0
- package/dist/storage/falkordb-store.d.ts.map +1 -0
- package/dist/storage/falkordb-store.js +346 -0
- package/dist/storage/falkordb-store.js.map +1 -0
- package/dist/storage/file-cache.d.ts +32 -0
- package/dist/storage/file-cache.d.ts.map +1 -0
- package/dist/storage/file-cache.js +115 -0
- package/dist/storage/file-cache.js.map +1 -0
- package/dist/storage/interfaces.d.ts +151 -0
- package/dist/storage/interfaces.d.ts.map +1 -0
- package/dist/storage/interfaces.js +7 -0
- package/dist/storage/interfaces.js.map +1 -0
- package/dist/storage/qdrant-store.d.ts +110 -0
- package/dist/storage/qdrant-store.d.ts.map +1 -0
- package/dist/storage/qdrant-store.js +467 -0
- package/dist/storage/qdrant-store.js.map +1 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +136 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite.d.ts +35 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +132 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/tools/collaboration-tools.d.ts +111 -0
- package/dist/tools/collaboration-tools.d.ts.map +1 -0
- package/dist/tools/collaboration-tools.js +174 -0
- package/dist/tools/collaboration-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +293 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +437 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/graph-tools.d.ts +129 -0
- package/dist/tools/graph-tools.d.ts.map +1 -0
- package/dist/tools/graph-tools.js +237 -0
- package/dist/tools/graph-tools.js.map +1 -0
- package/dist/tools/ingestion-tools.d.ts +96 -0
- package/dist/tools/ingestion-tools.d.ts.map +1 -0
- package/dist/tools/ingestion-tools.js +90 -0
- package/dist/tools/ingestion-tools.js.map +1 -0
- package/dist/tools/learning-tools.d.ts +168 -0
- package/dist/tools/learning-tools.d.ts.map +1 -0
- package/dist/tools/learning-tools.js +158 -0
- package/dist/tools/learning-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +183 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +197 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/phase-tools.d.ts +954 -0
- package/dist/tools/phase-tools.d.ts.map +1 -0
- package/dist/tools/phase-tools.js +1215 -0
- package/dist/tools/phase-tools.js.map +1 -0
- package/dist/tools/pipeline-tools.d.ts +140 -0
- package/dist/tools/pipeline-tools.d.ts.map +1 -0
- package/dist/tools/pipeline-tools.js +162 -0
- package/dist/tools/pipeline-tools.js.map +1 -0
- package/dist/tools/registration-tools.d.ts +220 -0
- package/dist/tools/registration-tools.d.ts.map +1 -0
- package/dist/tools/registration-tools.js +391 -0
- package/dist/tools/registration-tools.js.map +1 -0
- package/dist/util/circuit-breaker.d.ts +75 -0
- package/dist/util/circuit-breaker.d.ts.map +1 -0
- package/dist/util/circuit-breaker.js +159 -0
- package/dist/util/circuit-breaker.js.map +1 -0
- package/dist/util/config.d.ts +23 -0
- package/dist/util/config.d.ts.map +1 -0
- package/dist/util/config.js +164 -0
- package/dist/util/config.js.map +1 -0
- package/dist/util/logger.d.ts +13 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +45 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/token-counter.d.ts +24 -0
- package/dist/util/token-counter.d.ts.map +1 -0
- package/dist/util/token-counter.js +48 -0
- package/dist/util/token-counter.js.map +1 -0
- package/dist/util/types.d.ts +525 -0
- package/dist/util/types.d.ts.map +1 -0
- package/dist/util/types.js +5 -0
- package/dist/util/types.js.map +1 -0
- package/docker-compose.yml +20 -0
- package/docs/plans/2026-02-27-swarm-coordination/architecture.md +203 -0
- package/docs/plans/2026-02-27-swarm-coordination/vision.md +57 -0
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +1 -0
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +300 -0
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +480 -0
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +462 -0
- package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +181 -0
- package/package.json +39 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/README.md +134 -0
- package/plugin/agents/architect.md +367 -0
- package/plugin/agents/backend-specialist.md +263 -0
- package/plugin/agents/brainstormer.md +122 -0
- package/plugin/agents/data-specialist.md +266 -0
- package/plugin/agents/designer.md +408 -0
- package/plugin/agents/frontend-specialist.md +241 -0
- package/plugin/agents/inspector.md +406 -0
- package/plugin/agents/knowledge-keeper.md +443 -0
- package/plugin/agents/platform-engineer.md +326 -0
- package/plugin/agents/product-manager.md +268 -0
- package/plugin/agents/product-owner.md +438 -0
- package/plugin/agents/pulse-checker.md +73 -0
- package/plugin/agents/qa-strategist.md +500 -0
- package/plugin/agents/self-improver.md +310 -0
- package/plugin/agents/strategist.md +360 -0
- package/plugin/agents/supervisor.md +380 -0
- package/plugin/commands/brainstorm.md +25 -0
- package/plugin/commands/forge.md +88 -0
- package/plugin/docs/atlassian-integration.md +110 -0
- package/plugin/docs/workflow.md +126 -0
- package/plugin/skills/agent-development/.skillfish.json +10 -0
- package/plugin/skills/agent-development/SKILL.md +415 -0
- package/plugin/skills/agent-development/examples/agent-creation-prompt.md +238 -0
- package/plugin/skills/agent-development/examples/complete-agent-examples.md +427 -0
- package/plugin/skills/agent-development/references/agent-creation-system-prompt.md +207 -0
- package/plugin/skills/agent-development/references/system-prompt-design.md +411 -0
- package/plugin/skills/agent-development/references/triggering-examples.md +491 -0
- package/plugin/skills/agent-development/scripts/validate-agent.sh +217 -0
- package/plugin/skills/agent-handoff/SKILL.md +335 -0
- package/plugin/skills/anti-stub/SKILL.md +317 -0
- package/plugin/skills/brainstorm/SKILL.md +31 -0
- package/plugin/skills/debugging/SKILL.md +276 -0
- package/plugin/skills/fix/SKILL.md +62 -0
- package/plugin/skills/frontend-design/.skillfish.json +10 -0
- package/plugin/skills/frontend-design/SKILL.md +42 -0
- package/plugin/skills/gotchas/SKILL.md +61 -0
- package/plugin/skills/graph-orchestrator/SKILL.md +38 -0
- package/plugin/skills/history/SKILL.md +58 -0
- package/plugin/skills/impact/SKILL.md +59 -0
- package/plugin/skills/implementation-execution/SKILL.md +291 -0
- package/plugin/skills/index-repo/SKILL.md +55 -0
- package/plugin/skills/interviewing/SKILL.md +225 -0
- package/plugin/skills/knowledge-curation/SKILL.md +393 -0
- package/plugin/skills/learn/SKILL.md +69 -0
- package/plugin/skills/mcp-integration/.skillfish.json +10 -0
- package/plugin/skills/mcp-integration/SKILL.md +554 -0
- package/plugin/skills/mcp-integration/examples/http-server.json +20 -0
- package/plugin/skills/mcp-integration/examples/sse-server.json +19 -0
- package/plugin/skills/mcp-integration/examples/stdio-server.json +26 -0
- package/plugin/skills/mcp-integration/references/authentication.md +549 -0
- package/plugin/skills/mcp-integration/references/server-types.md +536 -0
- package/plugin/skills/mcp-integration/references/tool-usage.md +538 -0
- package/plugin/skills/nestjs/.skillfish.json +10 -0
- package/plugin/skills/nestjs/SKILL.md +669 -0
- package/plugin/skills/nestjs/drizzle-reference.md +1894 -0
- package/plugin/skills/nestjs/reference.md +1447 -0
- package/plugin/skills/nestjs/workflow-optimization.md +229 -0
- package/plugin/skills/parallel-dispatch/SKILL.md +308 -0
- package/plugin/skills/project-discovery/SKILL.md +304 -0
- package/plugin/skills/search/SKILL.md +56 -0
- package/plugin/skills/security-audit/SKILL.md +362 -0
- package/plugin/skills/skill-development/.skillfish.json +10 -0
- package/plugin/skills/skill-development/SKILL.md +637 -0
- package/plugin/skills/skill-development/references/skill-creator-original.md +209 -0
- package/plugin/skills/tdd/SKILL.md +273 -0
- package/plugin/skills/terminal-presentation/SKILL.md +395 -0
- package/plugin/skills/test-strategy/SKILL.md +365 -0
- package/plugin/skills/verification-protocol/SKILL.md +256 -0
- package/plugin/skills/visual-explainer/CHANGELOG.md +97 -0
- package/plugin/skills/visual-explainer/LICENSE +21 -0
- package/plugin/skills/visual-explainer/README.md +137 -0
- package/plugin/skills/visual-explainer/SKILL.md +352 -0
- package/plugin/skills/visual-explainer/banner.png +0 -0
- package/plugin/skills/visual-explainer/package.json +11 -0
- package/plugin/skills/visual-explainer/prompts/diff-review.md +68 -0
- package/plugin/skills/visual-explainer/prompts/fact-check.md +63 -0
- package/plugin/skills/visual-explainer/prompts/generate-slides.md +18 -0
- package/plugin/skills/visual-explainer/prompts/generate-web-diagram.md +10 -0
- package/plugin/skills/visual-explainer/prompts/plan-review.md +86 -0
- package/plugin/skills/visual-explainer/prompts/project-recap.md +61 -0
- package/plugin/skills/visual-explainer/references/css-patterns.md +1188 -0
- package/plugin/skills/visual-explainer/references/libraries.md +470 -0
- package/plugin/skills/visual-explainer/references/responsive-nav.md +212 -0
- package/plugin/skills/visual-explainer/references/slide-patterns.md +1403 -0
- package/plugin/skills/visual-explainer/templates/architecture.html +596 -0
- package/plugin/skills/visual-explainer/templates/data-table.html +540 -0
- package/plugin/skills/visual-explainer/templates/mermaid-flowchart.html +435 -0
- package/plugin/skills/visual-explainer/templates/slide-deck.html +913 -0
- package/src/cli.ts +655 -0
- package/src/context/.gitkeep +0 -0
- package/src/context/codebase.ts +393 -0
- package/src/context/injector.ts +797 -0
- package/src/context/memory.ts +187 -0
- package/src/context/session-index.ts +327 -0
- package/src/context/session.ts +152 -0
- package/src/index.ts +47 -0
- package/src/ingestion/.gitkeep +0 -0
- package/src/ingestion/chunker.ts +277 -0
- package/src/ingestion/embedder.ts +167 -0
- package/src/ingestion/git-analyzer.ts +545 -0
- package/src/ingestion/indexer.ts +984 -0
- package/src/ingestion/markdown-chunker.ts +337 -0
- package/src/ingestion/markdown-knowledge.ts +175 -0
- package/src/ingestion/parser.ts +475 -0
- package/src/ingestion/watcher.ts +182 -0
- package/src/knowledge/.gitkeep +0 -0
- package/src/knowledge/hydrator.ts +246 -0
- package/src/knowledge/registry.ts +463 -0
- package/src/knowledge/search.ts +565 -0
- package/src/knowledge/store.ts +262 -0
- package/src/learning/.gitkeep +0 -0
- package/src/learning/confidence.ts +193 -0
- package/src/learning/patterns.ts +360 -0
- package/src/learning/trajectory.ts +268 -0
- package/src/memory/.gitkeep +0 -0
- package/src/memory/memory-compat.ts +233 -0
- package/src/memory/observation-store.ts +224 -0
- package/src/memory/session-tracker.ts +332 -0
- package/src/pipeline/.gitkeep +0 -0
- package/src/pipeline/engine.ts +1139 -0
- package/src/pipeline/events.ts +253 -0
- package/src/pipeline/parallel.ts +394 -0
- package/src/pipeline/state-machine.ts +199 -0
- package/src/query/.gitkeep +0 -0
- package/src/query/graph-queries.ts +262 -0
- package/src/query/hybrid-search.ts +337 -0
- package/src/query/intent-detector.ts +131 -0
- package/src/query/ranking.ts +161 -0
- package/src/server.ts +352 -0
- package/src/storage/.gitkeep +0 -0
- package/src/storage/falkordb-store.ts +388 -0
- package/src/storage/file-cache.ts +141 -0
- package/src/storage/interfaces.ts +201 -0
- package/src/storage/qdrant-store.ts +557 -0
- package/src/storage/schema.ts +139 -0
- package/src/storage/sqlite.ts +168 -0
- package/src/tools/.gitkeep +0 -0
- package/src/tools/collaboration-tools.ts +208 -0
- package/src/tools/context-tools.ts +493 -0
- package/src/tools/graph-tools.ts +295 -0
- package/src/tools/ingestion-tools.ts +122 -0
- package/src/tools/learning-tools.ts +181 -0
- package/src/tools/memory-tools.ts +234 -0
- package/src/tools/phase-tools.ts +1452 -0
- package/src/tools/pipeline-tools.ts +188 -0
- package/src/tools/registration-tools.ts +450 -0
- package/src/util/.gitkeep +0 -0
- package/src/util/circuit-breaker.ts +193 -0
- package/src/util/config.ts +177 -0
- package/src/util/logger.ts +53 -0
- package/src/util/token-counter.ts +52 -0
- package/src/util/types.ts +710 -0
- package/tests/context/.gitkeep +0 -0
- package/tests/integration/.gitkeep +0 -0
- package/tests/knowledge/.gitkeep +0 -0
- package/tests/learning/.gitkeep +0 -0
- package/tests/pipeline/.gitkeep +0 -0
- package/tests/tools/.gitkeep +0 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +10 -0
- package/vscode-extension/.vscodeignore +7 -0
- package/vscode-extension/README.md +43 -0
- package/vscode-extension/out/edge-collector.js +274 -0
- package/vscode-extension/out/edge-collector.js.map +1 -0
- package/vscode-extension/out/extension.js +264 -0
- package/vscode-extension/out/extension.js.map +1 -0
- package/vscode-extension/out/forge-client.js +318 -0
- package/vscode-extension/out/forge-client.js.map +1 -0
- package/vscode-extension/package-lock.json +59 -0
- package/vscode-extension/package.json +71 -0
- package/vscode-extension/src/edge-collector.ts +320 -0
- package/vscode-extension/src/extension.ts +269 -0
- package/vscode-extension/src/forge-client.ts +364 -0
- package/vscode-extension/tsconfig.json +19 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git history analyzer for co-modification edges (Phase 2 full implementation).
|
|
3
|
+
* Runs `git log --name-only` to find files changed together frequently,
|
|
4
|
+
* creates CO_MODIFIED edges, and detects test file relationships.
|
|
5
|
+
*
|
|
6
|
+
* Phase 3 additions:
|
|
7
|
+
* - analyzeFileStats(): per-file commit count, stability, velocity
|
|
8
|
+
* - extractCommitRecords(): parse git log into CommitRecord[]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { exec } from 'child_process';
|
|
12
|
+
import { promisify } from 'util';
|
|
13
|
+
import { logger } from '../util/logger.js';
|
|
14
|
+
import type { GraphStore } from '../storage/interfaces.js';
|
|
15
|
+
import type { FileChangeStats, CommitRecord } from '../util/types.js';
|
|
16
|
+
import { UPSERT_CO_MODIFIED } from '../query/graph-queries.js';
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
20
|
+
export interface CoModificationPair {
|
|
21
|
+
fileA: string;
|
|
22
|
+
fileB: string;
|
|
23
|
+
count: number;
|
|
24
|
+
lastCommit: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GitAnalysisResult {
|
|
28
|
+
coModifications: CoModificationPair[];
|
|
29
|
+
testPairs: Array<{ testFile: string; sourceFile: string }>;
|
|
30
|
+
totalCommitsAnalyzed: number;
|
|
31
|
+
errors: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Analyze git history to find files frequently changed together.
|
|
36
|
+
* Parses `git log --name-only` output for the last `historyDepth` commits,
|
|
37
|
+
* counts co-occurrence pairs, and returns pairs with count >= minComodCount.
|
|
38
|
+
*/
|
|
39
|
+
export async function analyzeCoModifications(
|
|
40
|
+
repoPath: string,
|
|
41
|
+
historyDepth: number = 500,
|
|
42
|
+
minComodCount: number = 3
|
|
43
|
+
): Promise<CoModificationPair[]> {
|
|
44
|
+
logger.debug('git-analyzer: starting co-modification analysis', { repoPath, historyDepth });
|
|
45
|
+
|
|
46
|
+
let stdout: string;
|
|
47
|
+
try {
|
|
48
|
+
const { stdout: out } = await execAsync(
|
|
49
|
+
`git -C "${repoPath}" log --name-only --format="%H" -n ${historyDepth}`,
|
|
50
|
+
{ maxBuffer: 50 * 1024 * 1024 } // 50 MB buffer for large repos
|
|
51
|
+
);
|
|
52
|
+
stdout = out;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.warn('git log failed — not a git repo or git not available', {
|
|
55
|
+
repoPath,
|
|
56
|
+
error: String(err),
|
|
57
|
+
});
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const commitGroups = parseGitLog(stdout);
|
|
62
|
+
const pairCounts = new Map<string, { count: number; lastCommit: string }>();
|
|
63
|
+
|
|
64
|
+
for (const { hash, files } of commitGroups) {
|
|
65
|
+
// Only consider commits with 2-50 files (ignore mega-commits like reformats)
|
|
66
|
+
if (files.length < 2 || files.length > 50) continue;
|
|
67
|
+
|
|
68
|
+
// Count every ordered pair (lexicographically sorted to ensure canonical key)
|
|
69
|
+
for (let i = 0; i < files.length; i++) {
|
|
70
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
71
|
+
const [a, b] = [files[i], files[j]].sort();
|
|
72
|
+
const key = `${a}\x00${b}`;
|
|
73
|
+
const existing = pairCounts.get(key);
|
|
74
|
+
if (existing) {
|
|
75
|
+
existing.count++;
|
|
76
|
+
existing.lastCommit = hash;
|
|
77
|
+
} else {
|
|
78
|
+
pairCounts.set(key, { count: 1, lastCommit: hash });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result: CoModificationPair[] = [];
|
|
85
|
+
for (const [key, { count, lastCommit }] of pairCounts) {
|
|
86
|
+
if (count >= minComodCount) {
|
|
87
|
+
const parts = key.split('\x00');
|
|
88
|
+
const fileA = parts[0] ?? '';
|
|
89
|
+
const fileB = parts[1] ?? '';
|
|
90
|
+
result.push({ fileA, fileB, count, lastCommit });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
logger.debug('git-analyzer: co-modification analysis complete', {
|
|
95
|
+
repoPath,
|
|
96
|
+
totalCommits: commitGroups.length,
|
|
97
|
+
uniquePairs: pairCounts.size,
|
|
98
|
+
qualifyingPairs: result.length,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse `git log --name-only --format="%H"` output into commit groups.
|
|
106
|
+
* Each group contains a commit hash and the list of files changed.
|
|
107
|
+
*/
|
|
108
|
+
export function parseGitLog(
|
|
109
|
+
stdout: string
|
|
110
|
+
): Array<{ hash: string; files: string[] }> {
|
|
111
|
+
const groups: Array<{ hash: string; files: string[] }> = [];
|
|
112
|
+
const lines = stdout.split('\n');
|
|
113
|
+
|
|
114
|
+
let currentHash = '';
|
|
115
|
+
let currentFiles: string[] = [];
|
|
116
|
+
|
|
117
|
+
for (const rawLine of lines) {
|
|
118
|
+
const line = rawLine.trim();
|
|
119
|
+
if (!line) {
|
|
120
|
+
// Blank line separates commits; flush current group
|
|
121
|
+
if (currentHash && currentFiles.length > 0) {
|
|
122
|
+
groups.push({ hash: currentHash, files: currentFiles });
|
|
123
|
+
currentFiles = [];
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A commit hash is exactly 40 hex chars
|
|
129
|
+
if (/^[0-9a-f]{40}$/i.test(line)) {
|
|
130
|
+
// Flush previous group before starting a new commit
|
|
131
|
+
if (currentHash && currentFiles.length > 0) {
|
|
132
|
+
groups.push({ hash: currentHash, files: currentFiles });
|
|
133
|
+
currentFiles = [];
|
|
134
|
+
}
|
|
135
|
+
currentHash = line;
|
|
136
|
+
} else {
|
|
137
|
+
// This is a file name line
|
|
138
|
+
if (currentHash) {
|
|
139
|
+
currentFiles.push(line);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Flush final group
|
|
145
|
+
if (currentHash && currentFiles.length > 0) {
|
|
146
|
+
groups.push({ hash: currentHash, files: currentFiles });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return groups;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Detect test files and their corresponding source files.
|
|
154
|
+
* Returns pairs: { testFile, sourceFile }
|
|
155
|
+
*
|
|
156
|
+
* Handles patterns:
|
|
157
|
+
* foo.spec.ts -> foo.ts
|
|
158
|
+
* foo.test.ts -> foo.ts
|
|
159
|
+
* __tests__/foo.ts -> ../foo.ts (sibling detection)
|
|
160
|
+
*/
|
|
161
|
+
export async function detectTestFiles(
|
|
162
|
+
files: string[]
|
|
163
|
+
): Promise<Array<{ testFile: string; sourceFile: string }>> {
|
|
164
|
+
const fileSet = new Set(files);
|
|
165
|
+
const pairs: Array<{ testFile: string; sourceFile: string }> = [];
|
|
166
|
+
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
// Pattern 1: foo.spec.{ext} -> foo.{ext}
|
|
169
|
+
const specMatch = file.match(/^(.+)\.spec\.(ts|tsx|js|jsx|mts|cts)$/);
|
|
170
|
+
if (specMatch) {
|
|
171
|
+
const [, base, ext] = specMatch;
|
|
172
|
+
const candidate = `${base}.${ext}`;
|
|
173
|
+
if (fileSet.has(candidate)) {
|
|
174
|
+
pairs.push({ testFile: file, sourceFile: candidate });
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Pattern 2: foo.test.{ext} -> foo.{ext}
|
|
180
|
+
const testMatch = file.match(/^(.+)\.test\.(ts|tsx|js|jsx|mts|cts)$/);
|
|
181
|
+
if (testMatch) {
|
|
182
|
+
const [, base, ext] = testMatch;
|
|
183
|
+
const candidate = `${base}.${ext}`;
|
|
184
|
+
if (fileSet.has(candidate)) {
|
|
185
|
+
pairs.push({ testFile: file, sourceFile: candidate });
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Pattern 3: __tests__/foo.ts -> ../foo.ts (try parent dir)
|
|
191
|
+
const testsMatch = file.match(/^(.*\/)__tests__\/([^/]+)$/);
|
|
192
|
+
if (testsMatch) {
|
|
193
|
+
const [, dir, filename] = testsMatch;
|
|
194
|
+
const candidate = `${dir}${filename}`;
|
|
195
|
+
if (fileSet.has(candidate)) {
|
|
196
|
+
pairs.push({ testFile: file, sourceFile: candidate });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return pairs;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Run full git analysis for a repo: co-modifications + test file detection.
|
|
206
|
+
* Then write the edges to FalkorDB.
|
|
207
|
+
*/
|
|
208
|
+
export async function runGitAnalysis(
|
|
209
|
+
repoPath: string,
|
|
210
|
+
repoId: string,
|
|
211
|
+
allFiles: string[],
|
|
212
|
+
graphStore: GraphStore,
|
|
213
|
+
historyDepth: number = 500,
|
|
214
|
+
minComodCount: number = 3
|
|
215
|
+
): Promise<GitAnalysisResult> {
|
|
216
|
+
const errors: string[] = [];
|
|
217
|
+
|
|
218
|
+
// 1. Analyze co-modifications from git log
|
|
219
|
+
let coModifications: CoModificationPair[] = [];
|
|
220
|
+
let totalCommitsAnalyzed = 0;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
coModifications = await analyzeCoModifications(repoPath, historyDepth, minComodCount);
|
|
224
|
+
|
|
225
|
+
// Count commits (re-parse log for count only)
|
|
226
|
+
try {
|
|
227
|
+
const { stdout } = await execAsync(
|
|
228
|
+
`git -C "${repoPath}" rev-list --count HEAD -n ${historyDepth}`,
|
|
229
|
+
{ maxBuffer: 1024 * 1024 }
|
|
230
|
+
);
|
|
231
|
+
totalCommitsAnalyzed = parseInt(stdout.trim(), 10) || 0;
|
|
232
|
+
} catch {
|
|
233
|
+
totalCommitsAnalyzed = 0;
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
errors.push(`Co-modification analysis failed: ${String(err)}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2. Write CO_MODIFIED edges to graph
|
|
240
|
+
for (const pair of coModifications) {
|
|
241
|
+
try {
|
|
242
|
+
await graphStore.query(
|
|
243
|
+
UPSERT_CO_MODIFIED(pair.fileA, pair.fileB, repoId, pair.count, pair.lastCommit)
|
|
244
|
+
);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
logger.warn('Failed to write CO_MODIFIED edge', {
|
|
247
|
+
fileA: pair.fileA,
|
|
248
|
+
fileB: pair.fileB,
|
|
249
|
+
error: String(err),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 3. Detect test pairs
|
|
255
|
+
const testPairs = await detectTestFiles(allFiles);
|
|
256
|
+
|
|
257
|
+
// 4. Write TESTS edges to graph
|
|
258
|
+
for (const { testFile, sourceFile } of testPairs) {
|
|
259
|
+
try {
|
|
260
|
+
await graphStore.upsertEdge(
|
|
261
|
+
'File', { path: testFile, repo_id: repoId },
|
|
262
|
+
'TESTS',
|
|
263
|
+
{},
|
|
264
|
+
'File', { path: sourceFile, repo_id: repoId }
|
|
265
|
+
);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
logger.warn('Failed to write TESTS edge', {
|
|
268
|
+
testFile,
|
|
269
|
+
sourceFile,
|
|
270
|
+
error: String(err),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
logger.info('git analysis complete', {
|
|
276
|
+
repoId,
|
|
277
|
+
coModPairs: coModifications.length,
|
|
278
|
+
testPairs: testPairs.length,
|
|
279
|
+
errors: errors.length,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return { coModifications, testPairs, totalCommitsAnalyzed, errors };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ============================================================
|
|
286
|
+
// Phase 3: Git History Enrichment
|
|
287
|
+
// ============================================================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Analyze per-file change statistics from git history.
|
|
291
|
+
* Runs a second git log with `%H|%s|%an|%aI` format (hash, subject, author, ISO date)
|
|
292
|
+
* plus `--name-only` to map commits to files.
|
|
293
|
+
*
|
|
294
|
+
* Computes per-file:
|
|
295
|
+
* - commitCount: total commits touching this file
|
|
296
|
+
* - stabilityScore: 0-1, higher = fewer recent changes relative to history
|
|
297
|
+
* - changeVelocity: -1 to +1, positive = accelerating changes
|
|
298
|
+
* - lastCommitHash, lastCommitTs, recentMessages (last 5)
|
|
299
|
+
*/
|
|
300
|
+
export async function analyzeFileStats(
|
|
301
|
+
repoPath: string,
|
|
302
|
+
historyDepth: number = 500
|
|
303
|
+
): Promise<Map<string, FileChangeStats>> {
|
|
304
|
+
const normalizedPath = repoPath.replace(/\\/g, '/');
|
|
305
|
+
logger.debug('git-analyzer: starting file stats analysis', { repoPath: normalizedPath, historyDepth });
|
|
306
|
+
|
|
307
|
+
let stdout: string;
|
|
308
|
+
try {
|
|
309
|
+
const { stdout: out } = await execAsync(
|
|
310
|
+
`git -C "${normalizedPath}" log --format="%H|%s|%an|%aI" --name-only -n ${historyDepth}`,
|
|
311
|
+
{ maxBuffer: 50 * 1024 * 1024 }
|
|
312
|
+
);
|
|
313
|
+
stdout = out;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
logger.warn('git log (file stats) failed', { repoPath: normalizedPath, error: String(err) });
|
|
316
|
+
return new Map();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Parse into commit records with files
|
|
320
|
+
const commits = parseStatsGitLog(stdout);
|
|
321
|
+
|
|
322
|
+
// Build per-file stats
|
|
323
|
+
const fileStatsMap = new Map<string, {
|
|
324
|
+
commitCount: number;
|
|
325
|
+
commitTimestamps: number[];
|
|
326
|
+
lastCommitHash: string;
|
|
327
|
+
lastCommitTs: number;
|
|
328
|
+
recentMessages: string[];
|
|
329
|
+
}>();
|
|
330
|
+
|
|
331
|
+
for (const commit of commits) {
|
|
332
|
+
const ts = commit.timestamp;
|
|
333
|
+
|
|
334
|
+
for (const file of commit.files) {
|
|
335
|
+
const existing = fileStatsMap.get(file);
|
|
336
|
+
if (existing) {
|
|
337
|
+
existing.commitCount++;
|
|
338
|
+
existing.commitTimestamps.push(ts);
|
|
339
|
+
if (ts > existing.lastCommitTs) {
|
|
340
|
+
existing.lastCommitHash = commit.hash;
|
|
341
|
+
existing.lastCommitTs = ts;
|
|
342
|
+
}
|
|
343
|
+
// Keep only last 5 messages
|
|
344
|
+
if (existing.recentMessages.length < 5) {
|
|
345
|
+
existing.recentMessages.push(commit.message);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
fileStatsMap.set(file, {
|
|
349
|
+
commitCount: 1,
|
|
350
|
+
commitTimestamps: [ts],
|
|
351
|
+
lastCommitHash: commit.hash,
|
|
352
|
+
lastCommitTs: ts,
|
|
353
|
+
recentMessages: [commit.message],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Compute stability and velocity for each file
|
|
360
|
+
const result = new Map<string, FileChangeStats>();
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
|
|
363
|
+
for (const [filePath, raw] of fileStatsMap) {
|
|
364
|
+
const stability = computeStabilityScore(raw.commitTimestamps, now);
|
|
365
|
+
const velocity = computeChangeVelocity(raw.commitTimestamps);
|
|
366
|
+
|
|
367
|
+
result.set(filePath, {
|
|
368
|
+
commitCount: raw.commitCount,
|
|
369
|
+
stabilityScore: stability,
|
|
370
|
+
changeVelocity: velocity,
|
|
371
|
+
lastCommitHash: raw.lastCommitHash,
|
|
372
|
+
lastCommitTs: raw.lastCommitTs,
|
|
373
|
+
recentMessages: raw.recentMessages,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
logger.debug('git-analyzer: file stats analysis complete', {
|
|
378
|
+
repoPath: normalizedPath,
|
|
379
|
+
filesWithStats: result.size,
|
|
380
|
+
totalCommits: commits.length,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Extract commit records from git history.
|
|
388
|
+
* Parses git log into CommitRecord[], filtering merge commits and
|
|
389
|
+
* bulk reformats (commits touching >50 files).
|
|
390
|
+
*/
|
|
391
|
+
export async function extractCommitRecords(
|
|
392
|
+
repoPath: string,
|
|
393
|
+
historyDepth: number = 500
|
|
394
|
+
): Promise<CommitRecord[]> {
|
|
395
|
+
const normalizedPath = repoPath.replace(/\\/g, '/');
|
|
396
|
+
logger.debug('git-analyzer: extracting commit records', { repoPath: normalizedPath, historyDepth });
|
|
397
|
+
|
|
398
|
+
let stdout: string;
|
|
399
|
+
try {
|
|
400
|
+
const { stdout: out } = await execAsync(
|
|
401
|
+
`git -C "${normalizedPath}" log --format="%H|%s|%an|%aI" --name-only -n ${historyDepth}`,
|
|
402
|
+
{ maxBuffer: 50 * 1024 * 1024 }
|
|
403
|
+
);
|
|
404
|
+
stdout = out;
|
|
405
|
+
} catch (err) {
|
|
406
|
+
logger.warn('git log (commit records) failed', { repoPath: normalizedPath, error: String(err) });
|
|
407
|
+
return [];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const allCommits = parseStatsGitLog(stdout);
|
|
411
|
+
|
|
412
|
+
// Filter out merge commits and bulk reformats (>50 files)
|
|
413
|
+
const filtered = allCommits.filter(c => {
|
|
414
|
+
// Skip merge commits (common patterns)
|
|
415
|
+
const lowerMsg = c.message.toLowerCase();
|
|
416
|
+
if (lowerMsg.startsWith('merge ') || lowerMsg.startsWith('merge:')) return false;
|
|
417
|
+
|
|
418
|
+
// Skip bulk reformats (>50 files changed)
|
|
419
|
+
if (c.files.length > 50) return false;
|
|
420
|
+
|
|
421
|
+
// Skip commits with no files (empty commits)
|
|
422
|
+
if (c.files.length === 0) return false;
|
|
423
|
+
|
|
424
|
+
return true;
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
logger.debug('git-analyzer: commit records extracted', {
|
|
428
|
+
repoPath: normalizedPath,
|
|
429
|
+
totalCommits: allCommits.length,
|
|
430
|
+
filteredCommits: filtered.length,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return filtered;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Parse `git log --format="%H|%s|%an|%aI" --name-only` output into commit records.
|
|
438
|
+
* Each commit block is: header line (hash|subject|author|date) followed by blank + file lines.
|
|
439
|
+
*/
|
|
440
|
+
function parseStatsGitLog(stdout: string): CommitRecord[] {
|
|
441
|
+
const commits: CommitRecord[] = [];
|
|
442
|
+
const lines = stdout.split('\n');
|
|
443
|
+
|
|
444
|
+
let currentCommit: CommitRecord | null = null;
|
|
445
|
+
|
|
446
|
+
for (const rawLine of lines) {
|
|
447
|
+
const line = rawLine.trim();
|
|
448
|
+
|
|
449
|
+
if (!line) {
|
|
450
|
+
// Blank line separates commit header from files, or between commits
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Try to parse as a commit header (hash|subject|author|date)
|
|
455
|
+
const pipeIndex = line.indexOf('|');
|
|
456
|
+
if (pipeIndex === 40 && /^[0-9a-f]{40}$/i.test(line.slice(0, 40))) {
|
|
457
|
+
// Flush previous commit
|
|
458
|
+
if (currentCommit) {
|
|
459
|
+
commits.push(currentCommit);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const parts = line.split('|');
|
|
463
|
+
const hash = parts[0] ?? '';
|
|
464
|
+
const message = parts[1] ?? '';
|
|
465
|
+
const author = parts[2] ?? '';
|
|
466
|
+
const dateStr = parts.slice(3).join('|'); // ISO date may contain no pipes but be safe
|
|
467
|
+
|
|
468
|
+
currentCommit = {
|
|
469
|
+
hash,
|
|
470
|
+
message,
|
|
471
|
+
author,
|
|
472
|
+
timestamp: dateStr ? new Date(dateStr).getTime() : 0,
|
|
473
|
+
files: [],
|
|
474
|
+
};
|
|
475
|
+
} else if (currentCommit) {
|
|
476
|
+
// This is a file path line
|
|
477
|
+
currentCommit.files.push(line);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Flush final commit
|
|
482
|
+
if (currentCommit) {
|
|
483
|
+
commits.push(currentCommit);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return commits;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Compute stability score (0-1). Higher = more stable (fewer recent changes).
|
|
491
|
+
* Uses the ratio of commits in the recent half of the time window vs total commits.
|
|
492
|
+
* A file with all changes in the distant past is stable (score ~1.0).
|
|
493
|
+
* A file with all changes recently is unstable (score ~0.0).
|
|
494
|
+
*/
|
|
495
|
+
function computeStabilityScore(timestamps: number[], now: number): number {
|
|
496
|
+
if (timestamps.length <= 1) return 1.0;
|
|
497
|
+
|
|
498
|
+
const sorted = [...timestamps].sort((a, b) => a - b);
|
|
499
|
+
const oldest = sorted[0]!;
|
|
500
|
+
const timeRange = now - oldest;
|
|
501
|
+
if (timeRange <= 0) return 1.0;
|
|
502
|
+
|
|
503
|
+
const midpoint = oldest + timeRange / 2;
|
|
504
|
+
const recentCount = sorted.filter(t => t > midpoint).length;
|
|
505
|
+
const recentRatio = recentCount / sorted.length;
|
|
506
|
+
|
|
507
|
+
// Invert: fewer recent changes = more stable
|
|
508
|
+
return Math.max(0, Math.min(1, 1.0 - recentRatio));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Compute change velocity (-1 to +1).
|
|
513
|
+
* Compares the rate of changes in the first half vs second half of the commit history.
|
|
514
|
+
* Positive = changes are accelerating. Negative = changes are decelerating.
|
|
515
|
+
*/
|
|
516
|
+
function computeChangeVelocity(timestamps: number[]): number {
|
|
517
|
+
if (timestamps.length < 4) return 0;
|
|
518
|
+
|
|
519
|
+
const sorted = [...timestamps].sort((a, b) => a - b);
|
|
520
|
+
const mid = Math.floor(sorted.length / 2);
|
|
521
|
+
const firstHalf = sorted.slice(0, mid);
|
|
522
|
+
const secondHalf = sorted.slice(mid);
|
|
523
|
+
|
|
524
|
+
// Compute average inter-commit interval for each half
|
|
525
|
+
const avgInterval = (arr: number[]): number => {
|
|
526
|
+
if (arr.length < 2) return Infinity;
|
|
527
|
+
let totalGap = 0;
|
|
528
|
+
for (let i = 1; i < arr.length; i++) {
|
|
529
|
+
totalGap += arr[i]! - arr[i - 1]!;
|
|
530
|
+
}
|
|
531
|
+
return totalGap / (arr.length - 1);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const firstAvg = avgInterval(firstHalf);
|
|
535
|
+
const secondAvg = avgInterval(secondHalf);
|
|
536
|
+
|
|
537
|
+
if (firstAvg === Infinity && secondAvg === Infinity) return 0;
|
|
538
|
+
if (firstAvg === Infinity) return 1; // No first-half activity, all second-half
|
|
539
|
+
if (secondAvg === Infinity) return -1; // No second-half activity
|
|
540
|
+
|
|
541
|
+
// Shorter intervals in second half = accelerating
|
|
542
|
+
// Normalize to -1..+1 range
|
|
543
|
+
const ratio = (firstAvg - secondAvg) / Math.max(firstAvg, secondAvg);
|
|
544
|
+
return Math.max(-1, Math.min(1, ratio));
|
|
545
|
+
}
|