spectra-cli 4.1.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/go-adapter.d.ts.map +1 -1
- package/dist/adapters/go-adapter.js +2 -0
- package/dist/adapters/go-adapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/java-adapter.d.ts.map +1 -1
- package/dist/adapters/java-adapter.js +2 -0
- package/dist/adapters/java-adapter.js.map +1 -1
- package/dist/adapters/language-adapter.d.ts +47 -9
- package/dist/adapters/language-adapter.d.ts.map +1 -1
- package/dist/adapters/python-adapter.d.ts +34 -6
- package/dist/adapters/python-adapter.d.ts.map +1 -1
- package/dist/adapters/python-adapter.js +157 -59
- package/dist/adapters/python-adapter.js.map +1 -1
- package/dist/adapters/ts-js-adapter.d.ts +13 -5
- package/dist/adapters/ts-js-adapter.d.ts.map +1 -1
- package/dist/adapters/ts-js-adapter.js +55 -14
- package/dist/adapters/ts-js-adapter.js.map +1 -1
- package/dist/batch/batch-orchestrator.d.ts +30 -4
- package/dist/batch/batch-orchestrator.d.ts.map +1 -1
- package/dist/batch/batch-orchestrator.js +284 -7
- package/dist/batch/batch-orchestrator.js.map +1 -1
- package/dist/batch/delta-regenerator.d.ts +2 -2
- package/dist/batch/delta-regenerator.d.ts.map +1 -1
- package/dist/batch/delta-regenerator.js.map +1 -1
- package/dist/batch/module-grouper.d.ts +2 -2
- package/dist/batch/module-grouper.d.ts.map +1 -1
- package/dist/batch/module-grouper.js +1 -1
- package/dist/batch/module-grouper.js.map +1 -1
- package/dist/cli/commands/graph.js +1 -1
- package/dist/cli/commands/graph.js.map +1 -1
- package/dist/cli/commands/index.d.ts +8 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +274 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/query.d.ts.map +1 -1
- package/dist/cli/commands/query.js +3 -1
- package/dist/cli/commands/query.js.map +1 -1
- package/dist/cli/index.js +9 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/utils/parse-args.d.ts +13 -1
- package/dist/cli/utils/parse-args.d.ts.map +1 -1
- package/dist/cli/utils/parse-args.js +79 -2
- package/dist/cli/utils/parse-args.js.map +1 -1
- package/dist/core/ast-analyzer.d.ts.map +1 -1
- package/dist/core/ast-analyzer.js +79 -5
- package/dist/core/ast-analyzer.js.map +1 -1
- package/dist/core/import-resolver.d.ts +90 -0
- package/dist/core/import-resolver.d.ts.map +1 -0
- package/dist/core/import-resolver.js +273 -0
- package/dist/core/import-resolver.js.map +1 -0
- package/dist/core/query-mappers/base-mapper.d.ts +13 -0
- package/dist/core/query-mappers/base-mapper.d.ts.map +1 -1
- package/dist/core/query-mappers/go-mapper.d.ts +68 -0
- package/dist/core/query-mappers/go-mapper.d.ts.map +1 -1
- package/dist/core/query-mappers/go-mapper.js +421 -0
- package/dist/core/query-mappers/go-mapper.js.map +1 -1
- package/dist/core/query-mappers/java-mapper.d.ts +196 -0
- package/dist/core/query-mappers/java-mapper.d.ts.map +1 -1
- package/dist/core/query-mappers/java-mapper.js +600 -0
- package/dist/core/query-mappers/java-mapper.js.map +1 -1
- package/dist/core/query-mappers/python-mapper.d.ts +55 -0
- package/dist/core/query-mappers/python-mapper.d.ts.map +1 -1
- package/dist/core/query-mappers/python-mapper.js +273 -0
- package/dist/core/query-mappers/python-mapper.js.map +1 -1
- package/dist/core/query-mappers/typescript-mapper.d.ts +102 -0
- package/dist/core/query-mappers/typescript-mapper.d.ts.map +1 -1
- package/dist/core/query-mappers/typescript-mapper.js +470 -0
- package/dist/core/query-mappers/typescript-mapper.js.map +1 -1
- package/dist/core/single-spec-orchestrator.d.ts.map +1 -1
- package/dist/core/single-spec-orchestrator.js +146 -9
- package/dist/core/single-spec-orchestrator.js.map +1 -1
- package/dist/core/tree-sitter-analyzer.d.ts +6 -0
- package/dist/core/tree-sitter-analyzer.d.ts.map +1 -1
- package/dist/core/tree-sitter-analyzer.js +33 -1
- package/dist/core/tree-sitter-analyzer.js.map +1 -1
- package/dist/core/tree-sitter-fallback.d.ts +7 -4
- package/dist/core/tree-sitter-fallback.d.ts.map +1 -1
- package/dist/core/tree-sitter-fallback.js +157 -12
- package/dist/core/tree-sitter-fallback.js.map +1 -1
- package/dist/extraction/extraction-types.d.ts +25 -25
- package/dist/generator/index-generator.d.ts +3 -3
- package/dist/generator/index-generator.d.ts.map +1 -1
- package/dist/generator/index-generator.js +1 -1
- package/dist/generator/index-generator.js.map +1 -1
- package/dist/graph/directory-graph.d.ts +14 -4
- package/dist/graph/directory-graph.d.ts.map +1 -1
- package/dist/graph/directory-graph.js +71 -128
- package/dist/graph/directory-graph.js.map +1 -1
- package/dist/graph/mermaid-renderer.d.ts +8 -5
- package/dist/graph/mermaid-renderer.d.ts.map +1 -1
- package/dist/graph/mermaid-renderer.js +3 -3
- package/dist/graph/mermaid-renderer.js.map +1 -1
- package/dist/graph/topological-sort.d.ts +13 -6
- package/dist/graph/topological-sort.d.ts.map +1 -1
- package/dist/graph/topological-sort.js +2 -2
- package/dist/graph/topological-sort.js.map +1 -1
- package/dist/knowledge-graph/call-resolver.d.ts +64 -0
- package/dist/knowledge-graph/call-resolver.d.ts.map +1 -0
- package/dist/knowledge-graph/call-resolver.js +323 -0
- package/dist/knowledge-graph/call-resolver.js.map +1 -0
- package/dist/knowledge-graph/import-resolver.d.ts +114 -0
- package/dist/knowledge-graph/import-resolver.d.ts.map +1 -0
- package/dist/knowledge-graph/import-resolver.js +537 -0
- package/dist/knowledge-graph/import-resolver.js.map +1 -0
- package/dist/knowledge-graph/incremental.d.ts +110 -0
- package/dist/knowledge-graph/incremental.d.ts.map +1 -0
- package/dist/knowledge-graph/incremental.js +410 -0
- package/dist/knowledge-graph/incremental.js.map +1 -0
- package/dist/knowledge-graph/index.d.ts +74 -0
- package/dist/knowledge-graph/index.d.ts.map +1 -0
- package/dist/knowledge-graph/index.js +156 -0
- package/dist/knowledge-graph/index.js.map +1 -0
- package/dist/knowledge-graph/module-derivation.d.ts +238 -0
- package/dist/knowledge-graph/module-derivation.d.ts.map +1 -0
- package/dist/knowledge-graph/module-derivation.js +418 -0
- package/dist/knowledge-graph/module-derivation.js.map +1 -0
- package/dist/knowledge-graph/persistence.d.ts +270 -0
- package/dist/knowledge-graph/persistence.d.ts.map +1 -0
- package/dist/knowledge-graph/persistence.js +226 -0
- package/dist/knowledge-graph/persistence.js.map +1 -0
- package/dist/knowledge-graph/query-helpers.d.ts +164 -0
- package/dist/knowledge-graph/query-helpers.d.ts.map +1 -0
- package/dist/knowledge-graph/query-helpers.js +394 -0
- package/dist/knowledge-graph/query-helpers.js.map +1 -0
- package/dist/knowledge-graph/unified-graph.d.ts +310 -0
- package/dist/knowledge-graph/unified-graph.d.ts.map +1 -0
- package/dist/knowledge-graph/unified-graph.js +182 -0
- package/dist/knowledge-graph/unified-graph.js.map +1 -0
- package/dist/mcp/agent-context-tools.d.ts +104 -0
- package/dist/mcp/agent-context-tools.d.ts.map +1 -0
- package/dist/mcp/agent-context-tools.js +854 -0
- package/dist/mcp/agent-context-tools.js.map +1 -0
- package/dist/mcp/graph-tools.d.ts +20 -0
- package/dist/mcp/graph-tools.d.ts.map +1 -1
- package/dist/mcp/graph-tools.js +71 -11
- package/dist/mcp/graph-tools.js.map +1 -1
- package/dist/mcp/lib/response-helpers.d.ts +67 -0
- package/dist/mcp/lib/response-helpers.d.ts.map +1 -0
- package/dist/mcp/lib/response-helpers.js +103 -0
- package/dist/mcp/lib/response-helpers.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +9 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/models/call-site.d.ts +68 -0
- package/dist/models/call-site.d.ts.map +1 -0
- package/dist/models/call-site.js +60 -0
- package/dist/models/call-site.js.map +1 -0
- package/dist/models/code-skeleton.d.ts +78 -6
- package/dist/models/code-skeleton.d.ts.map +1 -1
- package/dist/models/code-skeleton.js +29 -0
- package/dist/models/code-skeleton.js.map +1 -1
- package/dist/models/drift-item.d.ts +4 -4
- package/dist/models/module-spec.d.ts +155 -94
- package/dist/models/module-spec.d.ts.map +1 -1
- package/dist/panoramic/adr-decision-pipeline.d.ts +53 -0
- package/dist/panoramic/adr-decision-pipeline.d.ts.map +1 -0
- package/dist/panoramic/adr-decision-pipeline.js +685 -0
- package/dist/panoramic/adr-decision-pipeline.js.map +1 -0
- package/dist/panoramic/api-surface-generator.d.ts +56 -0
- package/dist/panoramic/api-surface-generator.d.ts.map +1 -0
- package/dist/panoramic/api-surface-generator.js +1671 -0
- package/dist/panoramic/api-surface-generator.js.map +1 -0
- package/dist/panoramic/architecture-ir-builder.d.ts +19 -0
- package/dist/panoramic/architecture-ir-builder.d.ts.map +1 -0
- package/dist/panoramic/architecture-ir-builder.js +561 -0
- package/dist/panoramic/architecture-ir-builder.js.map +1 -0
- package/dist/panoramic/architecture-ir-exporters.d.ts +7 -0
- package/dist/panoramic/architecture-ir-exporters.d.ts.map +1 -0
- package/dist/panoramic/architecture-ir-exporters.js +183 -0
- package/dist/panoramic/architecture-ir-exporters.js.map +1 -0
- package/dist/panoramic/architecture-ir-generator.d.ts +36 -0
- package/dist/panoramic/architecture-ir-generator.d.ts.map +1 -0
- package/dist/panoramic/architecture-ir-generator.js +51 -0
- package/dist/panoramic/architecture-ir-generator.js.map +1 -0
- package/dist/panoramic/architecture-ir-mermaid-adapter.d.ts +6 -0
- package/dist/panoramic/architecture-ir-mermaid-adapter.d.ts.map +1 -0
- package/dist/panoramic/architecture-ir-mermaid-adapter.js +98 -0
- package/dist/panoramic/architecture-ir-mermaid-adapter.js.map +1 -0
- package/dist/panoramic/architecture-ir-model.d.ts +87 -0
- package/dist/panoramic/architecture-ir-model.d.ts.map +1 -0
- package/dist/panoramic/architecture-ir-model.js +20 -0
- package/dist/panoramic/architecture-ir-model.js.map +1 -0
- package/dist/panoramic/architecture-narrative.d.ts +68 -0
- package/dist/panoramic/architecture-narrative.d.ts.map +1 -0
- package/dist/panoramic/architecture-narrative.js +401 -0
- package/dist/panoramic/architecture-narrative.js.map +1 -0
- package/dist/panoramic/architecture-overview-generator.d.ts +31 -0
- package/dist/panoramic/architecture-overview-generator.d.ts.map +1 -0
- package/dist/panoramic/architecture-overview-generator.js +454 -0
- package/dist/panoramic/architecture-overview-generator.js.map +1 -0
- package/dist/panoramic/architecture-overview-model.d.ts +77 -0
- package/dist/panoramic/architecture-overview-model.d.ts.map +1 -0
- package/dist/panoramic/architecture-overview-model.js +31 -0
- package/dist/panoramic/architecture-overview-model.js.map +1 -0
- package/dist/panoramic/batch-project-docs.d.ts.map +1 -1
- package/dist/panoramic/batch-project-docs.js +4 -2
- package/dist/panoramic/batch-project-docs.js.map +1 -1
- package/dist/panoramic/builders/component-view-builder.d.ts +25 -1
- package/dist/panoramic/builders/component-view-builder.d.ts.map +1 -1
- package/dist/panoramic/builders/component-view-builder.js +83 -3
- package/dist/panoramic/builders/component-view-builder.js.map +1 -1
- package/dist/panoramic/builders/doc-graph-builder.d.ts +2 -2
- package/dist/panoramic/builders/doc-graph-builder.d.ts.map +1 -1
- package/dist/panoramic/builders/doc-graph-builder.js.map +1 -1
- package/dist/panoramic/component-view-builder.d.ts +17 -0
- package/dist/panoramic/component-view-builder.d.ts.map +1 -0
- package/dist/panoramic/component-view-builder.js +652 -0
- package/dist/panoramic/component-view-builder.js.map +1 -0
- package/dist/panoramic/component-view-model.d.ts +127 -0
- package/dist/panoramic/component-view-model.d.ts.map +1 -0
- package/dist/panoramic/component-view-model.js +65 -0
- package/dist/panoramic/component-view-model.js.map +1 -0
- package/dist/panoramic/config-reference-generator.d.ts +78 -0
- package/dist/panoramic/config-reference-generator.d.ts.map +1 -0
- package/dist/panoramic/config-reference-generator.js +199 -0
- package/dist/panoramic/config-reference-generator.js.map +1 -0
- package/dist/panoramic/coverage-auditor.d.ts +95 -0
- package/dist/panoramic/coverage-auditor.d.ts.map +1 -0
- package/dist/panoramic/coverage-auditor.js +290 -0
- package/dist/panoramic/coverage-auditor.js.map +1 -0
- package/dist/panoramic/cross-package-analyzer.d.ts +112 -0
- package/dist/panoramic/cross-package-analyzer.d.ts.map +1 -0
- package/dist/panoramic/cross-package-analyzer.js +258 -0
- package/dist/panoramic/cross-package-analyzer.js.map +1 -0
- package/dist/panoramic/data-model-generator.d.ts +528 -0
- package/dist/panoramic/data-model-generator.d.ts.map +1 -0
- package/dist/panoramic/data-model-generator.js +647 -0
- package/dist/panoramic/data-model-generator.js.map +1 -0
- package/dist/panoramic/doc-graph-builder.d.ts +80 -0
- package/dist/panoramic/doc-graph-builder.d.ts.map +1 -0
- package/dist/panoramic/doc-graph-builder.js +352 -0
- package/dist/panoramic/doc-graph-builder.js.map +1 -0
- package/dist/panoramic/docs-bundle-manifest-reader.d.ts +26 -0
- package/dist/panoramic/docs-bundle-manifest-reader.d.ts.map +1 -0
- package/dist/panoramic/docs-bundle-manifest-reader.js +106 -0
- package/dist/panoramic/docs-bundle-manifest-reader.js.map +1 -0
- package/dist/panoramic/docs-bundle-orchestrator.d.ts +14 -0
- package/dist/panoramic/docs-bundle-orchestrator.d.ts.map +1 -0
- package/dist/panoramic/docs-bundle-orchestrator.js +538 -0
- package/dist/panoramic/docs-bundle-orchestrator.js.map +1 -0
- package/dist/panoramic/docs-bundle-profiles.d.ts +4 -0
- package/dist/panoramic/docs-bundle-profiles.d.ts.map +1 -0
- package/dist/panoramic/docs-bundle-profiles.js +82 -0
- package/dist/panoramic/docs-bundle-profiles.js.map +1 -0
- package/dist/panoramic/docs-bundle-types.d.ts +94 -0
- package/dist/panoramic/docs-bundle-types.d.ts.map +1 -0
- package/dist/panoramic/docs-bundle-types.js +10 -0
- package/dist/panoramic/docs-bundle-types.js.map +1 -0
- package/dist/panoramic/docs-quality-evaluator.d.ts +31 -0
- package/dist/panoramic/docs-quality-evaluator.d.ts.map +1 -0
- package/dist/panoramic/docs-quality-evaluator.js +941 -0
- package/dist/panoramic/docs-quality-evaluator.js.map +1 -0
- package/dist/panoramic/docs-quality-model.d.ts +125 -0
- package/dist/panoramic/docs-quality-model.d.ts.map +1 -0
- package/dist/panoramic/docs-quality-model.js +166 -0
- package/dist/panoramic/docs-quality-model.js.map +1 -0
- package/dist/panoramic/dynamic-scenarios-builder.d.ts +18 -0
- package/dist/panoramic/dynamic-scenarios-builder.d.ts.map +1 -0
- package/dist/panoramic/dynamic-scenarios-builder.js +316 -0
- package/dist/panoramic/dynamic-scenarios-builder.js.map +1 -0
- package/dist/panoramic/event-surface-generator.d.ts +52 -0
- package/dist/panoramic/event-surface-generator.d.ts.map +1 -0
- package/dist/panoramic/event-surface-generator.js +507 -0
- package/dist/panoramic/event-surface-generator.js.map +1 -0
- package/dist/panoramic/generator-registry.d.ts +1 -1
- package/dist/panoramic/generator-registry.d.ts.map +1 -1
- package/dist/panoramic/generator-registry.js +5 -2
- package/dist/panoramic/generator-registry.js.map +1 -1
- package/dist/panoramic/generators/cross-package-analyzer.d.ts +5 -5
- package/dist/panoramic/generators/cross-package-analyzer.d.ts.map +1 -1
- package/dist/panoramic/generators/cross-package-analyzer.js +1 -1
- package/dist/panoramic/generators/cross-package-analyzer.js.map +1 -1
- package/dist/panoramic/generators/data-model-generator.d.ts +27 -27
- package/dist/panoramic/graph/confidence-mapper.d.ts +18 -0
- package/dist/panoramic/graph/confidence-mapper.d.ts.map +1 -1
- package/dist/panoramic/graph/confidence-mapper.js +26 -0
- package/dist/panoramic/graph/confidence-mapper.js.map +1 -1
- package/dist/panoramic/graph/graph-builder.d.ts.map +1 -1
- package/dist/panoramic/graph/graph-builder.js +78 -0
- package/dist/panoramic/graph/graph-builder.js.map +1 -1
- package/dist/panoramic/graph/graph-paths.d.ts +8 -0
- package/dist/panoramic/graph/graph-paths.d.ts.map +1 -1
- package/dist/panoramic/graph/graph-paths.js +10 -0
- package/dist/panoramic/graph/graph-paths.js.map +1 -1
- package/dist/panoramic/graph/graph-query.d.ts +33 -2
- package/dist/panoramic/graph/graph-query.d.ts.map +1 -1
- package/dist/panoramic/graph/graph-query.js +69 -19
- package/dist/panoramic/graph/graph-query.js.map +1 -1
- package/dist/panoramic/graph/graph-types.d.ts +32 -3
- package/dist/panoramic/graph/graph-types.d.ts.map +1 -1
- package/dist/panoramic/hyperedges/schema.d.ts +6 -6
- package/dist/panoramic/interface-surface-generator.d.ts +50 -0
- package/dist/panoramic/interface-surface-generator.d.ts.map +1 -0
- package/dist/panoramic/interface-surface-generator.js +388 -0
- package/dist/panoramic/interface-surface-generator.js.map +1 -0
- package/dist/panoramic/interfaces.d.ts +2 -2
- package/dist/panoramic/mock-readme-generator.d.ts +90 -0
- package/dist/panoramic/mock-readme-generator.d.ts.map +1 -0
- package/dist/panoramic/mock-readme-generator.js +111 -0
- package/dist/panoramic/mock-readme-generator.js.map +1 -0
- package/dist/panoramic/models/component-view-model.d.ts +1 -1
- package/dist/panoramic/models/component-view-model.d.ts.map +1 -1
- package/dist/panoramic/models/component-view-model.js.map +1 -1
- package/dist/panoramic/narrative-provenance-adapter.d.ts +8 -0
- package/dist/panoramic/narrative-provenance-adapter.d.ts.map +1 -0
- package/dist/panoramic/narrative-provenance-adapter.js +99 -0
- package/dist/panoramic/narrative-provenance-adapter.js.map +1 -0
- package/dist/panoramic/parser-registry.d.ts +1 -1
- package/dist/panoramic/parser-registry.js +1 -1
- package/dist/panoramic/parsers/types.d.ts +6 -6
- package/dist/panoramic/pattern-hints-generator.d.ts +28 -0
- package/dist/panoramic/pattern-hints-generator.d.ts.map +1 -0
- package/dist/panoramic/pattern-hints-generator.js +218 -0
- package/dist/panoramic/pattern-hints-generator.js.map +1 -0
- package/dist/panoramic/pattern-hints-model.d.ts +89 -0
- package/dist/panoramic/pattern-hints-model.d.ts.map +1 -0
- package/dist/panoramic/pattern-hints-model.js +61 -0
- package/dist/panoramic/pattern-hints-model.js.map +1 -0
- package/dist/panoramic/pattern-knowledge-base.d.ts +31 -0
- package/dist/panoramic/pattern-knowledge-base.d.ts.map +1 -0
- package/dist/panoramic/pattern-knowledge-base.js +407 -0
- package/dist/panoramic/pattern-knowledge-base.js.map +1 -0
- package/dist/panoramic/pipelines/coverage-auditor.d.ts.map +1 -1
- package/dist/panoramic/pipelines/coverage-auditor.js +4 -2
- package/dist/panoramic/pipelines/coverage-auditor.js.map +1 -1
- package/dist/panoramic/pipelines/docs-bundle-orchestrator.js +13 -6
- package/dist/panoramic/pipelines/docs-bundle-orchestrator.js.map +1 -1
- package/dist/panoramic/pipelines/product-ux-docs.d.ts +5 -0
- package/dist/panoramic/pipelines/product-ux-docs.d.ts.map +1 -1
- package/dist/panoramic/pipelines/product-ux-docs.js +34 -6
- package/dist/panoramic/pipelines/product-ux-docs.js.map +1 -1
- package/dist/panoramic/product-ux-docs.d.ts +104 -0
- package/dist/panoramic/product-ux-docs.d.ts.map +1 -0
- package/dist/panoramic/product-ux-docs.js +817 -0
- package/dist/panoramic/product-ux-docs.js.map +1 -0
- package/dist/panoramic/qa/__tests__/citation.test.d.ts +2 -0
- package/dist/panoramic/qa/__tests__/citation.test.d.ts.map +1 -0
- package/dist/panoramic/qa/__tests__/citation.test.js +193 -0
- package/dist/panoramic/qa/__tests__/citation.test.js.map +1 -0
- package/dist/panoramic/qa/__tests__/debt-context.test.d.ts +2 -0
- package/dist/panoramic/qa/__tests__/debt-context.test.d.ts.map +1 -0
- package/dist/panoramic/qa/__tests__/debt-context.test.js +154 -0
- package/dist/panoramic/qa/__tests__/debt-context.test.js.map +1 -0
- package/dist/panoramic/qa/__tests__/graph-retriever.test.d.ts +2 -0
- package/dist/panoramic/qa/__tests__/graph-retriever.test.d.ts.map +1 -0
- package/dist/panoramic/qa/__tests__/graph-retriever.test.js +148 -0
- package/dist/panoramic/qa/__tests__/graph-retriever.test.js.map +1 -0
- package/dist/panoramic/qa/__tests__/index.test.d.ts +2 -0
- package/dist/panoramic/qa/__tests__/index.test.d.ts.map +1 -0
- package/dist/panoramic/qa/__tests__/index.test.js +246 -0
- package/dist/panoramic/qa/__tests__/index.test.js.map +1 -0
- package/dist/panoramic/qa/__tests__/llm-caller.test.d.ts +2 -0
- package/dist/panoramic/qa/__tests__/llm-caller.test.d.ts.map +1 -0
- package/dist/panoramic/qa/__tests__/llm-caller.test.js +168 -0
- package/dist/panoramic/qa/__tests__/llm-caller.test.js.map +1 -0
- package/dist/panoramic/qa/__tests__/prompt-builder.test.d.ts +2 -0
- package/dist/panoramic/qa/__tests__/prompt-builder.test.d.ts.map +1 -0
- package/dist/panoramic/qa/__tests__/prompt-builder.test.js +150 -0
- package/dist/panoramic/qa/__tests__/prompt-builder.test.js.map +1 -0
- package/dist/panoramic/qa/__tests__/qa-integration.test.d.ts +2 -0
- package/dist/panoramic/qa/__tests__/qa-integration.test.d.ts.map +1 -0
- package/dist/panoramic/qa/__tests__/qa-integration.test.js +296 -0
- package/dist/panoramic/qa/__tests__/qa-integration.test.js.map +1 -0
- package/dist/panoramic/qa/__tests__/rag-reranker.test.d.ts +2 -0
- package/dist/panoramic/qa/__tests__/rag-reranker.test.d.ts.map +1 -0
- package/dist/panoramic/qa/__tests__/rag-reranker.test.js +136 -0
- package/dist/panoramic/qa/__tests__/rag-reranker.test.js.map +1 -0
- package/dist/panoramic/qa/index.d.ts.map +1 -1
- package/dist/panoramic/qa/index.js +10 -3
- package/dist/panoramic/qa/index.js.map +1 -1
- package/dist/panoramic/runtime-topology-generator.d.ts +77 -0
- package/dist/panoramic/runtime-topology-generator.d.ts.map +1 -0
- package/dist/panoramic/runtime-topology-generator.js +676 -0
- package/dist/panoramic/runtime-topology-generator.js.map +1 -0
- package/dist/panoramic/runtime-topology-model.d.ts +145 -0
- package/dist/panoramic/runtime-topology-model.d.ts.map +1 -0
- package/dist/panoramic/runtime-topology-model.js +300 -0
- package/dist/panoramic/runtime-topology-model.js.map +1 -0
- package/dist/panoramic/troubleshooting-generator.d.ts +51 -0
- package/dist/panoramic/troubleshooting-generator.d.ts.map +1 -0
- package/dist/panoramic/troubleshooting-generator.js +451 -0
- package/dist/panoramic/troubleshooting-generator.js.map +1 -0
- package/dist/panoramic/utils/text-segmenter.d.ts +15 -0
- package/dist/panoramic/utils/text-segmenter.d.ts.map +1 -1
- package/dist/panoramic/utils/text-segmenter.js +38 -0
- package/dist/panoramic/utils/text-segmenter.js.map +1 -1
- package/dist/panoramic/workspace-index-generator.d.ts +148 -0
- package/dist/panoramic/workspace-index-generator.d.ts.map +1 -0
- package/dist/panoramic/workspace-index-generator.js +497 -0
- package/dist/panoramic/workspace-index-generator.js.map +1 -0
- package/dist/runtime-bootstrap.d.ts +9 -0
- package/dist/runtime-bootstrap.d.ts.map +1 -0
- package/dist/runtime-bootstrap.js +33 -0
- package/dist/runtime-bootstrap.js.map +1 -0
- package/dist/utils/file-scanner.js +1 -1
- package/dist/utils/file-scanner.js.map +1 -1
- package/package.json +12 -2
- package/plugins/spec-driver/.claude-plugin/plugin.json +1 -1
- package/plugins/spec-driver/README.md +18 -1
- package/plugins/spec-driver/agents/implement.md +20 -1
- package/plugins/spec-driver/agents/plan.md +20 -1
- package/plugins/spec-driver/agents/quality-review.md +20 -1
- package/plugins/spec-driver/agents/spec-review.md +20 -1
- package/plugins/spec-driver/agents/verify.md +20 -1
- package/plugins/spec-driver/docs/customization.md +134 -0
- package/plugins/spec-driver/docs/spectra-mcp-integration.md +165 -0
- package/plugins/spec-driver/lib/preference-rules.mjs +96 -0
- package/plugins/spec-driver/scripts/postinstall.sh +1 -1
- package/plugins/spec-driver/scripts/sync-preference-rules.mjs +148 -0
- package/plugins/spec-driver/skills/spec-driver-feature/SKILL.md +10 -0
- package/plugins/spec-driver/skills/spec-driver-fix/SKILL.md +10 -0
- package/plugins/spec-driver/skills/spec-driver-implement/SKILL.md +10 -0
- package/plugins/spec-driver/skills/spec-driver-refactor/SKILL.md +10 -0
- package/plugins/spec-driver/skills/spec-driver-story/SKILL.md +10 -0
- package/plugins/spec-driver/templates/preference-rules.md +39 -0
- package/plugins/spectra/.claude-plugin/plugin.json +1 -1
- package/plugins/spectra/README.md +48 -1
- package/plugins/spectra/hooks/post-commit.sh +40 -0
- package/templates/product-overview.hbs +1 -1
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature 155 — Agent-Context MCP Tools (impact / context / detect_changes)
|
|
3
|
+
*
|
|
4
|
+
* 3 个 tool handler,建在 Feature 151 已 ship 的 UnifiedGraph + call-resolver 之上:
|
|
5
|
+
* - impact: blast radius — 反向 BFS 找受影响 symbols
|
|
6
|
+
* - context: 360° symbol 视图 — definition + callers + callees + imports + relatedSpec
|
|
7
|
+
* - detect_changes: git diff → changedSymbols → impact 链
|
|
8
|
+
*
|
|
9
|
+
* 数据流:
|
|
10
|
+
* getCachedGraphData(projectRoot) // 来自 graph-tools.ts,含 mtime/size stale 检测
|
|
11
|
+
* ↓
|
|
12
|
+
* query-helpers.ts (bfsTraverse / canonicalizeSymbolId / findFuzzyMatches / ...)
|
|
13
|
+
* ↓
|
|
14
|
+
* tool handler 输出 JSON envelope
|
|
15
|
+
*
|
|
16
|
+
* 错误处理(FR-050):统一 envelope `{ isError: true, content: [{type:'text', text: JSON({code, message, hint?, context?})}]}`
|
|
17
|
+
* 错误 code: graph-not-built / symbol-not-found / invalid-symbol-id / invalid-input /
|
|
18
|
+
* invalid-diff / payload-too-large / git-spawn-failed / git-timeout / internal-error
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { appendFileSync, existsSync } from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { getCachedGraphData } from './graph-tools.js';
|
|
25
|
+
import { bfsTraverse, canonicalizeSymbolId, computeRiskTier, findFuzzyMatches, findNode, moduleFileFromId, resolveEdgeConfidence, } from '../knowledge-graph/query-helpers.js';
|
|
26
|
+
import { buildTopImpactedRanking, buildTopRelevantCallers, generateNextStepHint, safeStderrLog, } from './lib/response-helpers.js';
|
|
27
|
+
function buildErrorResponse(code, message, hint, context) {
|
|
28
|
+
const payload = { code, message };
|
|
29
|
+
if (hint !== undefined)
|
|
30
|
+
payload['hint'] = hint;
|
|
31
|
+
if (context !== undefined)
|
|
32
|
+
payload['context'] = context;
|
|
33
|
+
return {
|
|
34
|
+
isError: true,
|
|
35
|
+
content: [{ type: 'text', text: JSON.stringify(payload) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 写入 telemetry JSONL —— 静默降级:
|
|
40
|
+
* - env 未设置 → no-op
|
|
41
|
+
* - 写入失败 → 静默吞,不影响 MCP response
|
|
42
|
+
*
|
|
43
|
+
* 实现说明(Codex W5 评估):使用 appendFileSync 而非 async appendFile。
|
|
44
|
+
* - eval 场景每 tool call 一次(非高频),SSD 上 sync write < 5ms 不构成瓶颈
|
|
45
|
+
* - sync 保证测试可重现(async fire-and-forget 在测试 readFileSync 时可能 race)
|
|
46
|
+
* - 如未来高频调用场景出现(生产 long-running MCP server),可切换到 async
|
|
47
|
+
*
|
|
48
|
+
* Feature 155 input/output schema 不变(FR-G 合同保护)。
|
|
49
|
+
*/
|
|
50
|
+
export function writeTelemetry(entry) {
|
|
51
|
+
const telPath = process.env['SPECTRA_MCP_TELEMETRY_PATH'];
|
|
52
|
+
if (telPath === undefined || telPath.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
appendFileSync(telPath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// FR-G-002: silent degrade — 写入失败时不抛异常,不影响 handler 返回
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 从 ToolResult 中提取 errorCode(仅 isError=true 时存在)。
|
|
63
|
+
* 解析失败时返回 undefined(telemetry 字段缺省)。
|
|
64
|
+
*/
|
|
65
|
+
function extractErrorCode(result) {
|
|
66
|
+
if (result.isError !== true)
|
|
67
|
+
return undefined;
|
|
68
|
+
const text = result.content?.[0]?.text;
|
|
69
|
+
if (typeof text !== 'string')
|
|
70
|
+
return undefined;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(text);
|
|
73
|
+
return typeof parsed.code === 'string' ? parsed.code : undefined;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Wrapper:记录 handler 调用 telemetry 后返回原 result。
|
|
81
|
+
* 包裹所有 return 路径(含 buildErrorResponse 早 return)。
|
|
82
|
+
*
|
|
83
|
+
* 设计:调用前先采样 startTime + requestSize,handler 完成后调本函数。
|
|
84
|
+
* 不破坏 Feature 155 的 input/output schema:result 原样返回。
|
|
85
|
+
*/
|
|
86
|
+
export function recordAndReturn(toolName, startTimeMs, requestSize, result, responseSummary, responseSamples) {
|
|
87
|
+
const responseText = result.content?.[0]?.text ?? '';
|
|
88
|
+
// 修 Codex W4:用 UTF-8 byte length 而非 char length(中文 / emoji 不会被低估)
|
|
89
|
+
const responseSize = typeof responseText === 'string' ? Buffer.byteLength(responseText, 'utf-8') : 0;
|
|
90
|
+
const entry = {
|
|
91
|
+
ts: new Date().toISOString(),
|
|
92
|
+
toolName,
|
|
93
|
+
requestSize,
|
|
94
|
+
responseSize,
|
|
95
|
+
durationMs: Date.now() - startTimeMs,
|
|
96
|
+
runId: process.env['SPECTRA_MCP_RUN_ID'] ?? 'unknown',
|
|
97
|
+
};
|
|
98
|
+
const errorCode = extractErrorCode(result);
|
|
99
|
+
if (errorCode !== undefined)
|
|
100
|
+
entry.errorCode = errorCode;
|
|
101
|
+
// Feature 165 — 仅 success 路径写 responseSummary / responseSamples(avoid 噪音字段)
|
|
102
|
+
if (responseSummary !== undefined && errorCode === undefined) {
|
|
103
|
+
entry.responseSummary = responseSummary;
|
|
104
|
+
}
|
|
105
|
+
if (responseSamples !== undefined && errorCode === undefined) {
|
|
106
|
+
entry.responseSamples = responseSamples;
|
|
107
|
+
}
|
|
108
|
+
writeTelemetry(entry);
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
const PAYLOAD_CAP_BYTES = 1_000_000;
|
|
112
|
+
/**
|
|
113
|
+
* 把响应序列化并应用 1 MB 上限(FR-053)。
|
|
114
|
+
* 超限时优先 truncate `affected` / `affectedSymbols` 列表(在 dataMutator 中执行)。
|
|
115
|
+
*/
|
|
116
|
+
function buildSuccessResponse(data, truncatableArrayKeys = [], warningsKey = 'warnings') {
|
|
117
|
+
let text = JSON.stringify(data);
|
|
118
|
+
let bytes = Buffer.byteLength(text, 'utf-8');
|
|
119
|
+
if (bytes > PAYLOAD_CAP_BYTES && truncatableArrayKeys.length > 0) {
|
|
120
|
+
let truncated = false;
|
|
121
|
+
let safety = 0;
|
|
122
|
+
while (bytes > PAYLOAD_CAP_BYTES && safety < 8) {
|
|
123
|
+
safety++;
|
|
124
|
+
let progressed = false;
|
|
125
|
+
for (const key of truncatableArrayKeys) {
|
|
126
|
+
const arr = data[key];
|
|
127
|
+
if (!Array.isArray(arr) || arr.length === 0)
|
|
128
|
+
continue;
|
|
129
|
+
// 按比例 0.7 收缩,至少减 1
|
|
130
|
+
const ratio = PAYLOAD_CAP_BYTES / bytes;
|
|
131
|
+
const newLen = Math.max(0, Math.min(arr.length - 1, Math.floor(arr.length * ratio * 0.7)));
|
|
132
|
+
if (newLen < arr.length) {
|
|
133
|
+
data[key] = arr.slice(0, newLen);
|
|
134
|
+
truncated = true;
|
|
135
|
+
progressed = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!progressed)
|
|
139
|
+
break;
|
|
140
|
+
text = JSON.stringify(data);
|
|
141
|
+
bytes = Buffer.byteLength(text, 'utf-8');
|
|
142
|
+
}
|
|
143
|
+
if (truncated) {
|
|
144
|
+
const warnings = data[warningsKey] ?? [];
|
|
145
|
+
if (!warnings.includes('payload-truncated')) {
|
|
146
|
+
data[warningsKey] = [...warnings, 'payload-truncated'];
|
|
147
|
+
text = JSON.stringify(data);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { content: [{ type: 'text', text }] };
|
|
152
|
+
}
|
|
153
|
+
// ============================================================
|
|
154
|
+
// impact tool
|
|
155
|
+
// ============================================================
|
|
156
|
+
const ImpactInputSchema = {
|
|
157
|
+
target: z.string().describe('symbol id (e.g. "micrograd/engine.py::Value.__add__")'),
|
|
158
|
+
depth: z.number().int().min(0).max(20).optional().describe('BFS depth (default 2, max 5; 超出 clamp)'),
|
|
159
|
+
minConfidence: z.number().min(0).max(1).optional().describe('confidence 阈值 (default 0.65)'),
|
|
160
|
+
direction: z.enum(['upstream', 'downstream', 'both']).optional().describe('BFS 方向 (default upstream)'),
|
|
161
|
+
budget: z.number().int().min(0).max(10000).optional().describe('节点上限 (default 200, max 1000; 超出 clamp)'),
|
|
162
|
+
projectRoot: z.string().optional().describe('default cwd'),
|
|
163
|
+
};
|
|
164
|
+
export async function handleImpact(args) {
|
|
165
|
+
// Feature 158 telemetry: 入口采样
|
|
166
|
+
const _telStart = Date.now();
|
|
167
|
+
const _telReqSize = (() => {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.stringify(args).length;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
try {
|
|
176
|
+
if (typeof args.target !== 'string' || args.target.length === 0) {
|
|
177
|
+
return recordAndReturn('impact', _telStart, _telReqSize, buildErrorResponse('invalid-input', 'target 必填且为非空字符串'));
|
|
178
|
+
}
|
|
179
|
+
const projectRoot = args.projectRoot ?? process.cwd();
|
|
180
|
+
const cached = getCachedGraphData(projectRoot);
|
|
181
|
+
if (cached === null) {
|
|
182
|
+
return recordAndReturn('impact', _telStart, _telReqSize, buildErrorResponse('graph-not-built', `graph.json 不存在或加载失败 (projectRoot=${projectRoot})`, '请先运行 `spectra batch` 或 `spectra prepare` 生成图谱'));
|
|
183
|
+
}
|
|
184
|
+
const { graphData, graphPath, mtimeMs, sizeBytes } = cached;
|
|
185
|
+
// 入参 clamp(FR-015)— handler 层负责 clamp 并附 warning,不依赖 zod max
|
|
186
|
+
const warnings = [];
|
|
187
|
+
const reqDepth = args.depth ?? 2;
|
|
188
|
+
const effectiveDepth = Math.min(Math.max(reqDepth, 0), 5);
|
|
189
|
+
if (effectiveDepth !== reqDepth)
|
|
190
|
+
warnings.push('depth-clamped');
|
|
191
|
+
const reqBudget = args.budget ?? 200;
|
|
192
|
+
const effectiveBudget = Math.min(Math.max(reqBudget, 0), 1000);
|
|
193
|
+
if (effectiveBudget !== reqBudget)
|
|
194
|
+
warnings.push('budget-clamped');
|
|
195
|
+
const reqMinConf = args.minConfidence ?? 0.65;
|
|
196
|
+
const minConfidence = Math.min(Math.max(reqMinConf, 0), 1);
|
|
197
|
+
if (minConfidence !== reqMinConf)
|
|
198
|
+
warnings.push('minConfidence-clamped');
|
|
199
|
+
const direction = args.direction ?? 'upstream';
|
|
200
|
+
// canonicalize symbol id
|
|
201
|
+
const canon = canonicalizeSymbolId(args.target, graphData, { projectRoot });
|
|
202
|
+
if (canon.reason === 'invalid') {
|
|
203
|
+
return recordAndReturn('impact', _telStart, _telReqSize, buildErrorResponse('invalid-symbol-id', `target 含非法字符或格式: ${args.target}`));
|
|
204
|
+
}
|
|
205
|
+
if (canon.reason === 'not-found' || canon.canonicalId === null) {
|
|
206
|
+
const fuzzy = findFuzzyMatches(graphData, args.target, 5);
|
|
207
|
+
return recordAndReturn('impact', _telStart, _telReqSize, buildErrorResponse('symbol-not-found', `target 在 graph 中未找到: ${args.target}`, '请检查 symbol id 格式或参考 fuzzyMatches 候选', { fuzzyMatches: fuzzy }));
|
|
208
|
+
}
|
|
209
|
+
const startId = canon.canonicalId;
|
|
210
|
+
// BFS
|
|
211
|
+
const r = bfsTraverse(graphData, startId, {
|
|
212
|
+
depth: effectiveDepth,
|
|
213
|
+
minConfidence,
|
|
214
|
+
direction,
|
|
215
|
+
budget: effectiveBudget,
|
|
216
|
+
graphPath,
|
|
217
|
+
graphMtimeMs: mtimeMs,
|
|
218
|
+
graphSizeBytes: sizeBytes,
|
|
219
|
+
relations: ['calls'],
|
|
220
|
+
});
|
|
221
|
+
// 合并 warnings
|
|
222
|
+
for (const w of r.warnings) {
|
|
223
|
+
if (!warnings.includes(w))
|
|
224
|
+
warnings.push(w);
|
|
225
|
+
}
|
|
226
|
+
// summary
|
|
227
|
+
const directCallers = r.affected.filter((a) => a.depth === 1).length;
|
|
228
|
+
const transitive = r.affected.length;
|
|
229
|
+
const riskTier = computeRiskTier(directCallers, transitive);
|
|
230
|
+
const data = {
|
|
231
|
+
affected: r.affected,
|
|
232
|
+
summary: { directCallers, transitive, riskTier },
|
|
233
|
+
effectiveDepth,
|
|
234
|
+
effectiveMinConfidence: minConfidence,
|
|
235
|
+
effectiveBudget,
|
|
236
|
+
effectiveDirection: direction,
|
|
237
|
+
};
|
|
238
|
+
if (warnings.length > 0)
|
|
239
|
+
data['warnings'] = warnings;
|
|
240
|
+
// F170c enrichment 三路径(plan G 节):临时变量 + 显式 catch reset 避免 partial fill
|
|
241
|
+
let topImpacted;
|
|
242
|
+
let nextStepHint;
|
|
243
|
+
let enrichmentDegraded;
|
|
244
|
+
try {
|
|
245
|
+
const _topImpacted = buildTopImpactedRanking(r.affected, 5);
|
|
246
|
+
const _nextStepHint = generateNextStepHint('impact', { topImpacted: _topImpacted, affected: r.affected }, 'success');
|
|
247
|
+
topImpacted = _topImpacted;
|
|
248
|
+
nextStepHint = _nextStepHint;
|
|
249
|
+
enrichmentDegraded = false;
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
topImpacted = [];
|
|
253
|
+
nextStepHint = '';
|
|
254
|
+
enrichmentDegraded = true;
|
|
255
|
+
safeStderrLog(`[F170c] impact enrichment degraded: ${String(e)}\n`);
|
|
256
|
+
}
|
|
257
|
+
data['topImpacted'] = topImpacted;
|
|
258
|
+
data['nextStepHint'] = nextStepHint;
|
|
259
|
+
if (enrichmentDegraded)
|
|
260
|
+
data['_enrichmentDegraded'] = true;
|
|
261
|
+
return recordAndReturn('impact', _telStart, _telReqSize, buildSuccessResponse(data, ['affected']));
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
return recordAndReturn('impact', _telStart, _telReqSize, buildErrorResponse('internal-error', err instanceof Error ? err.message : String(err), undefined, { stack: err instanceof Error && err.stack ? err.stack.slice(0, 200) : undefined }));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// ============================================================
|
|
268
|
+
// context tool
|
|
269
|
+
// ============================================================
|
|
270
|
+
const ContextInputSchema = {
|
|
271
|
+
symbolId: z.string().describe('symbol id'),
|
|
272
|
+
include: z
|
|
273
|
+
.array(z.enum(['callers', 'callees', 'imports', 'related-spec']))
|
|
274
|
+
.optional()
|
|
275
|
+
.describe('字段子集 (default ["callers","callees","imports"])'),
|
|
276
|
+
projectRoot: z.string().optional(),
|
|
277
|
+
};
|
|
278
|
+
export async function handleContext(args) {
|
|
279
|
+
// Feature 158 telemetry: 入口采样
|
|
280
|
+
const _telStart = Date.now();
|
|
281
|
+
const _telReqSize = (() => {
|
|
282
|
+
try {
|
|
283
|
+
return JSON.stringify(args).length;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
})();
|
|
289
|
+
try {
|
|
290
|
+
if (typeof args.symbolId !== 'string' || args.symbolId.length === 0) {
|
|
291
|
+
return recordAndReturn('context', _telStart, _telReqSize, buildErrorResponse('invalid-input', 'symbolId 必填且为非空字符串'));
|
|
292
|
+
}
|
|
293
|
+
const projectRoot = args.projectRoot ?? process.cwd();
|
|
294
|
+
const cached = getCachedGraphData(projectRoot);
|
|
295
|
+
if (cached === null) {
|
|
296
|
+
return recordAndReturn('context', _telStart, _telReqSize, buildErrorResponse('graph-not-built', `graph.json 不存在 (projectRoot=${projectRoot})`, '请先运行 `spectra batch` 生成图谱'));
|
|
297
|
+
}
|
|
298
|
+
const { graphData } = cached;
|
|
299
|
+
const include = args.include ?? ['callers', 'callees', 'imports'];
|
|
300
|
+
const canon = canonicalizeSymbolId(args.symbolId, graphData, { projectRoot });
|
|
301
|
+
if (canon.reason === 'invalid') {
|
|
302
|
+
return recordAndReturn('context', _telStart, _telReqSize, buildErrorResponse('invalid-symbol-id', `symbolId 含非法字符: ${args.symbolId}`));
|
|
303
|
+
}
|
|
304
|
+
if (canon.reason === 'not-found' || canon.canonicalId === null) {
|
|
305
|
+
const fuzzy = findFuzzyMatches(graphData, args.symbolId, 5);
|
|
306
|
+
return recordAndReturn('context', _telStart, _telReqSize, buildErrorResponse('symbol-not-found', `symbolId 在 graph 中未找到: ${args.symbolId}`, '请检查 id 格式或参考 fuzzyMatches 候选', { fuzzyMatches: fuzzy }));
|
|
307
|
+
}
|
|
308
|
+
const node = findNode(graphData, canon.canonicalId);
|
|
309
|
+
if (node === null) {
|
|
310
|
+
return recordAndReturn('context', _telStart, _telReqSize, buildErrorResponse('symbol-not-found', `节点对象未找到: ${canon.canonicalId}`));
|
|
311
|
+
}
|
|
312
|
+
const definition = buildDefinition(node);
|
|
313
|
+
const data = { definition };
|
|
314
|
+
if (include.includes('callers')) {
|
|
315
|
+
data['callers'] = collectNeighbors(graphData, canon.canonicalId, 'inbound', 'calls');
|
|
316
|
+
}
|
|
317
|
+
if (include.includes('callees')) {
|
|
318
|
+
data['callees'] = collectNeighbors(graphData, canon.canonicalId, 'outbound', 'calls');
|
|
319
|
+
}
|
|
320
|
+
if (include.includes('imports')) {
|
|
321
|
+
// imports 来自 module 节点的 outbound depends-on / cross-module
|
|
322
|
+
const moduleId = moduleFileFromId(canon.canonicalId);
|
|
323
|
+
const importEntries = collectNeighbors(graphData, moduleId, 'outbound', 'depends-on').concat(collectNeighbors(graphData, moduleId, 'outbound', 'cross-module'));
|
|
324
|
+
// 转换为 import schema
|
|
325
|
+
data['imports'] = importEntries.map((x) => ({
|
|
326
|
+
moduleId: x.id,
|
|
327
|
+
file: moduleFileFromId(x.id),
|
|
328
|
+
confidence: x.confidence,
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
if (include.includes('related-spec')) {
|
|
332
|
+
data['relatedSpec'] = deriveRelatedSpec(canon.canonicalId, projectRoot);
|
|
333
|
+
}
|
|
334
|
+
// F170c enrichment 三路径(plan G 节)
|
|
335
|
+
const callersRaw = data['callers'] ?? [];
|
|
336
|
+
let topRelevantCallers;
|
|
337
|
+
let nextStepHint;
|
|
338
|
+
let enrichmentDegraded;
|
|
339
|
+
try {
|
|
340
|
+
const _top = buildTopRelevantCallers(callersRaw, 3);
|
|
341
|
+
const _hint = generateNextStepHint('context', { definition, callers: callersRaw }, 'success');
|
|
342
|
+
topRelevantCallers = _top;
|
|
343
|
+
nextStepHint = _hint;
|
|
344
|
+
enrichmentDegraded = false;
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
topRelevantCallers = [];
|
|
348
|
+
nextStepHint = '';
|
|
349
|
+
enrichmentDegraded = true;
|
|
350
|
+
safeStderrLog(`[F170c] context enrichment degraded: ${String(e)}\n`);
|
|
351
|
+
}
|
|
352
|
+
data['topRelevantCallers'] = topRelevantCallers;
|
|
353
|
+
data['nextStepHint'] = nextStepHint;
|
|
354
|
+
if (enrichmentDegraded)
|
|
355
|
+
data['_enrichmentDegraded'] = true;
|
|
356
|
+
return recordAndReturn('context', _telStart, _telReqSize, buildSuccessResponse(data, ['callers', 'callees', 'imports']));
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
return recordAndReturn('context', _telStart, _telReqSize, buildErrorResponse('internal-error', err instanceof Error ? err.message : String(err), undefined, { stack: err instanceof Error && err.stack ? err.stack.slice(0, 200) : undefined }));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function buildDefinition(node) {
|
|
363
|
+
const md = node.metadata;
|
|
364
|
+
const def = {
|
|
365
|
+
id: node.id,
|
|
366
|
+
file: md['sourceFile'] ?? md['sourcePath'] ?? moduleFileFromId(node.id),
|
|
367
|
+
kind: node.kind,
|
|
368
|
+
label: node.label,
|
|
369
|
+
};
|
|
370
|
+
const lineRange = md['lineRange'];
|
|
371
|
+
if (lineRange) {
|
|
372
|
+
if (typeof lineRange.start === 'number')
|
|
373
|
+
def['lineStart'] = lineRange.start;
|
|
374
|
+
if (typeof lineRange.end === 'number')
|
|
375
|
+
def['lineEnd'] = lineRange.end;
|
|
376
|
+
}
|
|
377
|
+
const conf = md['confidence'];
|
|
378
|
+
if (typeof conf === 'string')
|
|
379
|
+
def['confidence'] = conf;
|
|
380
|
+
return def;
|
|
381
|
+
}
|
|
382
|
+
function collectNeighbors(graphData, nodeId, direction, relation) {
|
|
383
|
+
const out = [];
|
|
384
|
+
for (const link of graphData.links) {
|
|
385
|
+
if (link.relation !== relation)
|
|
386
|
+
continue;
|
|
387
|
+
const conf = resolveEdgeConfidence(link);
|
|
388
|
+
if (conf === null)
|
|
389
|
+
continue;
|
|
390
|
+
if (direction === 'inbound' && link.target === nodeId) {
|
|
391
|
+
out.push({ id: link.source, confidence: conf, relation: link.relation });
|
|
392
|
+
}
|
|
393
|
+
else if (direction === 'outbound' && link.source === nodeId) {
|
|
394
|
+
out.push({ id: link.target, confidence: conf, relation: link.relation });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
function deriveRelatedSpec(symbolId, projectRoot) {
|
|
400
|
+
const moduleFile = moduleFileFromId(symbolId);
|
|
401
|
+
const slug = path.basename(moduleFile, path.extname(moduleFile));
|
|
402
|
+
const candidates = [
|
|
403
|
+
path.join(projectRoot, 'panoramic', 'modules', `${slug}.spec.md`),
|
|
404
|
+
path.join(projectRoot, 'specs', 'products', 'spectra', '_generated', 'modules', `${slug}.spec.md`),
|
|
405
|
+
path.join(projectRoot, '_meta', 'modules', `${slug}.spec.md`),
|
|
406
|
+
];
|
|
407
|
+
for (const c of candidates) {
|
|
408
|
+
if (existsSync(c)) {
|
|
409
|
+
return { kind: 'module-coarse', path: path.relative(projectRoot, c) };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return { kind: 'unknown' };
|
|
413
|
+
}
|
|
414
|
+
// ============================================================
|
|
415
|
+
// detect_changes tool
|
|
416
|
+
// ============================================================
|
|
417
|
+
const DetectChangesInputSchema = {
|
|
418
|
+
diff: z.string().optional().describe('unified diff 文本'),
|
|
419
|
+
baseRef: z.string().optional().describe('git ref (e.g. HEAD~1)'),
|
|
420
|
+
projectRoot: z.string().optional(),
|
|
421
|
+
depth: z.number().int().min(0).max(10).optional().describe('default 2, max 5'),
|
|
422
|
+
budget: z.number().int().min(0).max(10000).optional().describe('default 200, max 1000'),
|
|
423
|
+
minConfidence: z.number().min(0).max(1).optional().describe('default 0.65'),
|
|
424
|
+
};
|
|
425
|
+
const MAX_DIFF_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
426
|
+
const BASEREF_WHITELIST = /^[A-Za-z0-9_./~^@{}-]+$/;
|
|
427
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
428
|
+
const REVPARSE_TIMEOUT_MS = 5_000;
|
|
429
|
+
export async function handleDetectChanges(args) {
|
|
430
|
+
// Feature 158 telemetry: 入口采样
|
|
431
|
+
const _telStart = Date.now();
|
|
432
|
+
const _telReqSize = (() => {
|
|
433
|
+
try {
|
|
434
|
+
return JSON.stringify(args).length;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
})();
|
|
440
|
+
try {
|
|
441
|
+
const hasDiff = typeof args.diff === 'string' && args.diff.length > 0;
|
|
442
|
+
const hasBaseRef = typeof args.baseRef === 'string' && args.baseRef.length > 0;
|
|
443
|
+
if (!hasDiff && !hasBaseRef) {
|
|
444
|
+
return recordAndReturn('detect_changes', _telStart, _telReqSize, buildErrorResponse('invalid-input', '必须提供 diff 或 baseRef 之一', undefined, { reason: 'diff-or-baseref-required' }));
|
|
445
|
+
}
|
|
446
|
+
const warnings = [];
|
|
447
|
+
if (hasDiff && hasBaseRef) {
|
|
448
|
+
warnings.push('baseRef-ignored');
|
|
449
|
+
}
|
|
450
|
+
const projectRoot = args.projectRoot ?? process.cwd();
|
|
451
|
+
const cached = getCachedGraphData(projectRoot);
|
|
452
|
+
if (cached === null) {
|
|
453
|
+
return recordAndReturn('detect_changes', _telStart, _telReqSize, buildErrorResponse('graph-not-built', `graph.json 不存在 (projectRoot=${projectRoot})`, '请先运行 `spectra batch` 生成图谱'));
|
|
454
|
+
}
|
|
455
|
+
const { graphData, graphPath, mtimeMs, sizeBytes } = cached;
|
|
456
|
+
// 1) 拿改动文件列表
|
|
457
|
+
let changedFiles;
|
|
458
|
+
let unmappedFromInput = [];
|
|
459
|
+
if (hasDiff) {
|
|
460
|
+
// 5MB 上限校验前置(CRITICAL fix:返回 payload-too-large 而非 invalid-diff)
|
|
461
|
+
if (Buffer.byteLength(args.diff, 'utf-8') > MAX_DIFF_BYTES) {
|
|
462
|
+
return recordAndReturn('detect_changes', _telStart, _telReqSize, buildErrorResponse('payload-too-large', `diff 超过上限 ${MAX_DIFF_BYTES} 字节`, undefined, { limitBytes: MAX_DIFF_BYTES }));
|
|
463
|
+
}
|
|
464
|
+
const parsed = parseUnifiedDiff(args.diff);
|
|
465
|
+
if (parsed.error !== undefined) {
|
|
466
|
+
return recordAndReturn('detect_changes', _telStart, _telReqSize, buildErrorResponse('invalid-diff', parsed.error));
|
|
467
|
+
}
|
|
468
|
+
changedFiles = parsed.changed;
|
|
469
|
+
unmappedFromInput = parsed.unmapped;
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
const r = runGitDiffNameStatus(args.baseRef, projectRoot);
|
|
473
|
+
if (!r.ok) {
|
|
474
|
+
return recordAndReturn('detect_changes', _telStart, _telReqSize, buildErrorResponse(r.code, r.message, undefined, r.context));
|
|
475
|
+
}
|
|
476
|
+
changedFiles = r.changed;
|
|
477
|
+
unmappedFromInput = r.unmapped;
|
|
478
|
+
}
|
|
479
|
+
// 没有改动文件 → success warning(FR-050 'no-changed-files' 是 warning 不是 error)
|
|
480
|
+
if (changedFiles.length === 0 && unmappedFromInput.length === 0) {
|
|
481
|
+
warnings.push('no-changed-files');
|
|
482
|
+
}
|
|
483
|
+
// 2) file → graph symbols 映射
|
|
484
|
+
const fileToSymbols = buildFileSymbolIndex(graphData);
|
|
485
|
+
const changedSymbolsOut = [];
|
|
486
|
+
const unmappedFiles = [...unmappedFromInput];
|
|
487
|
+
const allChangedSymbolIds = [];
|
|
488
|
+
for (const cf of changedFiles) {
|
|
489
|
+
const symbols = fileToSymbols.get(cf.file);
|
|
490
|
+
if (symbols === undefined || symbols.length === 0) {
|
|
491
|
+
unmappedFiles.push({ file: cf.file, reason: 'not-in-graph' });
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
changedSymbolsOut.push({ file: cf.file, changeKind: cf.changeKind, symbols });
|
|
495
|
+
for (const s of symbols)
|
|
496
|
+
allChangedSymbolIds.push(s);
|
|
497
|
+
}
|
|
498
|
+
// 3) 跨 changedSymbol 共享 budget BFS
|
|
499
|
+
const reqDepth = args.depth ?? 2;
|
|
500
|
+
const effectiveDepth = Math.min(Math.max(reqDepth, 0), 5);
|
|
501
|
+
if (effectiveDepth !== reqDepth)
|
|
502
|
+
warnings.push('depth-clamped');
|
|
503
|
+
const reqBudget = args.budget ?? 200;
|
|
504
|
+
const effectiveBudget = Math.min(Math.max(reqBudget, 0), 1000);
|
|
505
|
+
if (effectiveBudget !== reqBudget)
|
|
506
|
+
warnings.push('budget-clamped');
|
|
507
|
+
const minConfidence = args.minConfidence ?? 0.65;
|
|
508
|
+
const sharedVisited = new Set();
|
|
509
|
+
for (const s of allChangedSymbolIds)
|
|
510
|
+
sharedVisited.add(s);
|
|
511
|
+
const affectedAcc = [];
|
|
512
|
+
let remaining = effectiveBudget;
|
|
513
|
+
for (const startId of allChangedSymbolIds) {
|
|
514
|
+
if (remaining <= 0) {
|
|
515
|
+
warnings.push('budget-truncated');
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
const r = bfsTraverse(graphData, startId, {
|
|
519
|
+
depth: effectiveDepth,
|
|
520
|
+
minConfidence,
|
|
521
|
+
direction: 'upstream',
|
|
522
|
+
budget: remaining,
|
|
523
|
+
sharedVisited,
|
|
524
|
+
graphPath,
|
|
525
|
+
graphMtimeMs: mtimeMs,
|
|
526
|
+
graphSizeBytes: sizeBytes,
|
|
527
|
+
relations: ['calls'],
|
|
528
|
+
});
|
|
529
|
+
affectedAcc.push(...r.affected);
|
|
530
|
+
remaining = effectiveBudget - affectedAcc.length;
|
|
531
|
+
for (const w of r.warnings) {
|
|
532
|
+
if (!warnings.includes(w))
|
|
533
|
+
warnings.push(w);
|
|
534
|
+
}
|
|
535
|
+
if (r.warnings.includes('budget-truncated'))
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
// de-warning(去重)
|
|
539
|
+
const uniqWarnings = [...new Set(warnings)];
|
|
540
|
+
const totalChanged = allChangedSymbolIds.length;
|
|
541
|
+
const totalAffected = affectedAcc.length;
|
|
542
|
+
const riskTier = computeRiskTier(0, totalAffected);
|
|
543
|
+
const data = {
|
|
544
|
+
changedSymbols: changedSymbolsOut,
|
|
545
|
+
affectedSymbols: affectedAcc,
|
|
546
|
+
riskSummary: { totalChanged, totalAffected, riskTier },
|
|
547
|
+
unmappedFiles,
|
|
548
|
+
effectiveBudget,
|
|
549
|
+
effectiveDepth,
|
|
550
|
+
effectiveMinConfidence: minConfidence,
|
|
551
|
+
};
|
|
552
|
+
if (uniqWarnings.length > 0)
|
|
553
|
+
data['warnings'] = uniqWarnings;
|
|
554
|
+
// F170c enrichment(plan G + D 节)
|
|
555
|
+
const enrichment = _computeDetectChangesEnrichment(affectedAcc, riskTier, totalChanged);
|
|
556
|
+
data['riskTier'] = riskTier;
|
|
557
|
+
data['topImpacted'] = enrichment.topImpacted;
|
|
558
|
+
data['nextStepHint'] = enrichment.nextStepHint;
|
|
559
|
+
if (enrichment.degraded)
|
|
560
|
+
data['_enrichmentDegraded'] = true;
|
|
561
|
+
// telemetry sample(Feature 165)
|
|
562
|
+
const { symbolSample, fileSample } = _buildTelemetrySamples(changedSymbolsOut);
|
|
563
|
+
return recordAndReturn('detect_changes', _telStart, _telReqSize, buildSuccessResponse(data, ['affectedSymbols']), { changedSymbolsCount: totalChanged }, { symbols: symbolSample, files: fileSample });
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
return recordAndReturn('detect_changes', _telStart, _telReqSize, buildErrorResponse('internal-error', err instanceof Error ? err.message : String(err), undefined, { stack: err instanceof Error && err.stack ? err.stack.slice(0, 200) : undefined }));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// ─── detect_changes 私有辅助函数(F170c T-GREEN-2 cleanup) ───
|
|
570
|
+
function _computeDetectChangesEnrichment(affectedAcc, riskTier, totalChanged) {
|
|
571
|
+
try {
|
|
572
|
+
const topImpacted = buildTopImpactedRanking(affectedAcc, 5);
|
|
573
|
+
const nextStepHint = generateNextStepHint('detect_changes', { topImpacted, riskTier, totalChanged }, 'success');
|
|
574
|
+
return { topImpacted, nextStepHint, degraded: false };
|
|
575
|
+
}
|
|
576
|
+
catch (e) {
|
|
577
|
+
safeStderrLog(`[F170c] detect_changes enrichment degraded: ${String(e)}\n`);
|
|
578
|
+
return { topImpacted: [], nextStepHint: '', degraded: true };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function _buildTelemetrySamples(changedSymbolsOut) {
|
|
582
|
+
const sampleMaxN = 10;
|
|
583
|
+
const symbolSample = [];
|
|
584
|
+
for (const cf of changedSymbolsOut) {
|
|
585
|
+
for (const sym of cf.symbols) {
|
|
586
|
+
if (symbolSample.length >= sampleMaxN)
|
|
587
|
+
break;
|
|
588
|
+
symbolSample.push(sym);
|
|
589
|
+
}
|
|
590
|
+
if (symbolSample.length >= sampleMaxN)
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
const fileSample = [];
|
|
594
|
+
for (const cf of changedSymbolsOut) {
|
|
595
|
+
if (fileSample.length >= sampleMaxN)
|
|
596
|
+
break;
|
|
597
|
+
fileSample.push(cf.file);
|
|
598
|
+
}
|
|
599
|
+
return { symbolSample, fileSample };
|
|
600
|
+
}
|
|
601
|
+
// ─── unified diff 解析 ─────────────────────────────────────
|
|
602
|
+
function parseUnifiedDiff(diff) {
|
|
603
|
+
const lines = diff.split('\n');
|
|
604
|
+
const changed = [];
|
|
605
|
+
const unmapped = [];
|
|
606
|
+
let pendingHeader = null;
|
|
607
|
+
let sawAnyHeader = false;
|
|
608
|
+
const flushPending = (binary) => {
|
|
609
|
+
if (pendingHeader === null)
|
|
610
|
+
return;
|
|
611
|
+
const aPath = pendingHeader.aPath;
|
|
612
|
+
const bPath = pendingHeader.bPath;
|
|
613
|
+
// mode-only diff(仅 diff --git 头,没有 --- / +++ / rename / binary)→ 跳过
|
|
614
|
+
if (!binary && !pendingHeader.sawContent) {
|
|
615
|
+
pendingHeader = null;
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (binary) {
|
|
619
|
+
const file = bPath ?? aPath;
|
|
620
|
+
if (file !== undefined && file !== '/dev/null') {
|
|
621
|
+
unmapped.push({ file: stripDiffPrefix(file), reason: 'binary' });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// 删除:bPath 为 /dev/null
|
|
626
|
+
if (bPath === '/dev/null' && aPath !== undefined && aPath !== '/dev/null') {
|
|
627
|
+
unmapped.push({ file: stripDiffPrefix(aPath), reason: 'deleted-file' });
|
|
628
|
+
}
|
|
629
|
+
else if (aPath === '/dev/null' && bPath !== undefined && bPath !== '/dev/null') {
|
|
630
|
+
// 新增文件
|
|
631
|
+
unmapped.push({ file: stripDiffPrefix(bPath), reason: 'new-file-not-in-graph-yet' });
|
|
632
|
+
}
|
|
633
|
+
else if (bPath !== undefined && bPath !== '/dev/null') {
|
|
634
|
+
const cleanB = stripDiffPrefix(bPath);
|
|
635
|
+
const cleanA = aPath !== undefined ? stripDiffPrefix(aPath) : cleanB;
|
|
636
|
+
const isRename = cleanA !== cleanB;
|
|
637
|
+
changed.push({ file: cleanB, changeKind: isRename ? 'rename' : 'modified' });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
pendingHeader = null;
|
|
641
|
+
};
|
|
642
|
+
for (const rawLine of lines) {
|
|
643
|
+
const line = rawLine.replace(/\r$/, '');
|
|
644
|
+
if (line.startsWith('diff --git ')) {
|
|
645
|
+
// 上一组结束
|
|
646
|
+
flushPending(false);
|
|
647
|
+
sawAnyHeader = true;
|
|
648
|
+
// 解析 a/<path> b/<path>,可能含引号 + 路径含空格
|
|
649
|
+
// 优先匹配 quoted: diff --git "a/foo bar" "b/foo bar"
|
|
650
|
+
const quotedM = line.match(/^diff --git "(a\/.+?)" "(b\/.+?)"$/);
|
|
651
|
+
const unquotedM = line.match(/^diff --git (a\/[^\s]+) (b\/[^\s]+)$/);
|
|
652
|
+
if (quotedM) {
|
|
653
|
+
pendingHeader = { aPath: quotedM[1], bPath: quotedM[2], sawContent: false };
|
|
654
|
+
}
|
|
655
|
+
else if (unquotedM) {
|
|
656
|
+
pendingHeader = { aPath: unquotedM[1], bPath: unquotedM[2], sawContent: false };
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
// 兜底(rename 等场景 diff --git 头可能没 a/ b/ 前缀)
|
|
660
|
+
pendingHeader = { sawContent: false };
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
else if (line.startsWith('--- ')) {
|
|
664
|
+
if (pendingHeader === null)
|
|
665
|
+
pendingHeader = { sawContent: false };
|
|
666
|
+
pendingHeader.aPath = line.slice(4).trim().replace(/^"|"$/g, '');
|
|
667
|
+
pendingHeader.sawContent = true;
|
|
668
|
+
}
|
|
669
|
+
else if (line.startsWith('+++ ')) {
|
|
670
|
+
if (pendingHeader === null)
|
|
671
|
+
pendingHeader = { sawContent: false };
|
|
672
|
+
pendingHeader.bPath = line.slice(4).trim().replace(/^"|"$/g, '');
|
|
673
|
+
pendingHeader.sawContent = true;
|
|
674
|
+
}
|
|
675
|
+
else if (line.startsWith('Binary files') || line.startsWith('GIT binary patch')) {
|
|
676
|
+
flushPending(true);
|
|
677
|
+
}
|
|
678
|
+
else if (line.startsWith('rename to ')) {
|
|
679
|
+
if (pendingHeader === null)
|
|
680
|
+
pendingHeader = { sawContent: false };
|
|
681
|
+
pendingHeader.bPath = 'b/' + line.slice('rename to '.length).trim().replace(/^"|"$/g, '');
|
|
682
|
+
pendingHeader.sawContent = true;
|
|
683
|
+
}
|
|
684
|
+
else if (line.startsWith('rename from ')) {
|
|
685
|
+
if (pendingHeader === null)
|
|
686
|
+
pendingHeader = { sawContent: false };
|
|
687
|
+
pendingHeader.aPath = 'a/' + line.slice('rename from '.length).trim().replace(/^"|"$/g, '');
|
|
688
|
+
pendingHeader.sawContent = true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
flushPending(false);
|
|
692
|
+
if (!sawAnyHeader && diff.trim().length > 0) {
|
|
693
|
+
return {
|
|
694
|
+
changed: [],
|
|
695
|
+
unmapped: [],
|
|
696
|
+
error: 'unified diff 格式不合法:未找到 `diff --git` 头',
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
return { changed, unmapped };
|
|
700
|
+
}
|
|
701
|
+
function stripDiffPrefix(p) {
|
|
702
|
+
if (p.startsWith('a/'))
|
|
703
|
+
return p.slice(2);
|
|
704
|
+
if (p.startsWith('b/'))
|
|
705
|
+
return p.slice(2);
|
|
706
|
+
return p;
|
|
707
|
+
}
|
|
708
|
+
// ─── git diff baseRef 路径 ──────────────────────────────────
|
|
709
|
+
function runGitDiffNameStatus(baseRef, projectRoot) {
|
|
710
|
+
if (!BASEREF_WHITELIST.test(baseRef) || baseRef.startsWith('-')) {
|
|
711
|
+
return {
|
|
712
|
+
ok: false,
|
|
713
|
+
code: 'invalid-input',
|
|
714
|
+
message: `baseRef 含非法字符或以 - 开头: ${baseRef}`,
|
|
715
|
+
context: { reason: 'baseref-format' },
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
// 1) rev-parse 验证
|
|
719
|
+
const rev = spawnSync('git', ['rev-parse', '--verify', `${baseRef}^{commit}`], {
|
|
720
|
+
cwd: projectRoot,
|
|
721
|
+
shell: false,
|
|
722
|
+
timeout: REVPARSE_TIMEOUT_MS,
|
|
723
|
+
encoding: 'utf-8',
|
|
724
|
+
});
|
|
725
|
+
if (rev.error !== undefined && rev.error.code === 'ETIMEDOUT') {
|
|
726
|
+
return { ok: false, code: 'git-timeout', message: `rev-parse 超时 (${REVPARSE_TIMEOUT_MS} ms)` };
|
|
727
|
+
}
|
|
728
|
+
if (rev.status !== 0) {
|
|
729
|
+
const stderr = (rev.stderr ?? '').toString().slice(0, 200);
|
|
730
|
+
return {
|
|
731
|
+
ok: false,
|
|
732
|
+
code: 'git-spawn-failed',
|
|
733
|
+
message: `rev-parse 失败: ${stderr || 'unknown'}`,
|
|
734
|
+
context: { reason: 'baseref-invalid', stderr },
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
const sha = rev.stdout.trim();
|
|
738
|
+
if (sha.length === 0) {
|
|
739
|
+
return { ok: false, code: 'git-spawn-failed', message: 'rev-parse 返回空 sha' };
|
|
740
|
+
}
|
|
741
|
+
// 2) git diff --name-status
|
|
742
|
+
const diff = spawnSync('git', ['diff', '--name-status', `${sha}...HEAD`], {
|
|
743
|
+
cwd: projectRoot,
|
|
744
|
+
shell: false,
|
|
745
|
+
timeout: GIT_TIMEOUT_MS,
|
|
746
|
+
encoding: 'utf-8',
|
|
747
|
+
});
|
|
748
|
+
if (diff.error !== undefined && diff.error.code === 'ETIMEDOUT') {
|
|
749
|
+
return { ok: false, code: 'git-timeout', message: `git diff 超时 (${GIT_TIMEOUT_MS} ms)` };
|
|
750
|
+
}
|
|
751
|
+
if (diff.status !== 0) {
|
|
752
|
+
const stderr = (diff.stderr ?? '').toString().slice(0, 200);
|
|
753
|
+
return { ok: false, code: 'git-spawn-failed', message: `git diff 失败: ${stderr}` };
|
|
754
|
+
}
|
|
755
|
+
const changed = [];
|
|
756
|
+
const unmapped = [];
|
|
757
|
+
const out = diff.stdout.toString();
|
|
758
|
+
for (const rawLine of out.split('\n')) {
|
|
759
|
+
const line = rawLine.trim();
|
|
760
|
+
if (line.length === 0)
|
|
761
|
+
continue;
|
|
762
|
+
const parts = line.split('\t');
|
|
763
|
+
const status = parts[0] ?? '';
|
|
764
|
+
if (status.startsWith('R')) {
|
|
765
|
+
// R<score>\told\tnew
|
|
766
|
+
const newPath = parts[2];
|
|
767
|
+
if (typeof newPath === 'string' && newPath.length > 0) {
|
|
768
|
+
changed.push({ file: newPath, changeKind: 'rename' });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else if (status === 'M') {
|
|
772
|
+
const file = parts[1];
|
|
773
|
+
if (typeof file === 'string')
|
|
774
|
+
changed.push({ file, changeKind: 'modified' });
|
|
775
|
+
}
|
|
776
|
+
else if (status === 'A') {
|
|
777
|
+
const file = parts[1];
|
|
778
|
+
if (typeof file === 'string')
|
|
779
|
+
unmapped.push({ file, reason: 'new-file-not-in-graph-yet' });
|
|
780
|
+
}
|
|
781
|
+
else if (status === 'D') {
|
|
782
|
+
const file = parts[1];
|
|
783
|
+
if (typeof file === 'string')
|
|
784
|
+
unmapped.push({ file, reason: 'deleted-file' });
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
// 其他状态(C / T / U 等)按 modified 处理
|
|
788
|
+
const file = parts[parts.length - 1];
|
|
789
|
+
if (typeof file === 'string')
|
|
790
|
+
changed.push({ file, changeKind: 'modified' });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return { ok: true, changed, unmapped };
|
|
794
|
+
}
|
|
795
|
+
// ─── file → symbols 索引 ───────────────────────────────────
|
|
796
|
+
function buildFileSymbolIndex(graphData) {
|
|
797
|
+
const index = new Map();
|
|
798
|
+
for (const node of graphData.nodes) {
|
|
799
|
+
const file = moduleFileFromId(node.id);
|
|
800
|
+
const list = index.get(file) ?? [];
|
|
801
|
+
list.push(node.id);
|
|
802
|
+
index.set(file, list);
|
|
803
|
+
}
|
|
804
|
+
return index;
|
|
805
|
+
}
|
|
806
|
+
// ============================================================
|
|
807
|
+
// 注册入口
|
|
808
|
+
// ============================================================
|
|
809
|
+
/**
|
|
810
|
+
* 把 3 个 Agent-Context tool 注册到 MCP server。
|
|
811
|
+
* 在 server.ts 里 registerGraphTools(server) 之后调用。
|
|
812
|
+
*/
|
|
813
|
+
export function registerAgentContextTools(server) {
|
|
814
|
+
server.tool('impact', `查询 symbol 改动的 blast radius — 反向/正向 BFS 遍历调用链,返回受影响 symbols + risk summary。
|
|
815
|
+
|
|
816
|
+
Use this tool when:
|
|
817
|
+
- 改动前评估 caller 影响面
|
|
818
|
+
- 重构前看 transitive 影响(depth=3-5)
|
|
819
|
+
- 决定 PR review 范围
|
|
820
|
+
|
|
821
|
+
Example:
|
|
822
|
+
- Input: { target: "engine.py::Value.add", depth: 2 }
|
|
823
|
+
- Output: { affected, summary, topImpacted: [{ id, score }], nextStepHint }
|
|
824
|
+
|
|
825
|
+
Typical chained usage:
|
|
826
|
+
- 修代码前: detect_changes → impact → context`, ImpactInputSchema, async (args) => handleImpact(args));
|
|
827
|
+
server.tool('context', `查询 symbol 360° 上下文 — definition + callers + callees + imports + topRelevantCallers。
|
|
828
|
+
|
|
829
|
+
Use this tool when:
|
|
830
|
+
- 第一次接触某 symbol,理解位置/调用方/依赖
|
|
831
|
+
- 修改前查 caller 风格保持一致
|
|
832
|
+
- 调试时定位上游引用源头
|
|
833
|
+
|
|
834
|
+
Example:
|
|
835
|
+
- Input: { symbolId: "engine.py::Value" }
|
|
836
|
+
- Output: { definition, callers, callees, imports, topRelevantCallers, nextStepHint }
|
|
837
|
+
|
|
838
|
+
Typical chained usage:
|
|
839
|
+
- impact → context(查 top 受影响节点的上下文)`, ContextInputSchema, async (args) => handleContext(args));
|
|
840
|
+
server.tool('detect_changes', `从 git diff(unified diff 或 baseRef)派生 changedSymbols + BFS 影响链 + risk 总结。
|
|
841
|
+
|
|
842
|
+
Use this tool when:
|
|
843
|
+
- 收到 patch/PR diff 找实际改动 symbol
|
|
844
|
+
- 提交前自检本次改动的上游影响
|
|
845
|
+
- review 大型 PR 按 risk tier 分级
|
|
846
|
+
|
|
847
|
+
Example:
|
|
848
|
+
- Input: { diff: "diff --git a/engine.py..." } 或 { baseRef: "HEAD~3" }
|
|
849
|
+
- Output: { changedSymbols, affectedSymbols, riskSummary, riskTier, topImpacted, nextStepHint }
|
|
850
|
+
|
|
851
|
+
Typical chained usage:
|
|
852
|
+
- detect_changes → impact → context`, DetectChangesInputSchema, async (args) => handleDetectChanges(args));
|
|
853
|
+
}
|
|
854
|
+
//# sourceMappingURL=agent-context-tools.js.map
|