openlore 2.0.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/api/analyze.d.ts +17 -0
- package/dist/api/analyze.d.ts.map +1 -0
- package/dist/api/analyze.js +143 -0
- package/dist/api/analyze.js.map +1 -0
- package/dist/api/audit.d.ts +10 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +117 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/decisions.d.ts +55 -0
- package/dist/api/decisions.d.ts.map +1 -0
- package/dist/api/decisions.js +157 -0
- package/dist/api/decisions.js.map +1 -0
- package/dist/api/drift.d.ts +21 -0
- package/dist/api/drift.d.ts.map +1 -0
- package/dist/api/drift.js +152 -0
- package/dist/api/drift.js.map +1 -0
- package/dist/api/generate.d.ts +18 -0
- package/dist/api/generate.d.ts.map +1 -0
- package/dist/api/generate.js +259 -0
- package/dist/api/generate.js.map +1 -0
- package/dist/api/index.d.ts +41 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +34 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/init.d.ts +18 -0
- package/dist/api/init.d.ts.map +1 -0
- package/dist/api/init.js +83 -0
- package/dist/api/init.js.map +1 -0
- package/dist/api/run.d.ts +19 -0
- package/dist/api/run.d.ts.map +1 -0
- package/dist/api/run.js +312 -0
- package/dist/api/run.js.map +1 -0
- package/dist/api/specs.d.ts +49 -0
- package/dist/api/specs.d.ts.map +1 -0
- package/dist/api/specs.js +137 -0
- package/dist/api/specs.js.map +1 -0
- package/dist/api/types.d.ts +201 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +9 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/verify.d.ts +20 -0
- package/dist/api/verify.d.ts.map +1 -0
- package/dist/api/verify.js +117 -0
- package/dist/api/verify.js.map +1 -0
- package/dist/cli/commands/analyze.d.ts +30 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -0
- package/dist/cli/commands/analyze.js +683 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/audit.d.ts +9 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +98 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/decisions.d.ts +16 -0
- package/dist/cli/commands/decisions.d.ts.map +1 -0
- package/dist/cli/commands/decisions.js +864 -0
- package/dist/cli/commands/decisions.js.map +1 -0
- package/dist/cli/commands/digest.d.ts +9 -0
- package/dist/cli/commands/digest.d.ts.map +1 -0
- package/dist/cli/commands/digest.js +61 -0
- package/dist/cli/commands/digest.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +9 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +398 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/drift.d.ts +9 -0
- package/dist/cli/commands/drift.d.ts.map +1 -0
- package/dist/cli/commands/drift.js +550 -0
- package/dist/cli/commands/drift.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +9 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +565 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +173 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +2235 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +1384 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/refresh-stories.d.ts +10 -0
- package/dist/cli/commands/refresh-stories.d.ts.map +1 -0
- package/dist/cli/commands/refresh-stories.js +314 -0
- package/dist/cli/commands/refresh-stories.js.map +1 -0
- package/dist/cli/commands/run.d.ts +9 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +459 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +19 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +355 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/test.d.ts +22 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/test.js +180 -0
- package/dist/cli/commands/test.js.map +1 -0
- package/dist/cli/commands/verify.d.ts +9 -0
- package/dist/cli/commands/verify.d.ts.map +1 -0
- package/dist/cli/commands/verify.js +383 -0
- package/dist/cli/commands/verify.js.map +1 -0
- package/dist/cli/commands/view.d.ts +13 -0
- package/dist/cli/commands/view.d.ts.map +1 -0
- package/dist/cli/commands/view.js +547 -0
- package/dist/cli/commands/view.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +118 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/tui-approval.d.ts +11 -0
- package/dist/cli/tui-approval.d.ts.map +1 -0
- package/dist/cli/tui-approval.js +129 -0
- package/dist/cli/tui-approval.js.map +1 -0
- package/dist/constants.d.ts +314 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +382 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/analyzer/ai-config-generator.d.ts +54 -0
- package/dist/core/analyzer/ai-config-generator.d.ts.map +1 -0
- package/dist/core/analyzer/ai-config-generator.js +98 -0
- package/dist/core/analyzer/ai-config-generator.js.map +1 -0
- package/dist/core/analyzer/architecture-writer.d.ts +67 -0
- package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
- package/dist/core/analyzer/architecture-writer.js +209 -0
- package/dist/core/analyzer/architecture-writer.js.map +1 -0
- package/dist/core/analyzer/artifact-generator.d.ts +261 -0
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
- package/dist/core/analyzer/artifact-generator.js +909 -0
- package/dist/core/analyzer/artifact-generator.js.map +1 -0
- package/dist/core/analyzer/ast-chunker.d.ts +24 -0
- package/dist/core/analyzer/ast-chunker.d.ts.map +1 -0
- package/dist/core/analyzer/ast-chunker.js +198 -0
- package/dist/core/analyzer/ast-chunker.js.map +1 -0
- package/dist/core/analyzer/call-graph.d.ts +162 -0
- package/dist/core/analyzer/call-graph.d.ts.map +1 -0
- package/dist/core/analyzer/call-graph.js +2040 -0
- package/dist/core/analyzer/call-graph.js.map +1 -0
- package/dist/core/analyzer/code-shaper.d.ts +33 -0
- package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
- package/dist/core/analyzer/code-shaper.js +154 -0
- package/dist/core/analyzer/code-shaper.js.map +1 -0
- package/dist/core/analyzer/codebase-digest.d.ts +40 -0
- package/dist/core/analyzer/codebase-digest.d.ts.map +1 -0
- package/dist/core/analyzer/codebase-digest.js +195 -0
- package/dist/core/analyzer/codebase-digest.js.map +1 -0
- package/dist/core/analyzer/cpp-header-resolver.d.ts +30 -0
- package/dist/core/analyzer/cpp-header-resolver.d.ts.map +1 -0
- package/dist/core/analyzer/cpp-header-resolver.js +71 -0
- package/dist/core/analyzer/cpp-header-resolver.js.map +1 -0
- package/dist/core/analyzer/dependency-graph.d.ts +230 -0
- package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
- package/dist/core/analyzer/dependency-graph.js +752 -0
- package/dist/core/analyzer/dependency-graph.js.map +1 -0
- package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
- package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
- package/dist/core/analyzer/duplicate-detector.js +289 -0
- package/dist/core/analyzer/duplicate-detector.js.map +1 -0
- package/dist/core/analyzer/embedding-service.d.ts +56 -0
- package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
- package/dist/core/analyzer/embedding-service.js +118 -0
- package/dist/core/analyzer/embedding-service.js.map +1 -0
- package/dist/core/analyzer/env-extractor.d.ts +33 -0
- package/dist/core/analyzer/env-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/env-extractor.js +196 -0
- package/dist/core/analyzer/env-extractor.js.map +1 -0
- package/dist/core/analyzer/external-packages.d.ts +20 -0
- package/dist/core/analyzer/external-packages.d.ts.map +1 -0
- package/dist/core/analyzer/external-packages.js +175 -0
- package/dist/core/analyzer/external-packages.js.map +1 -0
- package/dist/core/analyzer/file-walker.d.ts +78 -0
- package/dist/core/analyzer/file-walker.d.ts.map +1 -0
- package/dist/core/analyzer/file-walker.js +532 -0
- package/dist/core/analyzer/file-walker.js.map +1 -0
- package/dist/core/analyzer/function-registry-trie.d.ts +21 -0
- package/dist/core/analyzer/function-registry-trie.d.ts.map +1 -0
- package/dist/core/analyzer/function-registry-trie.js +39 -0
- package/dist/core/analyzer/function-registry-trie.js.map +1 -0
- package/dist/core/analyzer/http-route-parser.d.ts +152 -0
- package/dist/core/analyzer/http-route-parser.d.ts.map +1 -0
- package/dist/core/analyzer/http-route-parser.js +971 -0
- package/dist/core/analyzer/http-route-parser.js.map +1 -0
- package/dist/core/analyzer/import-parser.d.ts +100 -0
- package/dist/core/analyzer/import-parser.d.ts.map +1 -0
- package/dist/core/analyzer/import-parser.js +952 -0
- package/dist/core/analyzer/import-parser.js.map +1 -0
- package/dist/core/analyzer/import-resolver-bridge.d.ts +25 -0
- package/dist/core/analyzer/import-resolver-bridge.d.ts.map +1 -0
- package/dist/core/analyzer/import-resolver-bridge.js +99 -0
- package/dist/core/analyzer/import-resolver-bridge.js.map +1 -0
- package/dist/core/analyzer/index.d.ts +10 -0
- package/dist/core/analyzer/index.d.ts.map +1 -0
- package/dist/core/analyzer/index.js +10 -0
- package/dist/core/analyzer/index.js.map +1 -0
- package/dist/core/analyzer/middleware-extractor.d.ts +29 -0
- package/dist/core/analyzer/middleware-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/middleware-extractor.js +195 -0
- package/dist/core/analyzer/middleware-extractor.js.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts +83 -0
- package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
- package/dist/core/analyzer/refactor-analyzer.js +351 -0
- package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
- package/dist/core/analyzer/repository-mapper.d.ts +150 -0
- package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
- package/dist/core/analyzer/repository-mapper.js +740 -0
- package/dist/core/analyzer/repository-mapper.js.map +1 -0
- package/dist/core/analyzer/schema-extractor.d.ts +41 -0
- package/dist/core/analyzer/schema-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/schema-extractor.js +229 -0
- package/dist/core/analyzer/schema-extractor.js.map +1 -0
- package/dist/core/analyzer/signature-extractor.d.ts +31 -0
- package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/signature-extractor.js +675 -0
- package/dist/core/analyzer/signature-extractor.js.map +1 -0
- package/dist/core/analyzer/significance-scorer.d.ts +79 -0
- package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
- package/dist/core/analyzer/significance-scorer.js +407 -0
- package/dist/core/analyzer/significance-scorer.js.map +1 -0
- package/dist/core/analyzer/spec-snapshot-generator.d.ts +17 -0
- package/dist/core/analyzer/spec-snapshot-generator.d.ts.map +1 -0
- package/dist/core/analyzer/spec-snapshot-generator.js +201 -0
- package/dist/core/analyzer/spec-snapshot-generator.js.map +1 -0
- package/dist/core/analyzer/spec-vector-index.d.ts +68 -0
- package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -0
- package/dist/core/analyzer/spec-vector-index.js +340 -0
- package/dist/core/analyzer/spec-vector-index.js.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts +51 -0
- package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/subgraph-extractor.js +147 -0
- package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
- package/dist/core/analyzer/type-inference-engine.d.ts +23 -0
- package/dist/core/analyzer/type-inference-engine.d.ts.map +1 -0
- package/dist/core/analyzer/type-inference-engine.js +130 -0
- package/dist/core/analyzer/type-inference-engine.js.map +1 -0
- package/dist/core/analyzer/ui-component-extractor.d.ts +43 -0
- package/dist/core/analyzer/ui-component-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/ui-component-extractor.js +245 -0
- package/dist/core/analyzer/ui-component-extractor.js.map +1 -0
- package/dist/core/analyzer/unified-search.d.ts +116 -0
- package/dist/core/analyzer/unified-search.d.ts.map +1 -0
- package/dist/core/analyzer/unified-search.js +231 -0
- package/dist/core/analyzer/unified-search.js.map +1 -0
- package/dist/core/analyzer/vector-index.d.ts +92 -0
- package/dist/core/analyzer/vector-index.d.ts.map +1 -0
- package/dist/core/analyzer/vector-index.js +451 -0
- package/dist/core/analyzer/vector-index.js.map +1 -0
- package/dist/core/decisions/consolidator.d.ts +14 -0
- package/dist/core/decisions/consolidator.d.ts.map +1 -0
- package/dist/core/decisions/consolidator.js +169 -0
- package/dist/core/decisions/consolidator.js.map +1 -0
- package/dist/core/decisions/extractor.d.ts +26 -0
- package/dist/core/decisions/extractor.d.ts.map +1 -0
- package/dist/core/decisions/extractor.js +156 -0
- package/dist/core/decisions/extractor.js.map +1 -0
- package/dist/core/decisions/index.d.ts +19 -0
- package/dist/core/decisions/index.d.ts.map +1 -0
- package/dist/core/decisions/index.js +16 -0
- package/dist/core/decisions/index.js.map +1 -0
- package/dist/core/decisions/store.d.ts +36 -0
- package/dist/core/decisions/store.d.ts.map +1 -0
- package/dist/core/decisions/store.js +109 -0
- package/dist/core/decisions/store.js.map +1 -0
- package/dist/core/decisions/syncer.d.ts +27 -0
- package/dist/core/decisions/syncer.d.ts.map +1 -0
- package/dist/core/decisions/syncer.js +214 -0
- package/dist/core/decisions/syncer.js.map +1 -0
- package/dist/core/decisions/verifier.d.ts +20 -0
- package/dist/core/decisions/verifier.d.ts.map +1 -0
- package/dist/core/decisions/verifier.js +115 -0
- package/dist/core/decisions/verifier.js.map +1 -0
- package/dist/core/digest/digest-generator.d.ts +29 -0
- package/dist/core/digest/digest-generator.d.ts.map +1 -0
- package/dist/core/digest/digest-generator.js +181 -0
- package/dist/core/digest/digest-generator.js.map +1 -0
- package/dist/core/drift/drift-detector.d.ts +102 -0
- package/dist/core/drift/drift-detector.d.ts.map +1 -0
- package/dist/core/drift/drift-detector.js +598 -0
- package/dist/core/drift/drift-detector.js.map +1 -0
- package/dist/core/drift/git-diff.d.ts +60 -0
- package/dist/core/drift/git-diff.d.ts.map +1 -0
- package/dist/core/drift/git-diff.js +383 -0
- package/dist/core/drift/git-diff.js.map +1 -0
- package/dist/core/drift/index.d.ts +12 -0
- package/dist/core/drift/index.d.ts.map +1 -0
- package/dist/core/drift/index.js +9 -0
- package/dist/core/drift/index.js.map +1 -0
- package/dist/core/drift/spec-mapper.d.ts +73 -0
- package/dist/core/drift/spec-mapper.d.ts.map +1 -0
- package/dist/core/drift/spec-mapper.js +353 -0
- package/dist/core/drift/spec-mapper.js.map +1 -0
- package/dist/core/drift/test-suggester.d.ts +18 -0
- package/dist/core/drift/test-suggester.d.ts.map +1 -0
- package/dist/core/drift/test-suggester.js +107 -0
- package/dist/core/drift/test-suggester.js.map +1 -0
- package/dist/core/generator/adr-generator.d.ts +32 -0
- package/dist/core/generator/adr-generator.d.ts.map +1 -0
- package/dist/core/generator/adr-generator.js +192 -0
- package/dist/core/generator/adr-generator.js.map +1 -0
- package/dist/core/generator/index.d.ts +9 -0
- package/dist/core/generator/index.d.ts.map +1 -0
- package/dist/core/generator/index.js +12 -0
- package/dist/core/generator/index.js.map +1 -0
- package/dist/core/generator/mapping-generator.d.ts +54 -0
- package/dist/core/generator/mapping-generator.d.ts.map +1 -0
- package/dist/core/generator/mapping-generator.js +240 -0
- package/dist/core/generator/mapping-generator.js.map +1 -0
- package/dist/core/generator/openspec-compat.d.ts +160 -0
- package/dist/core/generator/openspec-compat.d.ts.map +1 -0
- package/dist/core/generator/openspec-compat.js +524 -0
- package/dist/core/generator/openspec-compat.js.map +1 -0
- package/dist/core/generator/openspec-format-generator.d.ts +131 -0
- package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
- package/dist/core/generator/openspec-format-generator.js +963 -0
- package/dist/core/generator/openspec-format-generator.js.map +1 -0
- package/dist/core/generator/openspec-writer.d.ts +130 -0
- package/dist/core/generator/openspec-writer.d.ts.map +1 -0
- package/dist/core/generator/openspec-writer.js +404 -0
- package/dist/core/generator/openspec-writer.js.map +1 -0
- package/dist/core/generator/prompts.d.ts +35 -0
- package/dist/core/generator/prompts.d.ts.map +1 -0
- package/dist/core/generator/prompts.js +212 -0
- package/dist/core/generator/prompts.js.map +1 -0
- package/dist/core/generator/rag-manifest-generator.d.ts +37 -0
- package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
- package/dist/core/generator/rag-manifest-generator.js +134 -0
- package/dist/core/generator/rag-manifest-generator.js.map +1 -0
- package/dist/core/generator/schemas.d.ts +365 -0
- package/dist/core/generator/schemas.d.ts.map +1 -0
- package/dist/core/generator/schemas.js +190 -0
- package/dist/core/generator/schemas.js.map +1 -0
- package/dist/core/generator/spec-pipeline.d.ts +123 -0
- package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
- package/dist/core/generator/spec-pipeline.js +699 -0
- package/dist/core/generator/spec-pipeline.js.map +1 -0
- package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
- package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
- package/dist/core/generator/stages/stage1-survey.js +171 -0
- package/dist/core/generator/stages/stage1-survey.js.map +1 -0
- package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
- package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
- package/dist/core/generator/stages/stage2-entities.js +74 -0
- package/dist/core/generator/stages/stage2-entities.js.map +1 -0
- package/dist/core/generator/stages/stage3-services.d.ts +11 -0
- package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
- package/dist/core/generator/stages/stage3-services.js +85 -0
- package/dist/core/generator/stages/stage3-services.js.map +1 -0
- package/dist/core/generator/stages/stage4-api.d.ts +11 -0
- package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
- package/dist/core/generator/stages/stage4-api.js +72 -0
- package/dist/core/generator/stages/stage4-api.js.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts +11 -0
- package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
- package/dist/core/generator/stages/stage5-architecture.js +75 -0
- package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
- package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
- package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
- package/dist/core/generator/stages/stage6-adr.js +47 -0
- package/dist/core/generator/stages/stage6-adr.js.map +1 -0
- package/dist/core/services/chat-agent.d.ts +50 -0
- package/dist/core/services/chat-agent.d.ts.map +1 -0
- package/dist/core/services/chat-agent.js +369 -0
- package/dist/core/services/chat-agent.js.map +1 -0
- package/dist/core/services/chat-tools.d.ts +32 -0
- package/dist/core/services/chat-tools.d.ts.map +1 -0
- package/dist/core/services/chat-tools.js +494 -0
- package/dist/core/services/chat-tools.js.map +1 -0
- package/dist/core/services/config-manager.d.ts +61 -0
- package/dist/core/services/config-manager.d.ts.map +1 -0
- package/dist/core/services/config-manager.js +149 -0
- package/dist/core/services/config-manager.js.map +1 -0
- package/dist/core/services/edge-store.d.ts +57 -0
- package/dist/core/services/edge-store.d.ts.map +1 -0
- package/dist/core/services/edge-store.js +419 -0
- package/dist/core/services/edge-store.js.map +1 -0
- package/dist/core/services/gitignore-manager.d.ts +29 -0
- package/dist/core/services/gitignore-manager.d.ts.map +1 -0
- package/dist/core/services/gitignore-manager.js +95 -0
- package/dist/core/services/gitignore-manager.js.map +1 -0
- package/dist/core/services/index.d.ts +8 -0
- package/dist/core/services/index.d.ts.map +1 -0
- package/dist/core/services/index.js +8 -0
- package/dist/core/services/index.js.map +1 -0
- package/dist/core/services/llm-service.d.ts +379 -0
- package/dist/core/services/llm-service.d.ts.map +1 -0
- package/dist/core/services/llm-service.js +1553 -0
- package/dist/core/services/llm-service.js.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts +127 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/analysis.js +1185 -0
- package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
- package/dist/core/services/mcp-handlers/change.d.ts +14 -0
- package/dist/core/services/mcp-handlers/change.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/change.js +416 -0
- package/dist/core/services/mcp-handlers/change.js.map +1 -0
- package/dist/core/services/mcp-handlers/decisions.d.ts +16 -0
- package/dist/core/services/mcp-handlers/decisions.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/decisions.js +239 -0
- package/dist/core/services/mcp-handlers/decisions.js.map +1 -0
- package/dist/core/services/mcp-handlers/graph.d.ts +94 -0
- package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/graph.js +693 -0
- package/dist/core/services/mcp-handlers/graph.js.map +1 -0
- package/dist/core/services/mcp-handlers/orient.d.ts +17 -0
- package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/orient.js +357 -0
- package/dist/core/services/mcp-handlers/orient.js.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts +66 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.js +432 -0
- package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
- package/dist/core/services/mcp-handlers/utils.d.ts +85 -0
- package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/utils.js +262 -0
- package/dist/core/services/mcp-handlers/utils.js.map +1 -0
- package/dist/core/services/mcp-watcher.d.ts +41 -0
- package/dist/core/services/mcp-watcher.d.ts.map +1 -0
- package/dist/core/services/mcp-watcher.js +254 -0
- package/dist/core/services/mcp-watcher.js.map +1 -0
- package/dist/core/services/project-detector.d.ts +32 -0
- package/dist/core/services/project-detector.d.ts.map +1 -0
- package/dist/core/services/project-detector.js +100 -0
- package/dist/core/services/project-detector.js.map +1 -0
- package/dist/core/test-generator/coverage-analyzer.d.ts +27 -0
- package/dist/core/test-generator/coverage-analyzer.d.ts.map +1 -0
- package/dist/core/test-generator/coverage-analyzer.js +285 -0
- package/dist/core/test-generator/coverage-analyzer.js.map +1 -0
- package/dist/core/test-generator/framework-detector.d.ts +17 -0
- package/dist/core/test-generator/framework-detector.d.ts.map +1 -0
- package/dist/core/test-generator/framework-detector.js +65 -0
- package/dist/core/test-generator/framework-detector.js.map +1 -0
- package/dist/core/test-generator/index.d.ts +14 -0
- package/dist/core/test-generator/index.d.ts.map +1 -0
- package/dist/core/test-generator/index.js +11 -0
- package/dist/core/test-generator/index.js.map +1 -0
- package/dist/core/test-generator/renderers/catch2.d.ts +8 -0
- package/dist/core/test-generator/renderers/catch2.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/catch2.js +47 -0
- package/dist/core/test-generator/renderers/catch2.js.map +1 -0
- package/dist/core/test-generator/renderers/gtest.d.ts +8 -0
- package/dist/core/test-generator/renderers/gtest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/gtest.js +45 -0
- package/dist/core/test-generator/renderers/gtest.js.map +1 -0
- package/dist/core/test-generator/renderers/index.d.ts +20 -0
- package/dist/core/test-generator/renderers/index.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/index.js +35 -0
- package/dist/core/test-generator/renderers/index.js.map +1 -0
- package/dist/core/test-generator/renderers/playwright.d.ts +8 -0
- package/dist/core/test-generator/renderers/playwright.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/playwright.js +44 -0
- package/dist/core/test-generator/renderers/playwright.js.map +1 -0
- package/dist/core/test-generator/renderers/pytest.d.ts +8 -0
- package/dist/core/test-generator/renderers/pytest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/pytest.js +44 -0
- package/dist/core/test-generator/renderers/pytest.js.map +1 -0
- package/dist/core/test-generator/renderers/shared.d.ts +21 -0
- package/dist/core/test-generator/renderers/shared.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/shared.js +56 -0
- package/dist/core/test-generator/renderers/shared.js.map +1 -0
- package/dist/core/test-generator/renderers/vitest.d.ts +8 -0
- package/dist/core/test-generator/renderers/vitest.d.ts.map +1 -0
- package/dist/core/test-generator/renderers/vitest.js +52 -0
- package/dist/core/test-generator/renderers/vitest.js.map +1 -0
- package/dist/core/test-generator/scenario-parser.d.ts +33 -0
- package/dist/core/test-generator/scenario-parser.d.ts.map +1 -0
- package/dist/core/test-generator/scenario-parser.js +244 -0
- package/dist/core/test-generator/scenario-parser.js.map +1 -0
- package/dist/core/test-generator/test-generator.d.ts +30 -0
- package/dist/core/test-generator/test-generator.d.ts.map +1 -0
- package/dist/core/test-generator/test-generator.js +174 -0
- package/dist/core/test-generator/test-generator.js.map +1 -0
- package/dist/core/test-generator/test-writer.d.ts +25 -0
- package/dist/core/test-generator/test-writer.d.ts.map +1 -0
- package/dist/core/test-generator/test-writer.js +128 -0
- package/dist/core/test-generator/test-writer.js.map +1 -0
- package/dist/core/test-generator/then-matchers.d.ts +35 -0
- package/dist/core/test-generator/then-matchers.d.ts.map +1 -0
- package/dist/core/test-generator/then-matchers.js +211 -0
- package/dist/core/test-generator/then-matchers.js.map +1 -0
- package/dist/core/verifier/index.d.ts +5 -0
- package/dist/core/verifier/index.d.ts.map +1 -0
- package/dist/core/verifier/index.js +5 -0
- package/dist/core/verifier/index.js.map +1 -0
- package/dist/core/verifier/verification-engine.d.ts +293 -0
- package/dist/core/verifier/verification-engine.d.ts.map +1 -0
- package/dist/core/verifier/verification-engine.js +919 -0
- package/dist/core/verifier/verification-engine.js.map +1 -0
- package/dist/types/index.d.ts +368 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/pipeline.d.ts +167 -0
- package/dist/types/pipeline.d.ts.map +1 -0
- package/dist/types/pipeline.js +5 -0
- package/dist/types/pipeline.js.map +1 -0
- package/dist/types/test-generator.d.ts +103 -0
- package/dist/types/test-generator.d.ts.map +1 -0
- package/dist/types/test-generator.js +17 -0
- package/dist/types/test-generator.js.map +1 -0
- package/dist/utils/command-helpers.d.ts +68 -0
- package/dist/utils/command-helpers.d.ts.map +1 -0
- package/dist/utils/command-helpers.js +150 -0
- package/dist/utils/command-helpers.js.map +1 -0
- package/dist/utils/errors.d.ts +51 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +129 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +149 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +342 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/misc.d.ts +10 -0
- package/dist/utils/misc.d.ts.map +1 -0
- package/dist/utils/misc.js +21 -0
- package/dist/utils/misc.js.map +1 -0
- package/dist/utils/progress.d.ts +142 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +283 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/prompts.d.ts +53 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +199 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/shutdown.d.ts +89 -0
- package/dist/utils/shutdown.d.ts.map +1 -0
- package/dist/utils/shutdown.js +238 -0
- package/dist/utils/shutdown.js.map +1 -0
- package/examples/bmad/README.md +113 -0
- package/examples/bmad/agents/architect.md +226 -0
- package/examples/bmad/agents/dev-brownfield.md +69 -0
- package/examples/bmad/setup/architect.customize.yaml +14 -0
- package/examples/bmad/tasks/implement-story.md +254 -0
- package/examples/bmad/tasks/onboarding.md +169 -0
- package/examples/bmad/tasks/refactor.md +178 -0
- package/examples/bmad/tasks/sprint-planning.md +168 -0
- package/examples/bmad/templates/story.md +108 -0
- package/examples/cline-workflows/openlore-analyze-codebase.md +101 -0
- package/examples/cline-workflows/openlore-check-spec-drift.md +102 -0
- package/examples/cline-workflows/openlore-execute-refactor.md +212 -0
- package/examples/cline-workflows/openlore-implement-feature.md +266 -0
- package/examples/cline-workflows/openlore-plan-refactor.md +279 -0
- package/examples/cline-workflows/openlore-refactor-codebase.md +16 -0
- package/examples/cline-workflows/openlore-write-tests.md +177 -0
- package/examples/drift-demo/openspec/config.yaml +14 -0
- package/examples/drift-demo/openspec/specs/architecture/spec.md +30 -0
- package/examples/drift-demo/openspec/specs/auth/spec.md +71 -0
- package/examples/drift-demo/openspec/specs/database/spec.md +33 -0
- package/examples/drift-demo/openspec/specs/overview/spec.md +20 -0
- package/examples/drift-demo/openspec/specs/projects/spec.md +55 -0
- package/examples/drift-demo/openspec/specs/tasks/spec.md +78 -0
- package/examples/drift-demo/package.json +21 -0
- package/examples/drift-demo/src/auth/auth-middleware.ts +30 -0
- package/examples/drift-demo/src/auth/auth-routes.ts +29 -0
- package/examples/drift-demo/src/auth/auth-service.ts +45 -0
- package/examples/drift-demo/src/database/connection.ts +27 -0
- package/examples/drift-demo/src/index.ts +16 -0
- package/examples/drift-demo/src/projects/project-model.ts +15 -0
- package/examples/drift-demo/src/projects/project-service.ts +34 -0
- package/examples/drift-demo/src/tasks/task-model.ts +37 -0
- package/examples/drift-demo/src/tasks/task-routes.ts +53 -0
- package/examples/drift-demo/src/tasks/task-service.ts +60 -0
- package/examples/drift-demo/src/utils/validation.ts +11 -0
- package/examples/drift-demo/tests/auth.test.ts +4 -0
- package/examples/drift-demo/tests/tasks.test.ts +4 -0
- package/examples/drift-demo/tsconfig.json +10 -0
- package/examples/drift-test/run-drift-test.sh +1087 -0
- package/examples/gsd/README.md +119 -0
- package/examples/gsd/commands/gsd/openlore-drift.md +111 -0
- package/examples/gsd/commands/gsd/openlore-orient.md +191 -0
- package/examples/mistral-vibe/README.md +101 -0
- package/examples/mistral-vibe/antipatterns-template.md +18 -0
- package/examples/mistral-vibe/skills/openlore-analyze-codebase/SKILL.md +124 -0
- package/examples/mistral-vibe/skills/openlore-brainstorm/SKILL.md +379 -0
- package/examples/mistral-vibe/skills/openlore-debug/SKILL.md +330 -0
- package/examples/mistral-vibe/skills/openlore-execute-refactor/SKILL.md +291 -0
- package/examples/mistral-vibe/skills/openlore-generate/SKILL.md +245 -0
- package/examples/mistral-vibe/skills/openlore-implement-story/SKILL.md +326 -0
- package/examples/mistral-vibe/skills/openlore-plan-refactor/SKILL.md +365 -0
- package/examples/mistral-vibe/skills/openlore-review-changes/SKILL.md +128 -0
- package/examples/mistral-vibe/skills/openlore-write-tests/SKILL.md +261 -0
- package/examples/opencode/agent-guard.ts +170 -0
- package/examples/opencode/plugins/anti-laziness.ts +202 -0
- package/examples/opencode/plugins/lib/openlore-context-injector-helpers.ts +116 -0
- package/examples/opencode/plugins/lib/openlore-decision-extractor-helpers.ts +65 -0
- package/examples/opencode/plugins/openlore-context-injector.test.ts +211 -0
- package/examples/opencode/plugins/openlore-context-injector.ts +165 -0
- package/examples/opencode/plugins/openlore-decision-extractor.test.ts +131 -0
- package/examples/opencode/plugins/openlore-decision-extractor.ts +322 -0
- package/examples/opencode/plugins/openlore-enforcer.ts +227 -0
- package/examples/opencode/prompts/sisyphus-sdd.md +150 -0
- package/examples/opencode-skills/openlore-analyze-codebase/SKILL.md +101 -0
- package/examples/opencode-skills/openlore-brainstorm/SKILL.md +354 -0
- package/examples/opencode-skills/openlore-debug/SKILL.md +291 -0
- package/examples/opencode-skills/openlore-execute-refactor/SKILL.md +241 -0
- package/examples/opencode-skills/openlore-generate/SKILL.md +236 -0
- package/examples/opencode-skills/openlore-implement-story/SKILL.md +251 -0
- package/examples/opencode-skills/openlore-plan-refactor/SKILL.md +298 -0
- package/examples/opencode-skills/openlore-review-changes/SKILL.md +134 -0
- package/examples/opencode-skills/openlore-write-tests/SKILL.md +230 -0
- package/examples/openspec-analysis/README.md +59 -0
- package/examples/openspec-analysis/SUMMARY.md +72 -0
- package/examples/openspec-analysis/config.json +16 -0
- package/examples/openspec-analysis/dependencies.mermaid +35 -0
- package/examples/openspec-analysis/dependency-graph.json +12116 -0
- package/examples/openspec-analysis/llm-context.json +119 -0
- package/examples/openspec-analysis/repo-structure.json +871 -0
- package/examples/openspec-cli/README.md +67 -0
- package/examples/openspec-cli/openspec/config.yaml +26 -0
- package/examples/openspec-cli/openspec/specs/architecture/spec.md +178 -0
- package/examples/openspec-cli/openspec/specs/artifact-graph/spec.md +143 -0
- package/examples/openspec-cli/openspec/specs/cli/spec.md +138 -0
- package/examples/openspec-cli/openspec/specs/overview/spec.md +60 -0
- package/examples/openspec-cli/openspec/specs/parsing/spec.md +123 -0
- package/examples/openspec-cli/openspec/specs/validation/spec.md +108 -0
- package/examples/spec-kit/README.md +104 -0
- package/examples/spec-kit/commands/drift.md +87 -0
- package/examples/spec-kit/commands/orient.md +138 -0
- package/examples/spec-kit/extension.yml +54 -0
- package/package.json +125 -0
- package/src/viewer/InteractiveGraphViewer.jsx +1600 -0
- package/src/viewer/app/index.html +17 -0
- package/src/viewer/app/main.jsx +13 -0
- package/src/viewer/components/ArchitectureView.jsx +177 -0
- package/src/viewer/components/ChatPanel.jsx +450 -0
- package/src/viewer/components/ClassGraph.jsx +782 -0
- package/src/viewer/components/ClusterGraph.jsx +469 -0
- package/src/viewer/components/FilterBar.jsx +179 -0
- package/src/viewer/components/FlatGraph.jsx +282 -0
- package/src/viewer/components/MicroComponents.jsx +85 -0
- package/src/viewer/hooks/usePanZoom.js +79 -0
- package/src/viewer/utils/constants.js +64 -0
- package/src/viewer/utils/graph-helpers.js +303 -0
- package/src/viewer/utils/graph-helpers.test.ts +39 -0
- package/src/viewer/utils/themes.js +206 -0
- package/stubs/tree-sitter-cli-stub/package.json +6 -0
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool handlers for codebase analysis:
|
|
3
|
+
* analyze_codebase, get_architecture_overview, get_refactor_report,
|
|
4
|
+
* get_duplicate_report, get_signatures, get_mapping, check_spec_drift,
|
|
5
|
+
* get_function_skeleton, get_god_functions, get_route_inventory,
|
|
6
|
+
* get_middleware_inventory, get_schema_inventory, get_ui_components.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
9
|
+
import { join, relative } from 'node:path';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { DEFAULT_MAX_FILES, DEFAULT_DRIFT_MAX_FILES, TOP_REFACTOR_ISSUES_LIMIT, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, OPENLORE_ANALYSIS_REL_PATH, OPENSPEC_DIR, ARTIFACT_DEPENDENCY_GRAPH, ARTIFACT_MAPPING, ARTIFACT_REPO_STRUCTURE, ARTIFACT_ROUTE_INVENTORY, ARTIFACT_MIDDLEWARE_INVENTORY, ARTIFACT_SCHEMA_INVENTORY, ARTIFACT_UI_INVENTORY, ARTIFACT_ENV_INVENTORY, ARTIFACT_EXTERNAL_PACKAGES, TRANSITIVE_SCORE_MAX, } from '../../../constants.js';
|
|
14
|
+
import { runAnalysis } from '../../../cli/commands/analyze.js';
|
|
15
|
+
import { analyzeForRefactoring } from '../../analyzer/refactor-analyzer.js';
|
|
16
|
+
import { formatSignatureMaps } from '../../analyzer/signature-extractor.js';
|
|
17
|
+
import { getSkeletonContent, detectLanguage, isSkeletonWorthIncluding } from '../../analyzer/code-shaper.js';
|
|
18
|
+
import { buildArchitectureOverview } from '../../analyzer/architecture-writer.js';
|
|
19
|
+
import { isGitRepository, getChangedFiles, buildSpecMap, buildADRMap, detectDrift, } from '../../drift/index.js';
|
|
20
|
+
import { readOpenLoreConfig } from '../config-manager.js';
|
|
21
|
+
import { validateDirectory, readCachedContext, isCacheFresh, safeJoin } from './utils.js';
|
|
22
|
+
import { openloreAudit } from '../../../api/audit.js';
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// HANDLERS
|
|
25
|
+
// ============================================================================
|
|
26
|
+
/**
|
|
27
|
+
* Run a full static analysis pass on `directory` and return a compact summary.
|
|
28
|
+
*/
|
|
29
|
+
export async function handleAnalyzeCodebase(directory, force) {
|
|
30
|
+
const absDir = await validateDirectory(directory);
|
|
31
|
+
const outputPath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR);
|
|
32
|
+
if (!force && await isCacheFresh(absDir)) {
|
|
33
|
+
const ctx = await readCachedContext(absDir);
|
|
34
|
+
if (ctx) {
|
|
35
|
+
const cg = ctx.callGraph;
|
|
36
|
+
const topRefactorIssues = cg
|
|
37
|
+
? analyzeForRefactoring(cg).priorities.slice(0, TOP_REFACTOR_ISSUES_LIMIT).map(e => ({
|
|
38
|
+
function: e.function, file: e.file, issues: e.issues, priorityScore: e.priorityScore,
|
|
39
|
+
}))
|
|
40
|
+
: [];
|
|
41
|
+
return {
|
|
42
|
+
cached: true,
|
|
43
|
+
callGraph: cg
|
|
44
|
+
? { totalNodes: cg.stats.totalNodes, totalEdges: cg.stats.totalEdges,
|
|
45
|
+
hubs: cg.hubFunctions.length, entryPoints: cg.entryPoints.length,
|
|
46
|
+
layerViolations: cg.layerViolations.length }
|
|
47
|
+
: null,
|
|
48
|
+
topRefactorIssues,
|
|
49
|
+
analysisPath: OPENLORE_ANALYSIS_REL_PATH,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const result = await runAnalysis(absDir, outputPath, {
|
|
54
|
+
maxFiles: DEFAULT_MAX_FILES,
|
|
55
|
+
include: [],
|
|
56
|
+
exclude: [],
|
|
57
|
+
});
|
|
58
|
+
const { artifacts, repoMap, depGraph } = result;
|
|
59
|
+
const rs = artifacts.repoStructure;
|
|
60
|
+
const cg = artifacts.llmContext.callGraph;
|
|
61
|
+
let topRefactorIssues = [];
|
|
62
|
+
if (cg) {
|
|
63
|
+
const report = analyzeForRefactoring(cg);
|
|
64
|
+
topRefactorIssues = report.priorities.slice(0, TOP_REFACTOR_ISSUES_LIMIT).map(e => ({
|
|
65
|
+
function: e.function,
|
|
66
|
+
file: e.file,
|
|
67
|
+
issues: e.issues,
|
|
68
|
+
priorityScore: e.priorityScore,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
projectName: rs.projectName,
|
|
73
|
+
projectType: rs.projectType,
|
|
74
|
+
frameworks: rs.frameworks,
|
|
75
|
+
architecture: rs.architecture.pattern,
|
|
76
|
+
stats: {
|
|
77
|
+
files: repoMap.summary.totalFiles,
|
|
78
|
+
analyzedFiles: repoMap.summary.analyzedFiles,
|
|
79
|
+
depNodes: depGraph.statistics.nodeCount,
|
|
80
|
+
depEdges: depGraph.statistics.edgeCount,
|
|
81
|
+
importEdges: depGraph.statistics.importEdgeCount,
|
|
82
|
+
httpCrossEdges: depGraph.statistics.httpEdgeCount,
|
|
83
|
+
cycles: depGraph.statistics.cycleCount,
|
|
84
|
+
},
|
|
85
|
+
callGraph: cg
|
|
86
|
+
? {
|
|
87
|
+
totalNodes: cg.stats.totalNodes,
|
|
88
|
+
totalEdges: cg.stats.totalEdges,
|
|
89
|
+
hubs: cg.hubFunctions.length,
|
|
90
|
+
entryPoints: cg.entryPoints.length,
|
|
91
|
+
layerViolations: cg.layerViolations.length,
|
|
92
|
+
}
|
|
93
|
+
: null,
|
|
94
|
+
domains: rs.domains.map((d) => d.name),
|
|
95
|
+
topRefactorIssues,
|
|
96
|
+
analysisPath: OPENLORE_ANALYSIS_REL_PATH,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* High-level architecture map: clusters, cross-cluster deps, entry points, hubs.
|
|
101
|
+
*/
|
|
102
|
+
export async function handleGetArchitectureOverview(directory) {
|
|
103
|
+
const absDir = await validateDirectory(directory);
|
|
104
|
+
let depGraph = null;
|
|
105
|
+
try {
|
|
106
|
+
const raw = await readFile(join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_DEPENDENCY_GRAPH), 'utf-8');
|
|
107
|
+
depGraph = JSON.parse(raw);
|
|
108
|
+
}
|
|
109
|
+
catch { /* ignore */ }
|
|
110
|
+
const ctx = await readCachedContext(absDir);
|
|
111
|
+
if (!depGraph && !ctx) {
|
|
112
|
+
return { error: 'No analysis found. Run analyze_codebase first.' };
|
|
113
|
+
}
|
|
114
|
+
const overview = buildArchitectureOverview(depGraph, ctx, absDir);
|
|
115
|
+
return {
|
|
116
|
+
summary: overview.summary,
|
|
117
|
+
clusters: overview.clusters,
|
|
118
|
+
globalEntryPoints: overview.globalEntryPoints,
|
|
119
|
+
criticalHubs: overview.criticalHubs,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Return a prioritized refactor report from cached analysis.
|
|
124
|
+
*/
|
|
125
|
+
export async function handleGetRefactorReport(directory) {
|
|
126
|
+
const absDir = await validateDirectory(directory);
|
|
127
|
+
const ctx = await readCachedContext(absDir);
|
|
128
|
+
if (!ctx)
|
|
129
|
+
return { error: 'No analysis found. Run analyze_codebase first.' };
|
|
130
|
+
if (!ctx.callGraph)
|
|
131
|
+
return { error: 'Call graph not available in cached analysis. Re-run analyze_codebase.' };
|
|
132
|
+
return analyzeForRefactoring(ctx.callGraph);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Read the cached duplicate detection result.
|
|
136
|
+
*/
|
|
137
|
+
export async function handleGetDuplicateReport(directory) {
|
|
138
|
+
const absDir = await validateDirectory(directory);
|
|
139
|
+
const cachePath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, 'duplicates.json');
|
|
140
|
+
let raw;
|
|
141
|
+
try {
|
|
142
|
+
raw = await readFile(cachePath, 'utf-8');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return {
|
|
146
|
+
error: 'No duplicate report found. Run analyze_codebase first ' +
|
|
147
|
+
'(duplicates.json is generated during analysis).',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(raw);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return { error: 'Duplicate report cache is corrupted. Re-run analyze_codebase.' };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Return compact function and class signatures for files in the project.
|
|
159
|
+
*/
|
|
160
|
+
export async function handleGetSignatures(directory, filePattern) {
|
|
161
|
+
const absDir = await validateDirectory(directory);
|
|
162
|
+
const ctx = await readCachedContext(absDir);
|
|
163
|
+
if (!ctx)
|
|
164
|
+
return 'No analysis found. Run analyze_codebase first.';
|
|
165
|
+
if (!ctx.signatures || ctx.signatures.length === 0) {
|
|
166
|
+
return 'No signatures available in cached analysis. Re-run analyze_codebase.';
|
|
167
|
+
}
|
|
168
|
+
const filtered = filePattern
|
|
169
|
+
? ctx.signatures.filter((s) => s.path.includes(filePattern))
|
|
170
|
+
: ctx.signatures;
|
|
171
|
+
if (filtered.length === 0) {
|
|
172
|
+
return `No files matching pattern "${filePattern}" found in analysis.`;
|
|
173
|
+
}
|
|
174
|
+
const chunks = formatSignatureMaps(filtered);
|
|
175
|
+
return chunks.join('\n\n---\n\n');
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Return the requirement→function mapping from mapping.json.
|
|
179
|
+
*/
|
|
180
|
+
export async function handleGetMapping(directory, domain, orphansOnly) {
|
|
181
|
+
const absDir = await validateDirectory(directory);
|
|
182
|
+
let raw;
|
|
183
|
+
try {
|
|
184
|
+
raw = await readFile(join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_MAPPING), 'utf-8');
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return { error: 'No mapping found. Run openlore generate first.' };
|
|
188
|
+
}
|
|
189
|
+
let mapping;
|
|
190
|
+
try {
|
|
191
|
+
mapping = JSON.parse(raw);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return { error: 'Mapping file is corrupted. Re-run openlore generate.' };
|
|
195
|
+
}
|
|
196
|
+
if (orphansOnly) {
|
|
197
|
+
const filtered = domain
|
|
198
|
+
? mapping.orphanFunctions.filter((f) => f.file.includes(domain))
|
|
199
|
+
: mapping.orphanFunctions;
|
|
200
|
+
return { generatedAt: mapping.generatedAt, stats: mapping.stats, orphanFunctions: filtered };
|
|
201
|
+
}
|
|
202
|
+
const filteredMappings = domain
|
|
203
|
+
? mapping.mappings.filter((m) => m.domain === domain)
|
|
204
|
+
: mapping.mappings;
|
|
205
|
+
return {
|
|
206
|
+
generatedAt: mapping.generatedAt,
|
|
207
|
+
stats: mapping.stats,
|
|
208
|
+
mappings: filteredMappings,
|
|
209
|
+
orphanFunctions: domain ? [] : mapping.orphanFunctions,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Run spec-drift detection in static mode (no LLM).
|
|
214
|
+
*/
|
|
215
|
+
export async function handleCheckSpecDrift(directory, base = 'auto', files = [], domains = [], failOn = 'warning', maxFiles = DEFAULT_DRIFT_MAX_FILES) {
|
|
216
|
+
const absDir = await validateDirectory(directory);
|
|
217
|
+
if (!(await isGitRepository(absDir))) {
|
|
218
|
+
return { error: 'Not a git repository. Drift detection requires git.' };
|
|
219
|
+
}
|
|
220
|
+
const openloreConfig = await readOpenLoreConfig(absDir);
|
|
221
|
+
if (!openloreConfig) {
|
|
222
|
+
return { error: 'No openlore configuration found. Run "openlore init" first.' };
|
|
223
|
+
}
|
|
224
|
+
const openspecPath = join(absDir, openloreConfig.openspecPath ?? OPENSPEC_DIR);
|
|
225
|
+
const specsPath = join(openspecPath, 'specs');
|
|
226
|
+
try {
|
|
227
|
+
await stat(specsPath);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return { error: 'No specs found. Run "openlore generate" first.' };
|
|
231
|
+
}
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
const gitResult = await getChangedFiles({
|
|
234
|
+
rootPath: absDir,
|
|
235
|
+
baseRef: base,
|
|
236
|
+
pathFilter: files.length > 0 ? files : undefined,
|
|
237
|
+
includeUnstaged: true,
|
|
238
|
+
});
|
|
239
|
+
if (gitResult.files.length === 0) {
|
|
240
|
+
return {
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
baseRef: gitResult.resolvedBase,
|
|
243
|
+
totalChangedFiles: 0,
|
|
244
|
+
specRelevantFiles: 0,
|
|
245
|
+
issues: [],
|
|
246
|
+
summary: { gaps: 0, stale: 0, uncovered: 0, orphanedSpecs: 0, adrGaps: 0, adrOrphaned: 0, total: 0 },
|
|
247
|
+
hasDrift: false,
|
|
248
|
+
duration: Date.now() - startTime,
|
|
249
|
+
mode: 'static',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const actualChangedFiles = gitResult.files.length;
|
|
253
|
+
if (gitResult.files.length > maxFiles) {
|
|
254
|
+
gitResult.files = gitResult.files.slice(0, maxFiles);
|
|
255
|
+
}
|
|
256
|
+
const repoStructurePath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_REPO_STRUCTURE);
|
|
257
|
+
let hasRepoStructure = false;
|
|
258
|
+
try {
|
|
259
|
+
await stat(repoStructurePath);
|
|
260
|
+
hasRepoStructure = true;
|
|
261
|
+
}
|
|
262
|
+
catch { /* no prior analysis */ }
|
|
263
|
+
const specMap = await buildSpecMap({
|
|
264
|
+
rootPath: absDir,
|
|
265
|
+
openspecPath,
|
|
266
|
+
repoStructurePath: hasRepoStructure ? repoStructurePath : undefined,
|
|
267
|
+
});
|
|
268
|
+
const adrMap = await buildADRMap({
|
|
269
|
+
rootPath: absDir,
|
|
270
|
+
openspecPath,
|
|
271
|
+
repoStructurePath: hasRepoStructure ? repoStructurePath : undefined,
|
|
272
|
+
});
|
|
273
|
+
const result = await detectDrift({
|
|
274
|
+
rootPath: absDir,
|
|
275
|
+
specMap,
|
|
276
|
+
changedFiles: gitResult.files,
|
|
277
|
+
failOn,
|
|
278
|
+
domainFilter: domains.length > 0 ? domains : undefined,
|
|
279
|
+
openspecRelPath: openloreConfig.openspecPath ?? OPENSPEC_DIR,
|
|
280
|
+
baseRef: gitResult.resolvedBase,
|
|
281
|
+
adrMap: adrMap ?? undefined,
|
|
282
|
+
});
|
|
283
|
+
result.baseRef = gitResult.resolvedBase;
|
|
284
|
+
result.totalChangedFiles = actualChangedFiles;
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Return a noise-stripped skeleton of a source file.
|
|
289
|
+
*/
|
|
290
|
+
export async function handleGetFunctionSkeleton(directory, filePath) {
|
|
291
|
+
const absDir = await validateDirectory(directory);
|
|
292
|
+
const absFile = safeJoin(absDir, filePath);
|
|
293
|
+
let source;
|
|
294
|
+
try {
|
|
295
|
+
source = await readFile(absFile, 'utf-8');
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return { error: `File not found: ${filePath}` };
|
|
299
|
+
}
|
|
300
|
+
const language = detectLanguage(filePath);
|
|
301
|
+
const skeleton = getSkeletonContent(source, language);
|
|
302
|
+
const worthIncluding = isSkeletonWorthIncluding(source, skeleton);
|
|
303
|
+
return {
|
|
304
|
+
filePath,
|
|
305
|
+
language,
|
|
306
|
+
originalLines: source.split('\n').length,
|
|
307
|
+
skeletonLines: skeleton.split('\n').length,
|
|
308
|
+
reductionPct: Math.round((1 - skeleton.length / source.length) * 100),
|
|
309
|
+
worthIncluding,
|
|
310
|
+
skeleton,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// Note: handleGetGodFunctions lives in graph.ts (alongside other call-graph tools)
|
|
314
|
+
/**
|
|
315
|
+
* Extract the exact source text of a named function using the cached call graph.
|
|
316
|
+
*
|
|
317
|
+
* Uses the startIndex/endIndex byte offsets recorded in llm-context.json to
|
|
318
|
+
* slice the source file — no extra tree-sitter parsing needed at query time.
|
|
319
|
+
* Falls back to a line-number scan when the call graph is unavailable.
|
|
320
|
+
*/
|
|
321
|
+
export async function handleGetFunctionBody(directory, filePath, functionName) {
|
|
322
|
+
const absDir = await validateDirectory(directory);
|
|
323
|
+
const absFile = safeJoin(absDir, filePath);
|
|
324
|
+
let source;
|
|
325
|
+
try {
|
|
326
|
+
source = await readFile(absFile, 'utf-8');
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return { error: `File not found: ${filePath}` };
|
|
330
|
+
}
|
|
331
|
+
// Try call graph first: exact byte-range slice, no ambiguity
|
|
332
|
+
const contextPath = join(absDir, '.openlore', 'analysis', 'llm-context.json');
|
|
333
|
+
try {
|
|
334
|
+
const raw = await readFile(contextPath, 'utf-8');
|
|
335
|
+
const ctx = JSON.parse(raw);
|
|
336
|
+
if (ctx.callGraph?.nodes) {
|
|
337
|
+
const node = ctx.callGraph.nodes.find(n => n.name === functionName && (n.filePath === filePath || n.filePath.endsWith('/' + filePath.replace(/^\//, ''))));
|
|
338
|
+
if (node && node.startIndex < node.endIndex) {
|
|
339
|
+
const body = source.slice(node.startIndex, node.endIndex);
|
|
340
|
+
return {
|
|
341
|
+
functionName,
|
|
342
|
+
filePath,
|
|
343
|
+
language: node.language,
|
|
344
|
+
className: node.className ?? null,
|
|
345
|
+
startIndex: node.startIndex,
|
|
346
|
+
endIndex: node.endIndex,
|
|
347
|
+
body,
|
|
348
|
+
lineCount: body.split('\n').length,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch { /* no call graph — fall through to line-scan */ }
|
|
354
|
+
// Fallback: find the function by scanning for its declaration line
|
|
355
|
+
const lines = source.split('\n');
|
|
356
|
+
const declPattern = new RegExp(`\\b${functionName}\\s*[(<]`);
|
|
357
|
+
const startLine = lines.findIndex(l => declPattern.test(l));
|
|
358
|
+
if (startLine === -1) {
|
|
359
|
+
return { error: `Function "${functionName}" not found in ${filePath}. Run analyze_codebase first for exact byte-range extraction.` };
|
|
360
|
+
}
|
|
361
|
+
// Collect lines until matching brace depth returns to 0 (works for C-style languages)
|
|
362
|
+
let depth = 0;
|
|
363
|
+
let endLine = startLine;
|
|
364
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
365
|
+
for (const ch of lines[i]) {
|
|
366
|
+
if (ch === '{')
|
|
367
|
+
depth++;
|
|
368
|
+
else if (ch === '}')
|
|
369
|
+
depth--;
|
|
370
|
+
}
|
|
371
|
+
if (depth > 0 || i === startLine) {
|
|
372
|
+
endLine = i;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
endLine = i;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
const body = lines.slice(startLine, endLine + 1).join('\n');
|
|
379
|
+
return {
|
|
380
|
+
functionName,
|
|
381
|
+
filePath,
|
|
382
|
+
language: detectLanguage(filePath),
|
|
383
|
+
className: null,
|
|
384
|
+
startLine: startLine + 1,
|
|
385
|
+
endLine: endLine + 1,
|
|
386
|
+
body,
|
|
387
|
+
lineCount: endLine - startLine + 1,
|
|
388
|
+
note: 'Extracted via line scan (no call graph available). Run analyze_codebase for exact extraction.',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* List and optionally filter Architecture Decision Records from openspec/decisions/.
|
|
393
|
+
*/
|
|
394
|
+
export async function handleGetDecisions(directory, query) {
|
|
395
|
+
const { existsSync } = await import('node:fs');
|
|
396
|
+
const { readdir } = await import('node:fs/promises');
|
|
397
|
+
const { join: pjoin } = await import('node:path');
|
|
398
|
+
const absDir = await validateDirectory(directory);
|
|
399
|
+
// Resolve openspec path from config if present
|
|
400
|
+
let openspecRelPath = 'openspec';
|
|
401
|
+
try {
|
|
402
|
+
const cfgRaw = await readFile(join(absDir, '.openlore', 'config.json'), 'utf-8');
|
|
403
|
+
const cfg = JSON.parse(cfgRaw);
|
|
404
|
+
if (cfg.openspecPath)
|
|
405
|
+
openspecRelPath = cfg.openspecPath;
|
|
406
|
+
}
|
|
407
|
+
catch { /* use default */ }
|
|
408
|
+
const decisionsDir = pjoin(absDir, openspecRelPath, 'decisions');
|
|
409
|
+
if (!existsSync(decisionsDir)) {
|
|
410
|
+
return { decisions: [], note: `No decisions directory found at ${openspecRelPath}/decisions/. Run "openlore generate --adrs" first.` };
|
|
411
|
+
}
|
|
412
|
+
let entries;
|
|
413
|
+
try {
|
|
414
|
+
entries = await readdir(decisionsDir);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return { decisions: [] };
|
|
418
|
+
}
|
|
419
|
+
// Each ADR is a .md file directly in decisions/
|
|
420
|
+
const adrFiles = entries.filter(e => e.endsWith('.md'));
|
|
421
|
+
// Read all ADR files in parallel
|
|
422
|
+
const adrs = await Promise.all(adrFiles.map(async (filename) => {
|
|
423
|
+
const filePath = pjoin(decisionsDir, filename);
|
|
424
|
+
const content = await readFile(filePath, 'utf-8');
|
|
425
|
+
// Extract title from first H1 or H2
|
|
426
|
+
const titleMatch = content.match(/^#{1,2}\s+(.+)$/m);
|
|
427
|
+
const title = titleMatch?.[1]?.trim() ?? filename.replace(/\.md$/, '');
|
|
428
|
+
// Extract status from "Status:" line (ADR convention)
|
|
429
|
+
const statusMatch = content.match(/^\*\*?Status\*\*?:?\s*(.+)$/im);
|
|
430
|
+
const status = statusMatch?.[1]?.trim() ?? 'unknown';
|
|
431
|
+
return { filename, title, status, content };
|
|
432
|
+
}));
|
|
433
|
+
// Filter by query text if provided
|
|
434
|
+
const lowerQuery = query?.toLowerCase();
|
|
435
|
+
const filtered = lowerQuery
|
|
436
|
+
? adrs.filter(a => a.title.toLowerCase().includes(lowerQuery) ||
|
|
437
|
+
a.content.toLowerCase().includes(lowerQuery))
|
|
438
|
+
: adrs;
|
|
439
|
+
return {
|
|
440
|
+
count: filtered.length,
|
|
441
|
+
query: query ?? null,
|
|
442
|
+
decisions: filtered.map(a => ({
|
|
443
|
+
filename: a.filename,
|
|
444
|
+
title: a.title,
|
|
445
|
+
status: a.status,
|
|
446
|
+
content: a.content,
|
|
447
|
+
})),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// ROUTE INVENTORY HANDLER
|
|
452
|
+
// ============================================================================
|
|
453
|
+
/**
|
|
454
|
+
* Return the pre-computed route inventory from the last analysis run.
|
|
455
|
+
* Falls back to re-computing from source files if the artifact is missing.
|
|
456
|
+
*/
|
|
457
|
+
export async function handleGetRouteInventory(directory) {
|
|
458
|
+
const absDir = await validateDirectory(directory);
|
|
459
|
+
const artifactPath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_ROUTE_INVENTORY);
|
|
460
|
+
// Try reading cached artifact first
|
|
461
|
+
try {
|
|
462
|
+
const raw = await readFile(artifactPath, 'utf-8');
|
|
463
|
+
const inventory = JSON.parse(raw);
|
|
464
|
+
return { cached: true, ...inventory };
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Artifact not present — run live extraction
|
|
468
|
+
}
|
|
469
|
+
const { buildRouteInventory } = await import('../../analyzer/http-route-parser.js');
|
|
470
|
+
const { RepositoryMapper } = await import('../../analyzer/repository-mapper.js');
|
|
471
|
+
const { readOpenLoreConfig } = await import('../config-manager.js');
|
|
472
|
+
const openloreConfig = await readOpenLoreConfig(absDir);
|
|
473
|
+
const configExclude = openloreConfig?.analysis.excludePatterns ?? [];
|
|
474
|
+
const mapper = new RepositoryMapper(absDir, {
|
|
475
|
+
maxFiles: DEFAULT_MAX_FILES,
|
|
476
|
+
excludePatterns: configExclude.length > 0 ? configExclude : undefined,
|
|
477
|
+
});
|
|
478
|
+
const repoMap = await mapper.map();
|
|
479
|
+
const filePaths = repoMap.allFiles.map(f => f.path);
|
|
480
|
+
const inventory = await buildRouteInventory(filePaths, absDir);
|
|
481
|
+
return { cached: false, ...inventory };
|
|
482
|
+
}
|
|
483
|
+
// ============================================================================
|
|
484
|
+
// MIDDLEWARE INVENTORY HANDLER
|
|
485
|
+
// ============================================================================
|
|
486
|
+
/**
|
|
487
|
+
* Return the pre-computed middleware inventory from the last analysis run.
|
|
488
|
+
* Falls back to re-computing from source files if the artifact is missing.
|
|
489
|
+
*/
|
|
490
|
+
export async function handleGetMiddlewareInventory(directory) {
|
|
491
|
+
const absDir = await validateDirectory(directory);
|
|
492
|
+
const artifactPath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_MIDDLEWARE_INVENTORY);
|
|
493
|
+
// Try reading cached artifact first
|
|
494
|
+
try {
|
|
495
|
+
const raw = await readFile(artifactPath, 'utf-8');
|
|
496
|
+
const inventory = JSON.parse(raw);
|
|
497
|
+
return { cached: true, total: inventory.length, entries: inventory };
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// Artifact not present — run live extraction
|
|
501
|
+
}
|
|
502
|
+
const { extractMiddleware } = await import('../../analyzer/middleware-extractor.js');
|
|
503
|
+
const { RepositoryMapper } = await import('../../analyzer/repository-mapper.js');
|
|
504
|
+
const { readOpenLoreConfig } = await import('../config-manager.js');
|
|
505
|
+
const openloreConfig = await readOpenLoreConfig(absDir);
|
|
506
|
+
const configExclude = openloreConfig?.analysis.excludePatterns ?? [];
|
|
507
|
+
const mapper = new RepositoryMapper(absDir, {
|
|
508
|
+
maxFiles: DEFAULT_MAX_FILES,
|
|
509
|
+
excludePatterns: configExclude.length > 0 ? configExclude : undefined,
|
|
510
|
+
});
|
|
511
|
+
const repoMap = await mapper.map();
|
|
512
|
+
const filePaths = repoMap.allFiles.map(f => f.path);
|
|
513
|
+
const entries = await extractMiddleware(filePaths, absDir);
|
|
514
|
+
return { cached: false, total: entries.length, entries };
|
|
515
|
+
}
|
|
516
|
+
// ============================================================================
|
|
517
|
+
// SCHEMA INVENTORY HANDLER
|
|
518
|
+
// ============================================================================
|
|
519
|
+
/**
|
|
520
|
+
* Return the pre-computed database schema inventory from the last analysis run.
|
|
521
|
+
* Falls back to re-computing from source files if the artifact is missing.
|
|
522
|
+
*/
|
|
523
|
+
export async function handleGetSchemaInventory(directory) {
|
|
524
|
+
const absDir = await validateDirectory(directory);
|
|
525
|
+
const artifactPath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_SCHEMA_INVENTORY);
|
|
526
|
+
try {
|
|
527
|
+
const raw = await readFile(artifactPath, 'utf-8');
|
|
528
|
+
const schemas = JSON.parse(raw);
|
|
529
|
+
return { cached: true, total: schemas.length, schemas };
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// Artifact not present — run live extraction
|
|
533
|
+
}
|
|
534
|
+
const { extractSchemas } = await import('../../analyzer/schema-extractor.js');
|
|
535
|
+
const { RepositoryMapper } = await import('../../analyzer/repository-mapper.js');
|
|
536
|
+
const { readOpenLoreConfig } = await import('../config-manager.js');
|
|
537
|
+
const openloreConfig = await readOpenLoreConfig(absDir);
|
|
538
|
+
const configExclude = openloreConfig?.analysis.excludePatterns ?? [];
|
|
539
|
+
const mapper = new RepositoryMapper(absDir, {
|
|
540
|
+
maxFiles: DEFAULT_MAX_FILES,
|
|
541
|
+
excludePatterns: configExclude.length > 0 ? configExclude : undefined,
|
|
542
|
+
});
|
|
543
|
+
const repoMap = await mapper.map();
|
|
544
|
+
const filePaths = repoMap.allFiles.map(f => f.path);
|
|
545
|
+
const schemas = await extractSchemas(filePaths, absDir);
|
|
546
|
+
return { cached: false, total: schemas.length, schemas };
|
|
547
|
+
}
|
|
548
|
+
// ============================================================================
|
|
549
|
+
// UI COMPONENTS HANDLER
|
|
550
|
+
// ============================================================================
|
|
551
|
+
/**
|
|
552
|
+
* Return the pre-computed UI component inventory from the last analysis run.
|
|
553
|
+
* Falls back to re-computing from source files if the artifact is missing.
|
|
554
|
+
*/
|
|
555
|
+
export async function handleGetUIComponents(directory) {
|
|
556
|
+
const absDir = await validateDirectory(directory);
|
|
557
|
+
const artifactPath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_UI_INVENTORY);
|
|
558
|
+
try {
|
|
559
|
+
const raw = await readFile(artifactPath, 'utf-8');
|
|
560
|
+
const components = JSON.parse(raw);
|
|
561
|
+
return { cached: true, total: components.length, components };
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Artifact not present — run live extraction
|
|
565
|
+
}
|
|
566
|
+
const { extractUIComponents } = await import('../../analyzer/ui-component-extractor.js');
|
|
567
|
+
const { RepositoryMapper } = await import('../../analyzer/repository-mapper.js');
|
|
568
|
+
const { readOpenLoreConfig } = await import('../config-manager.js');
|
|
569
|
+
const openloreConfig = await readOpenLoreConfig(absDir);
|
|
570
|
+
const configExclude = openloreConfig?.analysis.excludePatterns ?? [];
|
|
571
|
+
const mapper = new RepositoryMapper(absDir, {
|
|
572
|
+
maxFiles: DEFAULT_MAX_FILES,
|
|
573
|
+
excludePatterns: configExclude.length > 0 ? configExclude : undefined,
|
|
574
|
+
});
|
|
575
|
+
const repoMap = await mapper.map();
|
|
576
|
+
const filePaths = repoMap.allFiles.map(f => f.path);
|
|
577
|
+
const components = await extractUIComponents(filePaths, absDir);
|
|
578
|
+
return { cached: false, total: components.length, components };
|
|
579
|
+
}
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// ENV VARS HANDLER
|
|
582
|
+
// ============================================================================
|
|
583
|
+
/**
|
|
584
|
+
* Return the pre-computed environment variable inventory from the last analysis run.
|
|
585
|
+
* Falls back to re-computing from source files if the artifact is missing.
|
|
586
|
+
*/
|
|
587
|
+
export async function handleGetEnvVars(directory) {
|
|
588
|
+
const absDir = await validateDirectory(directory);
|
|
589
|
+
const artifactPath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_ENV_INVENTORY);
|
|
590
|
+
try {
|
|
591
|
+
const raw = await readFile(artifactPath, 'utf-8');
|
|
592
|
+
const envVars = JSON.parse(raw);
|
|
593
|
+
return { cached: true, total: envVars.length, envVars };
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
// Artifact not present — run live extraction
|
|
597
|
+
}
|
|
598
|
+
const { extractEnvVars } = await import('../../analyzer/env-extractor.js');
|
|
599
|
+
const { RepositoryMapper } = await import('../../analyzer/repository-mapper.js');
|
|
600
|
+
const { readOpenLoreConfig } = await import('../config-manager.js');
|
|
601
|
+
const openloreConfig = await readOpenLoreConfig(absDir);
|
|
602
|
+
const configExclude = openloreConfig?.analysis.excludePatterns ?? [];
|
|
603
|
+
const mapper = new RepositoryMapper(absDir, {
|
|
604
|
+
maxFiles: DEFAULT_MAX_FILES,
|
|
605
|
+
excludePatterns: configExclude.length > 0 ? configExclude : undefined,
|
|
606
|
+
});
|
|
607
|
+
const repoMap = await mapper.map();
|
|
608
|
+
const filePaths = repoMap.allFiles.map(f => f.path);
|
|
609
|
+
const envVars = await extractEnvVars(filePaths, absDir);
|
|
610
|
+
return { cached: false, total: envVars.length, envVars };
|
|
611
|
+
}
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// EXTERNAL PACKAGES HANDLER
|
|
614
|
+
// ============================================================================
|
|
615
|
+
/**
|
|
616
|
+
* Return direct external dependencies from package manifests
|
|
617
|
+
* (package.json, pyproject.toml, requirements.txt, Cargo.toml, go.mod).
|
|
618
|
+
* Falls back to live extraction if cached artifact is absent.
|
|
619
|
+
*/
|
|
620
|
+
export async function handleGetExternalPackages(directory) {
|
|
621
|
+
const absDir = await validateDirectory(directory);
|
|
622
|
+
const artifactPath = join(absDir, OPENLORE_DIR, OPENLORE_ANALYSIS_SUBDIR, ARTIFACT_EXTERNAL_PACKAGES);
|
|
623
|
+
try {
|
|
624
|
+
const raw = await readFile(artifactPath, 'utf-8');
|
|
625
|
+
const result = JSON.parse(raw);
|
|
626
|
+
return { cached: true, ...result };
|
|
627
|
+
}
|
|
628
|
+
catch { /* not cached */ }
|
|
629
|
+
const { extractExternalPackages } = await import('../../analyzer/external-packages.js');
|
|
630
|
+
const result = await extractExternalPackages(absDir);
|
|
631
|
+
return { cached: false, ...result };
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Parity audit: report spec coverage gaps without any LLM call.
|
|
635
|
+
* Returns uncovered functions, hub gaps, orphan requirements, and stale domains.
|
|
636
|
+
*/
|
|
637
|
+
export async function handleAuditSpecCoverage(directory, maxUncovered = 50, hubThreshold = 5) {
|
|
638
|
+
const absDir = await validateDirectory(directory);
|
|
639
|
+
try {
|
|
640
|
+
const report = await openloreAudit({
|
|
641
|
+
rootPath: absDir,
|
|
642
|
+
maxUncovered,
|
|
643
|
+
hubThreshold,
|
|
644
|
+
save: true,
|
|
645
|
+
});
|
|
646
|
+
return report;
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
return { error: `Audit failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// ============================================================================
|
|
653
|
+
// TEST GENERATION HANDLERS
|
|
654
|
+
// ============================================================================
|
|
655
|
+
/**
|
|
656
|
+
* Generate spec-driven test files from OpenSpec scenarios.
|
|
657
|
+
*/
|
|
658
|
+
export async function handleGenerateTests(args) {
|
|
659
|
+
const absDir = await validateDirectory(args.directory);
|
|
660
|
+
const { parseScenarios, generateTests, writeTestFiles, detectFramework } = await import('../../../core/test-generator/index.js');
|
|
661
|
+
const { FRAMEWORK_EXTENSIONS } = await import('../../../types/test-generator.js');
|
|
662
|
+
const scenarios = await parseScenarios({
|
|
663
|
+
rootPath: absDir,
|
|
664
|
+
domains: args.domains,
|
|
665
|
+
});
|
|
666
|
+
if (scenarios.length === 0) {
|
|
667
|
+
return { files: [], message: 'No scenarios found. Run "openlore generate" first.' };
|
|
668
|
+
}
|
|
669
|
+
// Resolve framework
|
|
670
|
+
let framework;
|
|
671
|
+
const valid = Object.keys(FRAMEWORK_EXTENSIONS);
|
|
672
|
+
if (!args.framework || args.framework === 'auto') {
|
|
673
|
+
framework = await detectFramework(absDir);
|
|
674
|
+
}
|
|
675
|
+
else if (valid.includes(args.framework)) {
|
|
676
|
+
framework = args.framework;
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
return { error: `Unknown framework "${args.framework}". Valid: ${valid.join(', ')}` };
|
|
680
|
+
}
|
|
681
|
+
const files = await generateTests({
|
|
682
|
+
scenarios,
|
|
683
|
+
framework,
|
|
684
|
+
outputDir: 'spec-tests',
|
|
685
|
+
rootPath: absDir,
|
|
686
|
+
useLlm: args.useLlm ?? false,
|
|
687
|
+
});
|
|
688
|
+
const dryRun = args.dryRun ?? true; // MCP defaults to dry-run for safety
|
|
689
|
+
const writeResult = await writeTestFiles({
|
|
690
|
+
files,
|
|
691
|
+
rootPath: absDir,
|
|
692
|
+
dryRun,
|
|
693
|
+
merge: false,
|
|
694
|
+
});
|
|
695
|
+
return {
|
|
696
|
+
framework,
|
|
697
|
+
dryRun,
|
|
698
|
+
files: files.map((f) => ({
|
|
699
|
+
path: f.outputPath,
|
|
700
|
+
domain: f.domain,
|
|
701
|
+
scenarioCount: f.scenarios.length,
|
|
702
|
+
content: f.content,
|
|
703
|
+
})),
|
|
704
|
+
summary: writeResult,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Report spec test coverage for a project.
|
|
709
|
+
*/
|
|
710
|
+
export async function handleGetTestCoverage(args) {
|
|
711
|
+
const absDir = await validateDirectory(args.directory);
|
|
712
|
+
const { analyzeTestCoverage } = await import('../../../core/test-generator/index.js');
|
|
713
|
+
const report = await analyzeTestCoverage({
|
|
714
|
+
rootPath: absDir,
|
|
715
|
+
testDirs: ['spec-tests', 'src'],
|
|
716
|
+
domains: args.domains,
|
|
717
|
+
minCoverage: args.minCoverage,
|
|
718
|
+
// discover without LLM: tag-based only
|
|
719
|
+
discover: false,
|
|
720
|
+
});
|
|
721
|
+
return report;
|
|
722
|
+
}
|
|
723
|
+
// ============================================================================
|
|
724
|
+
// get_minimal_context
|
|
725
|
+
// ============================================================================
|
|
726
|
+
/**
|
|
727
|
+
* Return the bare minimum an agent needs to safely modify a function:
|
|
728
|
+
* its signature + body, direct callers (signatures), direct callees (signatures),
|
|
729
|
+
* and which test files cover it. Typically 200-600 tokens vs orient's 2000+.
|
|
730
|
+
*/
|
|
731
|
+
export async function handleGetMinimalContext(directory, functionName, filePath) {
|
|
732
|
+
const absDir = await validateDirectory(directory);
|
|
733
|
+
const ctx = await readCachedContext(absDir);
|
|
734
|
+
if (!ctx?.callGraph)
|
|
735
|
+
return { error: 'No call graph. Run analyze_codebase first.' };
|
|
736
|
+
const cg = ctx.callGraph;
|
|
737
|
+
const nodeMap = new Map(cg.nodes.map(n => [n.id, n]));
|
|
738
|
+
// Find target node(s)
|
|
739
|
+
const candidates = cg.nodes.filter(n => n.name === functionName &&
|
|
740
|
+
!n.isExternal && !n.isTest &&
|
|
741
|
+
(!filePath || n.filePath.endsWith(filePath)));
|
|
742
|
+
if (candidates.length === 0)
|
|
743
|
+
return { error: `Function "${functionName}" not found. Run analyze_codebase first.` };
|
|
744
|
+
const target = candidates[0];
|
|
745
|
+
// Risk-aware depth: high fan-in/out functions get more caller/callee context
|
|
746
|
+
const riskLevel = target.fanIn >= 30 || target.fanOut >= 15 ? 'high' :
|
|
747
|
+
target.fanIn >= 15 || target.fanOut >= 8 ? 'medium' : 'low';
|
|
748
|
+
const callerLimit = riskLevel === 'high' ? 24 : riskLevel === 'medium' ? 18 : 12;
|
|
749
|
+
const calleeLimit = riskLevel === 'high' ? 24 : riskLevel === 'medium' ? 18 : 12;
|
|
750
|
+
// Direct callers and callees from calls edges
|
|
751
|
+
const callsEdges = cg.edges.filter(e => !e.kind || e.kind === 'calls');
|
|
752
|
+
const sig = (n) => n.signature ?? n.name + (n.isExternal ? ' [external]' : '');
|
|
753
|
+
// Build per-callee edge list to get callType
|
|
754
|
+
const edgesForCallee = new Map();
|
|
755
|
+
const edgesForCaller = new Map();
|
|
756
|
+
for (const e of callsEdges) {
|
|
757
|
+
if (!edgesForCallee.has(e.calleeId))
|
|
758
|
+
edgesForCallee.set(e.calleeId, []);
|
|
759
|
+
edgesForCallee.get(e.calleeId).push({ callerId: e.callerId, callType: e.callType });
|
|
760
|
+
if (!edgesForCaller.has(e.callerId))
|
|
761
|
+
edgesForCaller.set(e.callerId, []);
|
|
762
|
+
edgesForCaller.get(e.callerId).push({ calleeId: e.calleeId, callType: e.callType });
|
|
763
|
+
}
|
|
764
|
+
const callerEdges = edgesForCallee.get(target.id) ?? [];
|
|
765
|
+
const calleeEdgeList = edgesForCaller.get(target.id) ?? [];
|
|
766
|
+
const callerIds = callerEdges.map(e => e.callerId);
|
|
767
|
+
const calleeIds = calleeEdgeList.map(e => e.calleeId);
|
|
768
|
+
const callers = [...new Set(callerIds)]
|
|
769
|
+
.map(id => {
|
|
770
|
+
const n = nodeMap.get(id);
|
|
771
|
+
if (!n || n.isExternal)
|
|
772
|
+
return null;
|
|
773
|
+
const edge = callerEdges.find(e => e.callerId === id);
|
|
774
|
+
return { name: n.name, file: relative(absDir, n.filePath), sig: sig(n), callType: edge?.callType ?? 'direct', isExternal: false };
|
|
775
|
+
})
|
|
776
|
+
.filter((n) => !!n)
|
|
777
|
+
.slice(0, callerLimit);
|
|
778
|
+
const callees = [...new Set(calleeIds)]
|
|
779
|
+
.map(id => {
|
|
780
|
+
const n = nodeMap.get(id);
|
|
781
|
+
if (!n)
|
|
782
|
+
return null;
|
|
783
|
+
const edge = calleeEdgeList.find(e => e.calleeId === id);
|
|
784
|
+
return {
|
|
785
|
+
name: n.isExternal ? `[external] ${n.name}` : n.name,
|
|
786
|
+
file: n.isExternal ? 'external' : relative(absDir, n.filePath),
|
|
787
|
+
sig: sig(n),
|
|
788
|
+
callType: edge?.callType ?? 'direct',
|
|
789
|
+
isExternal: n.isExternal ?? false,
|
|
790
|
+
kind: n.externalKind,
|
|
791
|
+
};
|
|
792
|
+
})
|
|
793
|
+
.filter((n) => !!n)
|
|
794
|
+
.slice(0, calleeLimit);
|
|
795
|
+
// Test coverage — distinguish import-based vs call-based tracing
|
|
796
|
+
const seenTestNames = new Set();
|
|
797
|
+
const testedBy = cg.edges
|
|
798
|
+
.filter(e => e.kind === 'tested_by' && e.callerId === target.id)
|
|
799
|
+
.flatMap(e => {
|
|
800
|
+
if (seenTestNames.has(e.calleeName))
|
|
801
|
+
return [];
|
|
802
|
+
seenTestNames.add(e.calleeName);
|
|
803
|
+
const confidence = e.confidence === 'import' ? 'imported' : 'called';
|
|
804
|
+
return [{ name: e.calleeName, confidence }];
|
|
805
|
+
});
|
|
806
|
+
// Function body (byte-range slice from source)
|
|
807
|
+
let body = null;
|
|
808
|
+
try {
|
|
809
|
+
const src = await readFile(target.filePath, 'utf-8');
|
|
810
|
+
body = src.slice(target.startIndex, target.endIndex);
|
|
811
|
+
}
|
|
812
|
+
catch { /* source unavailable */ }
|
|
813
|
+
return {
|
|
814
|
+
function: {
|
|
815
|
+
name: target.name,
|
|
816
|
+
file: relative(absDir, target.filePath),
|
|
817
|
+
signature: target.signature ?? target.name,
|
|
818
|
+
language: target.language,
|
|
819
|
+
className: target.className ?? null,
|
|
820
|
+
startLine: target.startLine ?? null,
|
|
821
|
+
fanIn: target.fanIn,
|
|
822
|
+
fanOut: target.fanOut,
|
|
823
|
+
community: target.communityLabel ?? null,
|
|
824
|
+
riskLevel,
|
|
825
|
+
body,
|
|
826
|
+
},
|
|
827
|
+
callers,
|
|
828
|
+
callees,
|
|
829
|
+
testedBy,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
// ============================================================================
|
|
833
|
+
// get_cluster
|
|
834
|
+
// ============================================================================
|
|
835
|
+
/**
|
|
836
|
+
* Return all functions in the same community as the given function.
|
|
837
|
+
* Communities are computed via label propagation on the call graph at analyze time.
|
|
838
|
+
* Useful for understanding the "cluster" of related functions without traversing the graph.
|
|
839
|
+
*/
|
|
840
|
+
export async function handleGetCluster(directory, functionName) {
|
|
841
|
+
const absDir = await validateDirectory(directory);
|
|
842
|
+
const ctx = await readCachedContext(absDir);
|
|
843
|
+
if (!ctx?.callGraph)
|
|
844
|
+
return { error: 'No call graph. Run analyze_codebase first.' };
|
|
845
|
+
const cg = ctx.callGraph;
|
|
846
|
+
// Find the target node
|
|
847
|
+
const target = cg.nodes.find(n => n.name === functionName && !n.isExternal && !n.isTest);
|
|
848
|
+
if (!target)
|
|
849
|
+
return { error: `Function "${functionName}" not found.` };
|
|
850
|
+
if (!target.communityId)
|
|
851
|
+
return { error: `No community data. Re-run analyze_codebase.` };
|
|
852
|
+
// All nodes in same community
|
|
853
|
+
const members = cg.nodes
|
|
854
|
+
.filter(n => n.communityId === target.communityId && !n.isExternal && !n.isTest)
|
|
855
|
+
.sort((a, b) => b.fanIn - a.fanIn);
|
|
856
|
+
// Internal edges within community
|
|
857
|
+
const memberIds = new Set(members.map(n => n.id));
|
|
858
|
+
const nameById = new Map(members.map(n => [n.id, n.name]));
|
|
859
|
+
const rawInternal = cg.edges
|
|
860
|
+
.filter(e => (!e.kind || e.kind === 'calls') && memberIds.has(e.callerId) && memberIds.has(e.calleeId));
|
|
861
|
+
// Deduplicate, count all unique internal edges for density, show top 15
|
|
862
|
+
const seen = new Set();
|
|
863
|
+
const callEdges = [];
|
|
864
|
+
let uniqueInternalCount = 0;
|
|
865
|
+
for (const e of rawInternal) {
|
|
866
|
+
const key = `${e.callerId}→${e.calleeId}`;
|
|
867
|
+
if (seen.has(key))
|
|
868
|
+
continue;
|
|
869
|
+
seen.add(key);
|
|
870
|
+
uniqueInternalCount++;
|
|
871
|
+
if (callEdges.length < 15) {
|
|
872
|
+
callEdges.push(`${nameById.get(e.callerId)} → ${nameById.get(e.calleeId)}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const m = members.length;
|
|
876
|
+
const clusterDensity = m > 1 ? Math.round((uniqueInternalCount / (m * (m - 1))) * 1000) / 1000 : 0;
|
|
877
|
+
// Files the community spans
|
|
878
|
+
const files = [...new Set(members.map(n => relative(absDir, n.filePath)))].sort();
|
|
879
|
+
return {
|
|
880
|
+
communityLabel: target.communityLabel,
|
|
881
|
+
communityId: target.communityId,
|
|
882
|
+
stats: {
|
|
883
|
+
members: m,
|
|
884
|
+
files: files.length,
|
|
885
|
+
internalEdges: uniqueInternalCount,
|
|
886
|
+
clusterDensity,
|
|
887
|
+
},
|
|
888
|
+
files,
|
|
889
|
+
// Internal call edges show WHY these functions cluster together
|
|
890
|
+
internalCallGraph: callEdges,
|
|
891
|
+
functions: members.map(n => ({
|
|
892
|
+
name: n.name,
|
|
893
|
+
file: relative(absDir, n.filePath),
|
|
894
|
+
signature: n.signature ?? n.name,
|
|
895
|
+
fanIn: n.fanIn,
|
|
896
|
+
fanOut: n.fanOut,
|
|
897
|
+
})),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
// ============================================================================
|
|
901
|
+
// detect_changes
|
|
902
|
+
// ============================================================================
|
|
903
|
+
/** Run git with explicit stdio pipes — safe inside MCP server (which owns stdin/stdout). */
|
|
904
|
+
function runGit(args, cwd) {
|
|
905
|
+
// Use shell file-redirect to temp files instead of pipes — libuv pipe() calls
|
|
906
|
+
// fail with EBADF inside the MCP server because its FD 0/1 are the JSON-RPC
|
|
907
|
+
// protocol sockets; avoiding pipe creation altogether sidesteps the issue.
|
|
908
|
+
return new Promise((resolve, reject) => {
|
|
909
|
+
const PATH = (process.env.PATH ?? '') + ':/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin';
|
|
910
|
+
const tmp = mkdtempSync(join(tmpdir(), 'sg-git-'));
|
|
911
|
+
const outPath = join(tmp, 'o');
|
|
912
|
+
const errPath = join(tmp, 'e');
|
|
913
|
+
const escaped = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
|
|
914
|
+
const cmd = `/usr/bin/git ${escaped} >'${outPath}' 2>'${errPath}'`;
|
|
915
|
+
const r = spawnSync('/bin/sh', ['-c', cmd], {
|
|
916
|
+
cwd,
|
|
917
|
+
stdio: 'ignore',
|
|
918
|
+
env: { ...process.env, PATH },
|
|
919
|
+
});
|
|
920
|
+
let stdout = '';
|
|
921
|
+
let stderr = '';
|
|
922
|
+
try {
|
|
923
|
+
stdout = readFileSync(outPath, 'utf8');
|
|
924
|
+
}
|
|
925
|
+
catch { /* no output */ }
|
|
926
|
+
try {
|
|
927
|
+
stderr = readFileSync(errPath, 'utf8');
|
|
928
|
+
}
|
|
929
|
+
catch { /* no output */ }
|
|
930
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
931
|
+
if (r.error) {
|
|
932
|
+
reject(r.error);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if ((r.status ?? 1) !== 0) {
|
|
936
|
+
reject(new Error(stderr || `git exit ${r.status}`));
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
resolve(stdout);
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
// ── change-type classifier ────────────────────────────────────────────────────
|
|
943
|
+
// Signature line patterns across languages (TS/JS, Python, Go, Rust, Java/C++)
|
|
944
|
+
const SIG_PATTERN = /^\s*(export\s+)?(default\s+)?(async\s+)?function\b|\bdef\s+\w+\s*[([:]|\bfunc\s+(\([^)]*\)\s*)?\w+\s*\(|\bfn\s+\w+\b|\bclass\s+\w+/;
|
|
945
|
+
// Control-flow keywords (broad, multi-language)
|
|
946
|
+
const LOGIC_PATTERN = /\b(if|else|for|while|switch|try|catch|throw|return|yield|await|break|continue|elif|except|raise|match|case)\b/;
|
|
947
|
+
function classifyChangeType(node, addedLines) {
|
|
948
|
+
const fnStart = node.startLine ?? 1;
|
|
949
|
+
const fnEnd = node.endLine ?? fnStart;
|
|
950
|
+
const linesInFn = [];
|
|
951
|
+
for (const [ln, content] of addedLines) {
|
|
952
|
+
if (ln >= fnStart && ln <= fnEnd)
|
|
953
|
+
linesInFn.push(content);
|
|
954
|
+
}
|
|
955
|
+
if (linesInFn.length === 0)
|
|
956
|
+
return 'logic';
|
|
957
|
+
// Signature change: function's own declaration line was modified
|
|
958
|
+
const sigLine = addedLines.get(fnStart);
|
|
959
|
+
if (sigLine !== undefined && SIG_PATTERN.test(sigLine))
|
|
960
|
+
return 'signature';
|
|
961
|
+
// Logic change: any added line has a control-flow keyword
|
|
962
|
+
if (linesInFn.some(l => LOGIC_PATTERN.test(l)))
|
|
963
|
+
return 'logic';
|
|
964
|
+
return 'config';
|
|
965
|
+
}
|
|
966
|
+
const CHANGE_TYPE_MULTIPLIER = {
|
|
967
|
+
signature: 1.5, // breaking-change candidate
|
|
968
|
+
logic: 1.0, // default
|
|
969
|
+
config: 0.4, // literal / comment / config tweak
|
|
970
|
+
};
|
|
971
|
+
function buildReason(params) {
|
|
972
|
+
const { changeType, directCallers, tests, bScore, fanIn } = params;
|
|
973
|
+
const parts = [];
|
|
974
|
+
if (changeType === 'signature')
|
|
975
|
+
parts.push('signature change');
|
|
976
|
+
else if (changeType === 'config')
|
|
977
|
+
parts.push('config/literal change');
|
|
978
|
+
if (fanIn > 0) {
|
|
979
|
+
const awaitedCount = directCallers.filter(c => c.callType === 'awaited').length;
|
|
980
|
+
const total = directCallers.length || fanIn;
|
|
981
|
+
if (awaitedCount === total && awaitedCount > 0)
|
|
982
|
+
parts.push('all callers awaited');
|
|
983
|
+
else if (awaitedCount > 0)
|
|
984
|
+
parts.push(`${awaitedCount} awaited callers`);
|
|
985
|
+
else
|
|
986
|
+
parts.push(`${fanIn} callers`);
|
|
987
|
+
}
|
|
988
|
+
const calledTests = tests.filter(t => t.confidence === 'called').length;
|
|
989
|
+
if (tests.length === 0)
|
|
990
|
+
parts.push('no tests');
|
|
991
|
+
else if (calledTests === 0)
|
|
992
|
+
parts.push('import-only tests');
|
|
993
|
+
else
|
|
994
|
+
parts.push(`${calledTests} direct test${calledTests > 1 ? 's' : ''}`);
|
|
995
|
+
if (bScore >= 0.67)
|
|
996
|
+
parts.push('HTTP/DB boundary');
|
|
997
|
+
else if (bScore > 0)
|
|
998
|
+
parts.push('external boundary');
|
|
999
|
+
return parts.join(' · ') || 'low-risk change';
|
|
1000
|
+
}
|
|
1001
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1002
|
+
/**
|
|
1003
|
+
* Detect recently changed functions and rank them by blast radius (fanIn of callers via BFS).
|
|
1004
|
+
* Runs git diff to find changed files/lines, maps to function nodes, scores by impact.
|
|
1005
|
+
*/
|
|
1006
|
+
export async function handleDetectChanges(directory, base) {
|
|
1007
|
+
const absDir = await validateDirectory(directory);
|
|
1008
|
+
const ctx = await readCachedContext(absDir);
|
|
1009
|
+
if (!ctx?.callGraph)
|
|
1010
|
+
return { error: 'No call graph. Run analyze_codebase first.' };
|
|
1011
|
+
const ref = base ?? 'HEAD';
|
|
1012
|
+
let diffOutput;
|
|
1013
|
+
try {
|
|
1014
|
+
diffOutput = await runGit(['diff', '--unified=0', ref, '--', '.'], absDir);
|
|
1015
|
+
if (!diffOutput.trim()) {
|
|
1016
|
+
diffOutput = await runGit(['diff', '--unified=0', '--cached', '--', '.'], absDir);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
catch (err) {
|
|
1020
|
+
return { error: `git diff failed: ${err.message}` };
|
|
1021
|
+
}
|
|
1022
|
+
if (!diffOutput.trim())
|
|
1023
|
+
return { changedFunctions: [], message: 'No changes detected.' };
|
|
1024
|
+
// Parse unified diff: collect line ranges AND added-line content per file.
|
|
1025
|
+
// --unified=0 means no context lines; only '+' and '-' lines appear in hunks.
|
|
1026
|
+
const changedFileData = new Map();
|
|
1027
|
+
let curFile = null;
|
|
1028
|
+
let newLineNum = 0;
|
|
1029
|
+
for (const line of diffOutput.split('\n')) {
|
|
1030
|
+
const fileMatch = line.match(/^\+\+\+ b\/(.+)$/);
|
|
1031
|
+
if (fileMatch) {
|
|
1032
|
+
curFile = join(absDir, fileMatch[1]);
|
|
1033
|
+
newLineNum = 0;
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
1037
|
+
if (hunkMatch && curFile) {
|
|
1038
|
+
newLineNum = parseInt(hunkMatch[1], 10);
|
|
1039
|
+
const count = hunkMatch[2] !== undefined ? parseInt(hunkMatch[2], 10) : 1;
|
|
1040
|
+
if (count === 0)
|
|
1041
|
+
continue;
|
|
1042
|
+
if (!changedFileData.has(curFile))
|
|
1043
|
+
changedFileData.set(curFile, { ranges: [], addedLines: new Map() });
|
|
1044
|
+
changedFileData.get(curFile).ranges.push([newLineNum, newLineNum + count - 1]);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (!curFile || !changedFileData.has(curFile))
|
|
1048
|
+
continue;
|
|
1049
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
1050
|
+
changedFileData.get(curFile).addedLines.set(newLineNum++, line.slice(1));
|
|
1051
|
+
}
|
|
1052
|
+
// '-' lines don't advance new-file position; no context lines with --unified=0
|
|
1053
|
+
}
|
|
1054
|
+
// Backward-compat: changedLines used by the node-overlap loop below
|
|
1055
|
+
const changedLines = new Map([...changedFileData.entries()].map(([k, v]) => [k, v.ranges]));
|
|
1056
|
+
const cg = ctx.callGraph;
|
|
1057
|
+
const nodeMap = new Map(cg.nodes.map(n => [n.id, n]));
|
|
1058
|
+
// Map changed line ranges to function nodes; track overlapping line count per function
|
|
1059
|
+
const changedFnIds = new Set();
|
|
1060
|
+
const fnChangedLineCount = new Map(); // nodeId → #lines overlapping with diff
|
|
1061
|
+
for (const [filePath, ranges] of changedLines) {
|
|
1062
|
+
const fileNodes = cg.nodes.filter(n => n.filePath === filePath && !n.isExternal && !n.isTest && n.startLine);
|
|
1063
|
+
for (const node of fileNodes) {
|
|
1064
|
+
const fnEnd = node.endLine ?? node.startLine;
|
|
1065
|
+
let overlap = 0;
|
|
1066
|
+
for (const [start, end] of ranges) {
|
|
1067
|
+
if (node.startLine <= end && fnEnd >= start) {
|
|
1068
|
+
changedFnIds.add(node.id);
|
|
1069
|
+
overlap += Math.min(end, fnEnd) - Math.max(start, node.startLine) + 1;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (overlap > 0)
|
|
1073
|
+
fnChangedLineCount.set(node.id, (fnChangedLineCount.get(node.id) ?? 0) + overlap);
|
|
1074
|
+
}
|
|
1075
|
+
// Fallback: no line match — include all functions in the file
|
|
1076
|
+
if (fileNodes.length > 0 && !fileNodes.some(n => changedFnIds.has(n.id))) {
|
|
1077
|
+
for (const n of fileNodes)
|
|
1078
|
+
changedFnIds.add(n.id);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (changedFnIds.size === 0) {
|
|
1082
|
+
return { changedFunctions: [], message: 'Changed files found but no matching function nodes. Re-run analyze_codebase.' };
|
|
1083
|
+
}
|
|
1084
|
+
const callsEdges = cg.edges.filter(e => !e.kind || e.kind === 'calls');
|
|
1085
|
+
// callerIndex: calleeId → [{id, callType}] — callType weights BFS contribution
|
|
1086
|
+
const callerIndex = new Map();
|
|
1087
|
+
for (const e of callsEdges) {
|
|
1088
|
+
if (!callerIndex.has(e.calleeId))
|
|
1089
|
+
callerIndex.set(e.calleeId, []);
|
|
1090
|
+
callerIndex.get(e.calleeId).push({ id: e.callerId, callType: e.callType });
|
|
1091
|
+
}
|
|
1092
|
+
// calleeIndex: callerId → calleeIds (for boundary score)
|
|
1093
|
+
const calleeIndex = new Map();
|
|
1094
|
+
for (const e of callsEdges) {
|
|
1095
|
+
if (!calleeIndex.has(e.callerId))
|
|
1096
|
+
calleeIndex.set(e.callerId, []);
|
|
1097
|
+
calleeIndex.get(e.callerId).push(e.calleeId);
|
|
1098
|
+
}
|
|
1099
|
+
// awaited callers most sensitive; callback least (detached, survives interface change)
|
|
1100
|
+
const callTypeWeight = (ct) => ct === 'awaited' ? 1.0 : ct === 'direct' ? 0.7 : ct === 'method' ? 0.6 :
|
|
1101
|
+
ct === 'callback' ? 0.4 : 0.5; // 0.5 default covers 'constructor' and unknown
|
|
1102
|
+
// Distance-weighted BFS: Σ weight/d² — clamped to prevent cross-repo drift
|
|
1103
|
+
const transitiveScore = (startId) => {
|
|
1104
|
+
const visited = new Set();
|
|
1105
|
+
const queue = [{ id: startId, depth: 1 }];
|
|
1106
|
+
let score = 0;
|
|
1107
|
+
while (queue.length) {
|
|
1108
|
+
const { id, depth } = queue.shift();
|
|
1109
|
+
for (const caller of callerIndex.get(id) ?? []) {
|
|
1110
|
+
if (!visited.has(caller.id)) {
|
|
1111
|
+
visited.add(caller.id);
|
|
1112
|
+
score += callTypeWeight(caller.callType) / (depth * depth);
|
|
1113
|
+
queue.push({ id: caller.id, depth: depth + 1 });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return Math.min(score, TRANSITIVE_SCORE_MAX);
|
|
1118
|
+
};
|
|
1119
|
+
// Boundary score: outgoing edges to external nodes; http/db weighted 3×, others 1×; normalized
|
|
1120
|
+
const boundaryScore = (nodeId) => {
|
|
1121
|
+
let raw = 0;
|
|
1122
|
+
for (const calleeId of calleeIndex.get(nodeId) ?? []) {
|
|
1123
|
+
const callee = nodeMap.get(calleeId);
|
|
1124
|
+
if (!callee?.isExternal)
|
|
1125
|
+
continue;
|
|
1126
|
+
raw += (callee.externalKind === 'http' || callee.externalKind === 'database') ? 3 : 1;
|
|
1127
|
+
}
|
|
1128
|
+
return Math.min(raw / 3, 1);
|
|
1129
|
+
};
|
|
1130
|
+
// testedBy map: nodeId → [{name, confidence}]
|
|
1131
|
+
const testedByMap = new Map();
|
|
1132
|
+
for (const e of cg.edges.filter(e => e.kind === 'tested_by')) {
|
|
1133
|
+
if (!testedByMap.has(e.callerId))
|
|
1134
|
+
testedByMap.set(e.callerId, []);
|
|
1135
|
+
const arr = testedByMap.get(e.callerId);
|
|
1136
|
+
if (!arr.some(x => x.name === e.calleeName)) {
|
|
1137
|
+
arr.push({ name: e.calleeName, confidence: e.confidence === 'import' ? 'imported' : 'called' });
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
const scored = [...changedFnIds].map(id => {
|
|
1141
|
+
const n = nodeMap.get(id);
|
|
1142
|
+
const fnLength = Math.max(1, (n.endLine ?? n.startLine ?? 1) - (n.startLine ?? 1) + 1);
|
|
1143
|
+
const changed = fnChangedLineCount.get(id) ?? Math.round(fnLength * 0.5);
|
|
1144
|
+
// Blend relative (sensitivity) + absolute (log scale) — prevents tiny fully-changed fns
|
|
1145
|
+
// from outranking large ones; log(201)≈5.3 so 200 changed lines ≈ absScore 1.0
|
|
1146
|
+
const relScore = changed / fnLength;
|
|
1147
|
+
const absScore = Math.log(1 + changed) / Math.log(201);
|
|
1148
|
+
const rawChangeScore = Math.min(0.6 * relScore + 0.4 * absScore, 1);
|
|
1149
|
+
// Semantic modifier: signature changes are higher risk than config/literal tweaks
|
|
1150
|
+
const changeType = classifyChangeType(n, changedFileData.get(n.filePath)?.addedLines ?? new Map());
|
|
1151
|
+
const changeScore = Math.min(rawChangeScore * CHANGE_TYPE_MULTIPLIER[changeType], 1);
|
|
1152
|
+
const tests = testedByMap.get(id) ?? [];
|
|
1153
|
+
// called-edges are direct proof; imported-only is weaker (survives vi.mock)
|
|
1154
|
+
const effectiveTests = tests.reduce((s, t) => s + (t.confidence === 'called' ? 1.0 : 0.3), 0);
|
|
1155
|
+
const coveragePenalty = 1 / (1 + Math.log(1 + effectiveTests));
|
|
1156
|
+
const tScore = transitiveScore(id);
|
|
1157
|
+
const bScore = boundaryScore(id);
|
|
1158
|
+
// Multiplicative model: risk = likelihood × impact
|
|
1159
|
+
// Decouples change probability from structural blast radius; prevents correlated
|
|
1160
|
+
// signals (fanIn ↔ transitive) from triple-stacking additively
|
|
1161
|
+
const likelihood = changeScore * (1 + coveragePenalty);
|
|
1162
|
+
const impact = Math.log(1 + n.fanIn) * 0.8 + tScore + bScore;
|
|
1163
|
+
const riskScore = Math.round(likelihood * impact * 100) / 100;
|
|
1164
|
+
const directCallers = callerIndex.get(id) ?? [];
|
|
1165
|
+
const reason = buildReason({ changeType, directCallers, tests, bScore, fanIn: n.fanIn });
|
|
1166
|
+
return {
|
|
1167
|
+
name: n.name,
|
|
1168
|
+
file: relative(absDir, n.filePath),
|
|
1169
|
+
startLine: n.startLine ?? null,
|
|
1170
|
+
endLine: n.endLine ?? null,
|
|
1171
|
+
fanIn: n.fanIn,
|
|
1172
|
+
blastRadius: Math.round(tScore * 100) / 100,
|
|
1173
|
+
changeType,
|
|
1174
|
+
riskScore,
|
|
1175
|
+
reason,
|
|
1176
|
+
testedBy: testedByMap.get(id) ?? [],
|
|
1177
|
+
};
|
|
1178
|
+
}).sort((a, b) => b.riskScore - a.riskScore);
|
|
1179
|
+
return {
|
|
1180
|
+
base: ref,
|
|
1181
|
+
totalChanged: scored.length,
|
|
1182
|
+
changedFunctions: scored,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
//# sourceMappingURL=analysis.js.map
|