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,1671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApiSurfaceGenerator -- API Surface Reference 生成器
|
|
3
|
+
*
|
|
4
|
+
* 优先级固定为:
|
|
5
|
+
* 1. 读取现有 OpenAPI / Swagger 产物
|
|
6
|
+
* 2. 静态解析 FastAPI / tsoa 元数据
|
|
7
|
+
* 3. Express AST fallback
|
|
8
|
+
*
|
|
9
|
+
* 实现 DocumentGenerator<ApiSurfaceInput, ApiSurfaceOutput> 接口。
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { Project, Node } from 'ts-morph';
|
|
14
|
+
import { loadTemplate } from './utils/template-loader.js';
|
|
15
|
+
// ============================================================
|
|
16
|
+
// 常量
|
|
17
|
+
// ============================================================
|
|
18
|
+
const HTTP_METHODS = [
|
|
19
|
+
'get',
|
|
20
|
+
'post',
|
|
21
|
+
'put',
|
|
22
|
+
'patch',
|
|
23
|
+
'delete',
|
|
24
|
+
'options',
|
|
25
|
+
'head',
|
|
26
|
+
'trace',
|
|
27
|
+
];
|
|
28
|
+
const HTTP_METHOD_SET = new Set([...HTTP_METHODS, 'all']);
|
|
29
|
+
const TSOA_HTTP_DECORATORS = new Map([
|
|
30
|
+
['Get', 'GET'],
|
|
31
|
+
['Post', 'POST'],
|
|
32
|
+
['Put', 'PUT'],
|
|
33
|
+
['Patch', 'PATCH'],
|
|
34
|
+
['Delete', 'DELETE'],
|
|
35
|
+
['Options', 'OPTIONS'],
|
|
36
|
+
['Head', 'HEAD'],
|
|
37
|
+
]);
|
|
38
|
+
const DEFAULT_IGNORE_DIRS = new Set([
|
|
39
|
+
'.git',
|
|
40
|
+
'node_modules',
|
|
41
|
+
'dist',
|
|
42
|
+
'build',
|
|
43
|
+
'coverage',
|
|
44
|
+
'.next',
|
|
45
|
+
'.nuxt',
|
|
46
|
+
'__pycache__',
|
|
47
|
+
'.venv',
|
|
48
|
+
'venv',
|
|
49
|
+
]);
|
|
50
|
+
const OPENAPI_FILE_PATTERN = /(?:^|[-_.])(openapi|swagger)(?:[-_.].*)?\.(json|yaml|yml)$/i;
|
|
51
|
+
const AUTH_HINT_PATTERN = /(auth|guard|jwt|session|passport|protected|require|permission|token|acl)/i;
|
|
52
|
+
const GENERIC_TAG_SEGMENTS = new Set(['api']);
|
|
53
|
+
const TS_SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts'];
|
|
54
|
+
const PYTHON_EXTENSIONS = ['.py'];
|
|
55
|
+
// ============================================================
|
|
56
|
+
// 通用辅助函数
|
|
57
|
+
// ============================================================
|
|
58
|
+
function toPosixPath(filePath) {
|
|
59
|
+
return filePath.replace(/\\/g, '/');
|
|
60
|
+
}
|
|
61
|
+
function uniqueStrings(items) {
|
|
62
|
+
return [...new Set([...items]
|
|
63
|
+
.map((item) => item.trim())
|
|
64
|
+
.filter((item) => item.length > 0))];
|
|
65
|
+
}
|
|
66
|
+
function getRelativePath(projectRoot, filePath) {
|
|
67
|
+
return toPosixPath(path.relative(projectRoot, filePath));
|
|
68
|
+
}
|
|
69
|
+
function collectProjectFiles(projectRoot, options) {
|
|
70
|
+
const results = [];
|
|
71
|
+
const allowedExtensions = new Set((options.extensions ?? []).map((ext) => ext.toLowerCase()));
|
|
72
|
+
const fileNamePattern = options.fileNamePattern;
|
|
73
|
+
function walk(dir) {
|
|
74
|
+
let entries;
|
|
75
|
+
try {
|
|
76
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (entry.isSymbolicLink()) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const fullPath = path.join(dir, entry.name);
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
if (DEFAULT_IGNORE_DIRS.has(entry.name)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
walk(fullPath);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!entry.isFile()) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
97
|
+
const matchesExt = allowedExtensions.size === 0 || allowedExtensions.has(ext);
|
|
98
|
+
const matchesName = fileNamePattern ? fileNamePattern.test(entry.name) : true;
|
|
99
|
+
if (matchesExt && matchesName) {
|
|
100
|
+
results.push(fullPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
walk(projectRoot);
|
|
105
|
+
results.sort();
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
function detectProjectName(projectRoot) {
|
|
109
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
110
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
111
|
+
try {
|
|
112
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
113
|
+
if (typeof pkg.name === 'string' && pkg.name.trim().length > 0) {
|
|
114
|
+
return pkg.name.trim();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
|
|
122
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
123
|
+
try {
|
|
124
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
125
|
+
const match = content.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
126
|
+
if (match?.[1]) {
|
|
127
|
+
return match[1];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// ignore
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return path.basename(projectRoot);
|
|
135
|
+
}
|
|
136
|
+
function joinUrlPaths(...segments) {
|
|
137
|
+
const parts = [];
|
|
138
|
+
for (const segment of segments) {
|
|
139
|
+
const trimmed = segment.trim();
|
|
140
|
+
if (!trimmed || trimmed === '/') {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const cleaned = trimmed.replace(/^\/+|\/+$/g, '');
|
|
144
|
+
if (cleaned.length > 0) {
|
|
145
|
+
parts.push(cleaned);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (parts.length === 0) {
|
|
149
|
+
return '/';
|
|
150
|
+
}
|
|
151
|
+
return `/${parts.join('/')}`.replace(/\/{2,}/g, '/');
|
|
152
|
+
}
|
|
153
|
+
function splitTopLevel(text, separator = ',') {
|
|
154
|
+
const result = [];
|
|
155
|
+
let current = '';
|
|
156
|
+
let parenDepth = 0;
|
|
157
|
+
let bracketDepth = 0;
|
|
158
|
+
let braceDepth = 0;
|
|
159
|
+
let quote = null;
|
|
160
|
+
let escape = false;
|
|
161
|
+
for (const ch of text) {
|
|
162
|
+
if (quote) {
|
|
163
|
+
current += ch;
|
|
164
|
+
if (escape) {
|
|
165
|
+
escape = false;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (ch === '\\') {
|
|
169
|
+
escape = true;
|
|
170
|
+
}
|
|
171
|
+
else if (ch === quote) {
|
|
172
|
+
quote = null;
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
177
|
+
quote = ch;
|
|
178
|
+
current += ch;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (ch === '(')
|
|
182
|
+
parenDepth++;
|
|
183
|
+
else if (ch === ')')
|
|
184
|
+
parenDepth--;
|
|
185
|
+
else if (ch === '[')
|
|
186
|
+
bracketDepth++;
|
|
187
|
+
else if (ch === ']')
|
|
188
|
+
bracketDepth--;
|
|
189
|
+
else if (ch === '{')
|
|
190
|
+
braceDepth++;
|
|
191
|
+
else if (ch === '}')
|
|
192
|
+
braceDepth--;
|
|
193
|
+
if (ch === separator &&
|
|
194
|
+
parenDepth === 0 &&
|
|
195
|
+
bracketDepth === 0 &&
|
|
196
|
+
braceDepth === 0) {
|
|
197
|
+
const piece = current.trim();
|
|
198
|
+
if (piece.length > 0) {
|
|
199
|
+
result.push(piece);
|
|
200
|
+
}
|
|
201
|
+
current = '';
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
current += ch;
|
|
205
|
+
}
|
|
206
|
+
const tail = current.trim();
|
|
207
|
+
if (tail.length > 0) {
|
|
208
|
+
result.push(tail);
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
function extractBalancedContent(text, openParenIndex) {
|
|
213
|
+
let depth = 0;
|
|
214
|
+
let quote = null;
|
|
215
|
+
let escape = false;
|
|
216
|
+
for (let i = openParenIndex; i < text.length; i++) {
|
|
217
|
+
const ch = text[i];
|
|
218
|
+
if (quote) {
|
|
219
|
+
if (escape) {
|
|
220
|
+
escape = false;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (ch === '\\') {
|
|
224
|
+
escape = true;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (ch === quote) {
|
|
228
|
+
quote = null;
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
233
|
+
quote = ch;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (ch === '(') {
|
|
237
|
+
depth++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (ch === ')') {
|
|
241
|
+
depth--;
|
|
242
|
+
if (depth === 0) {
|
|
243
|
+
return {
|
|
244
|
+
content: text.slice(openParenIndex + 1, i),
|
|
245
|
+
endIndex: i,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function stripWrappingQuotes(text) {
|
|
253
|
+
const trimmed = text.trim();
|
|
254
|
+
const match = trimmed.match(/^(["'`])([\s\S]*)\1$/);
|
|
255
|
+
return match ? match[2] : null;
|
|
256
|
+
}
|
|
257
|
+
function getNamedArgumentValue(argsText, key) {
|
|
258
|
+
for (const part of splitTopLevel(argsText)) {
|
|
259
|
+
const eqIndex = part.indexOf('=');
|
|
260
|
+
if (eqIndex <= 0) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const name = part.slice(0, eqIndex).trim();
|
|
264
|
+
if (name === key) {
|
|
265
|
+
return part.slice(eqIndex + 1).trim();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
function getPositionalArguments(argsText) {
|
|
271
|
+
return splitTopLevel(argsText).filter((part) => !/^[a-zA-Z_]\w*\s*=/.test(part));
|
|
272
|
+
}
|
|
273
|
+
function extractStringArray(text) {
|
|
274
|
+
if (!text) {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
const trimmed = text.trim();
|
|
278
|
+
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
|
|
279
|
+
const single = stripWrappingQuotes(trimmed);
|
|
280
|
+
return single ? [single] : [];
|
|
281
|
+
}
|
|
282
|
+
const inner = trimmed.slice(1, -1);
|
|
283
|
+
return splitTopLevel(inner)
|
|
284
|
+
.map((part) => stripWrappingQuotes(part))
|
|
285
|
+
.filter((item) => item !== null);
|
|
286
|
+
}
|
|
287
|
+
function extractDependencyNames(text) {
|
|
288
|
+
const names = [];
|
|
289
|
+
const dependsPattern = /\b(?:Depends|Security)\(\s*([A-Za-z_]\w*)/g;
|
|
290
|
+
let match;
|
|
291
|
+
while ((match = dependsPattern.exec(text)) !== null) {
|
|
292
|
+
names.push(match[1]);
|
|
293
|
+
}
|
|
294
|
+
return uniqueStrings(names);
|
|
295
|
+
}
|
|
296
|
+
function unwrapPromiseType(typeText) {
|
|
297
|
+
let current = typeText.trim();
|
|
298
|
+
while (/^Promise<.*>$/.test(current)) {
|
|
299
|
+
current = current.replace(/^Promise<(.*)>$/, '$1').trim();
|
|
300
|
+
}
|
|
301
|
+
return current || 'unknown';
|
|
302
|
+
}
|
|
303
|
+
function normalizeTypeText(typeText) {
|
|
304
|
+
if (!typeText || typeText.trim().length === 0) {
|
|
305
|
+
return 'unknown';
|
|
306
|
+
}
|
|
307
|
+
return unwrapPromiseType(typeText.replace(/\s+/g, ' ').trim());
|
|
308
|
+
}
|
|
309
|
+
function methodSortKey(method) {
|
|
310
|
+
const upper = method.toUpperCase();
|
|
311
|
+
const order = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD', 'TRACE', 'ALL'];
|
|
312
|
+
const idx = order.indexOf(upper);
|
|
313
|
+
return idx >= 0 ? idx : order.length;
|
|
314
|
+
}
|
|
315
|
+
function dedupeParameters(parameters) {
|
|
316
|
+
const deduped = new Map();
|
|
317
|
+
for (const parameter of parameters) {
|
|
318
|
+
deduped.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
319
|
+
}
|
|
320
|
+
return [...deduped.values()].sort((a, b) => `${a.in}:${a.name}`.localeCompare(`${b.in}:${b.name}`));
|
|
321
|
+
}
|
|
322
|
+
function dedupeResponses(responses) {
|
|
323
|
+
const deduped = new Map();
|
|
324
|
+
for (const response of responses) {
|
|
325
|
+
deduped.set(`${response.statusCode}:${response.type}`, response);
|
|
326
|
+
}
|
|
327
|
+
return [...deduped.values()].sort((a, b) => a.statusCode.localeCompare(b.statusCode));
|
|
328
|
+
}
|
|
329
|
+
function renderResponseType(responses) {
|
|
330
|
+
if (responses.length === 0) {
|
|
331
|
+
return 'unknown';
|
|
332
|
+
}
|
|
333
|
+
return responses
|
|
334
|
+
.map((response) => `${response.statusCode}: ${response.type}`)
|
|
335
|
+
.join(' | ');
|
|
336
|
+
}
|
|
337
|
+
function extractPathParameters(pathValue) {
|
|
338
|
+
const parameters = [];
|
|
339
|
+
const openApiPattern = /\{([^}]+)\}/g;
|
|
340
|
+
const expressPattern = /:([A-Za-z_]\w*)/g;
|
|
341
|
+
let match;
|
|
342
|
+
while ((match = openApiPattern.exec(pathValue)) !== null) {
|
|
343
|
+
parameters.push({
|
|
344
|
+
name: match[1],
|
|
345
|
+
in: 'path',
|
|
346
|
+
type: 'string',
|
|
347
|
+
required: true,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
while ((match = expressPattern.exec(pathValue)) !== null) {
|
|
351
|
+
parameters.push({
|
|
352
|
+
name: match[1],
|
|
353
|
+
in: 'path',
|
|
354
|
+
type: 'string',
|
|
355
|
+
required: true,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return dedupeParameters(parameters);
|
|
359
|
+
}
|
|
360
|
+
function isMeaningfulTagSegment(segment) {
|
|
361
|
+
return (segment.length > 0 &&
|
|
362
|
+
!segment.startsWith(':') &&
|
|
363
|
+
!segment.startsWith('{') &&
|
|
364
|
+
!GENERIC_TAG_SEGMENTS.has(segment.toLowerCase()) &&
|
|
365
|
+
!/^v\d+$/i.test(segment));
|
|
366
|
+
}
|
|
367
|
+
function deriveTags(pathValue, sourceFile) {
|
|
368
|
+
const pathSegments = pathValue.split('/').filter(Boolean);
|
|
369
|
+
const meaningful = pathSegments.find(isMeaningfulTagSegment);
|
|
370
|
+
if (meaningful) {
|
|
371
|
+
return [meaningful];
|
|
372
|
+
}
|
|
373
|
+
if (sourceFile) {
|
|
374
|
+
const fileName = path.basename(sourceFile, path.extname(sourceFile));
|
|
375
|
+
if (fileName.length > 0) {
|
|
376
|
+
return [fileName];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return ['default'];
|
|
380
|
+
}
|
|
381
|
+
function isAuthHint(text) {
|
|
382
|
+
return AUTH_HINT_PATTERN.test(text);
|
|
383
|
+
}
|
|
384
|
+
function collectAuthHints(names) {
|
|
385
|
+
return uniqueStrings(names.filter(isAuthHint)).sort();
|
|
386
|
+
}
|
|
387
|
+
function finalizeEndpoint(endpoint) {
|
|
388
|
+
const parameters = dedupeParameters([
|
|
389
|
+
...extractPathParameters(endpoint.path),
|
|
390
|
+
...endpoint.parameters,
|
|
391
|
+
]);
|
|
392
|
+
const responses = dedupeResponses(endpoint.responses);
|
|
393
|
+
const tags = uniqueStrings(endpoint.tags).sort();
|
|
394
|
+
const auth = uniqueStrings(endpoint.auth).sort();
|
|
395
|
+
return {
|
|
396
|
+
...endpoint,
|
|
397
|
+
method: endpoint.method.toUpperCase(),
|
|
398
|
+
path: endpoint.path,
|
|
399
|
+
parameters,
|
|
400
|
+
responses,
|
|
401
|
+
responseType: renderResponseType(responses),
|
|
402
|
+
tags: tags.length > 0 ? tags : deriveTags(endpoint.path, endpoint.sourceFile),
|
|
403
|
+
auth,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function dedupeEndpoints(endpoints) {
|
|
407
|
+
const merged = new Map();
|
|
408
|
+
for (const rawEndpoint of endpoints) {
|
|
409
|
+
const endpoint = finalizeEndpoint(rawEndpoint);
|
|
410
|
+
const key = `${endpoint.method}:${endpoint.path}`;
|
|
411
|
+
const existing = merged.get(key);
|
|
412
|
+
if (!existing) {
|
|
413
|
+
merged.set(key, endpoint);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
merged.set(key, finalizeEndpoint({
|
|
417
|
+
...existing,
|
|
418
|
+
parameters: [...existing.parameters, ...endpoint.parameters],
|
|
419
|
+
responses: [...existing.responses, ...endpoint.responses],
|
|
420
|
+
auth: [...existing.auth, ...endpoint.auth],
|
|
421
|
+
tags: [...existing.tags, ...endpoint.tags],
|
|
422
|
+
sourceFile: existing.sourceFile ?? endpoint.sourceFile,
|
|
423
|
+
summary: existing.summary ?? endpoint.summary,
|
|
424
|
+
operationId: existing.operationId ?? endpoint.operationId,
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
return [...merged.values()].sort((a, b) => {
|
|
428
|
+
const pathCompare = a.path.localeCompare(b.path);
|
|
429
|
+
if (pathCompare !== 0) {
|
|
430
|
+
return pathCompare;
|
|
431
|
+
}
|
|
432
|
+
return methodSortKey(a.method) - methodSortKey(b.method);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function tryReadFile(filePath) {
|
|
436
|
+
try {
|
|
437
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function resolveModulePath(fromFile, specifier, extensions) {
|
|
444
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const basePath = specifier.startsWith('/')
|
|
448
|
+
? specifier
|
|
449
|
+
: path.resolve(path.dirname(fromFile), specifier);
|
|
450
|
+
const candidates = new Set([
|
|
451
|
+
basePath,
|
|
452
|
+
...extensions.map((ext) => `${basePath}${ext}`),
|
|
453
|
+
...extensions.map((ext) => path.join(basePath, `index${ext}`)),
|
|
454
|
+
]);
|
|
455
|
+
for (const candidate of candidates) {
|
|
456
|
+
try {
|
|
457
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
458
|
+
return path.resolve(candidate);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// ignore
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
function resolvePythonModulePath(fromFile, specifier, projectRoot) {
|
|
468
|
+
let candidateBase;
|
|
469
|
+
if (specifier.startsWith('.')) {
|
|
470
|
+
let level = 0;
|
|
471
|
+
while (specifier[level] === '.') {
|
|
472
|
+
level++;
|
|
473
|
+
}
|
|
474
|
+
let baseDir = path.dirname(fromFile);
|
|
475
|
+
for (let i = 1; i < level; i++) {
|
|
476
|
+
baseDir = path.dirname(baseDir);
|
|
477
|
+
}
|
|
478
|
+
const remainder = specifier.slice(level);
|
|
479
|
+
candidateBase = remainder.length > 0
|
|
480
|
+
? path.join(baseDir, ...remainder.split('.'))
|
|
481
|
+
: baseDir;
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
candidateBase = path.join(projectRoot, ...specifier.split('.'));
|
|
485
|
+
}
|
|
486
|
+
const candidates = [
|
|
487
|
+
`${candidateBase}.py`,
|
|
488
|
+
path.join(candidateBase, '__init__.py'),
|
|
489
|
+
];
|
|
490
|
+
for (const candidate of candidates) {
|
|
491
|
+
try {
|
|
492
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
493
|
+
return path.resolve(candidate);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// ignore
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
// ============================================================
|
|
503
|
+
// Schema ingest
|
|
504
|
+
// ============================================================
|
|
505
|
+
function isPlainObject(value) {
|
|
506
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
507
|
+
}
|
|
508
|
+
function decodeJsonPointerSegment(segment) {
|
|
509
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
510
|
+
}
|
|
511
|
+
function resolveRef(doc, ref) {
|
|
512
|
+
if (typeof ref !== 'string' || !ref.startsWith('#/')) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
const segments = ref.slice(2).split('/').map(decodeJsonPointerSegment);
|
|
516
|
+
let current = doc;
|
|
517
|
+
for (const segment of segments) {
|
|
518
|
+
if (!isPlainObject(current) && !Array.isArray(current)) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
current = current[segment];
|
|
522
|
+
}
|
|
523
|
+
return current ?? null;
|
|
524
|
+
}
|
|
525
|
+
function dereference(doc, value) {
|
|
526
|
+
if (!isPlainObject(value) || typeof value.$ref !== 'string') {
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
const resolved = resolveRef(doc, value.$ref);
|
|
530
|
+
if (!resolved || !isPlainObject(resolved)) {
|
|
531
|
+
return value;
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
...resolved,
|
|
535
|
+
...Object.fromEntries(Object.entries(value).filter(([key]) => key !== '$ref')),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function formatSchemaType(doc, schema) {
|
|
539
|
+
if (!schema) {
|
|
540
|
+
return 'unknown';
|
|
541
|
+
}
|
|
542
|
+
if (isPlainObject(schema) && typeof schema.$ref === 'string') {
|
|
543
|
+
const refSegments = schema.$ref.split('/');
|
|
544
|
+
return refSegments[refSegments.length - 1] ?? 'unknown';
|
|
545
|
+
}
|
|
546
|
+
const resolvedSchema = dereference(doc, schema);
|
|
547
|
+
if (!isPlainObject(resolvedSchema)) {
|
|
548
|
+
return 'unknown';
|
|
549
|
+
}
|
|
550
|
+
if (Array.isArray(resolvedSchema.oneOf) && resolvedSchema.oneOf.length > 0) {
|
|
551
|
+
return uniqueStrings(resolvedSchema.oneOf.map((item) => formatSchemaType(doc, item))).join(' | ');
|
|
552
|
+
}
|
|
553
|
+
if (Array.isArray(resolvedSchema.anyOf) && resolvedSchema.anyOf.length > 0) {
|
|
554
|
+
return uniqueStrings(resolvedSchema.anyOf.map((item) => formatSchemaType(doc, item))).join(' | ');
|
|
555
|
+
}
|
|
556
|
+
if (Array.isArray(resolvedSchema.allOf) && resolvedSchema.allOf.length > 0) {
|
|
557
|
+
return uniqueStrings(resolvedSchema.allOf.map((item) => formatSchemaType(doc, item))).join(' & ');
|
|
558
|
+
}
|
|
559
|
+
if (Array.isArray(resolvedSchema.enum) && resolvedSchema.enum.length > 0) {
|
|
560
|
+
return `enum(${resolvedSchema.enum.map(String).join(', ')})`;
|
|
561
|
+
}
|
|
562
|
+
if (resolvedSchema.type === 'array') {
|
|
563
|
+
return `${formatSchemaType(doc, resolvedSchema.items)}[]`;
|
|
564
|
+
}
|
|
565
|
+
if (resolvedSchema.type === 'object' && resolvedSchema.additionalProperties) {
|
|
566
|
+
return `Record<string, ${formatSchemaType(doc, resolvedSchema.additionalProperties)}>`;
|
|
567
|
+
}
|
|
568
|
+
if (typeof resolvedSchema.type === 'string') {
|
|
569
|
+
return resolvedSchema.type;
|
|
570
|
+
}
|
|
571
|
+
if (isPlainObject(resolvedSchema.properties)) {
|
|
572
|
+
return 'object';
|
|
573
|
+
}
|
|
574
|
+
return 'unknown';
|
|
575
|
+
}
|
|
576
|
+
function normalizeSchemaParameters(doc, rawParameters) {
|
|
577
|
+
const parameters = [];
|
|
578
|
+
for (const rawParameter of rawParameters) {
|
|
579
|
+
const parameter = dereference(doc, rawParameter);
|
|
580
|
+
if (!isPlainObject(parameter)) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const name = typeof parameter.name === 'string' ? parameter.name : 'body';
|
|
584
|
+
const location = typeof parameter.in === 'string'
|
|
585
|
+
? parameter.in
|
|
586
|
+
: 'unknown';
|
|
587
|
+
let type = 'unknown';
|
|
588
|
+
if (parameter.schema) {
|
|
589
|
+
type = formatSchemaType(doc, parameter.schema);
|
|
590
|
+
}
|
|
591
|
+
else if (parameter.content && isPlainObject(parameter.content)) {
|
|
592
|
+
const firstContent = Object.values(parameter.content)[0];
|
|
593
|
+
type = formatSchemaType(doc, isPlainObject(firstContent) ? firstContent.schema : undefined);
|
|
594
|
+
}
|
|
595
|
+
parameters.push({
|
|
596
|
+
name,
|
|
597
|
+
in: location,
|
|
598
|
+
type,
|
|
599
|
+
required: location === 'path' ? true : Boolean(parameter.required),
|
|
600
|
+
description: typeof parameter.description === 'string' ? parameter.description : undefined,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return parameters;
|
|
604
|
+
}
|
|
605
|
+
function extractRequestBodyParameter(doc, rawRequestBody) {
|
|
606
|
+
const requestBody = dereference(doc, rawRequestBody);
|
|
607
|
+
if (!isPlainObject(requestBody) || !isPlainObject(requestBody.content)) {
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
const preferredContent = requestBody.content['application/json'] ??
|
|
611
|
+
Object.values(requestBody.content)[0];
|
|
612
|
+
const schema = isPlainObject(preferredContent) ? preferredContent.schema : undefined;
|
|
613
|
+
return [{
|
|
614
|
+
name: 'body',
|
|
615
|
+
in: 'body',
|
|
616
|
+
type: formatSchemaType(doc, schema),
|
|
617
|
+
required: Boolean(requestBody.required),
|
|
618
|
+
description: typeof requestBody.description === 'string' ? requestBody.description : undefined,
|
|
619
|
+
}];
|
|
620
|
+
}
|
|
621
|
+
function extractSchemaResponses(doc, rawResponses) {
|
|
622
|
+
if (!isPlainObject(rawResponses)) {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
const responses = [];
|
|
626
|
+
for (const [statusCode, rawValue] of Object.entries(rawResponses)) {
|
|
627
|
+
const response = dereference(doc, rawValue);
|
|
628
|
+
if (!isPlainObject(response)) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
let type = 'unknown';
|
|
632
|
+
if (isPlainObject(response.content)) {
|
|
633
|
+
const preferredContent = response.content['application/json'] ??
|
|
634
|
+
Object.values(response.content)[0];
|
|
635
|
+
type = formatSchemaType(doc, isPlainObject(preferredContent) ? preferredContent.schema : undefined);
|
|
636
|
+
}
|
|
637
|
+
else if (response.schema) {
|
|
638
|
+
type = formatSchemaType(doc, response.schema);
|
|
639
|
+
}
|
|
640
|
+
responses.push({
|
|
641
|
+
statusCode,
|
|
642
|
+
type,
|
|
643
|
+
description: typeof response.description === 'string' ? response.description : undefined,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
const preferred = responses
|
|
647
|
+
.filter((response) => /^2\d\d$/.test(response.statusCode))
|
|
648
|
+
.sort((a, b) => a.statusCode.localeCompare(b.statusCode));
|
|
649
|
+
if (preferred.length > 0) {
|
|
650
|
+
return [...preferred, ...responses.filter((response) => !/^2\d\d$/.test(response.statusCode))];
|
|
651
|
+
}
|
|
652
|
+
return responses.sort((a, b) => a.statusCode.localeCompare(b.statusCode));
|
|
653
|
+
}
|
|
654
|
+
function extractSecuritySchemes(rawSecurity) {
|
|
655
|
+
if (!Array.isArray(rawSecurity)) {
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
const schemeNames = [];
|
|
659
|
+
for (const entry of rawSecurity) {
|
|
660
|
+
if (!isPlainObject(entry)) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
schemeNames.push(...Object.keys(entry));
|
|
664
|
+
}
|
|
665
|
+
return uniqueStrings(schemeNames).sort();
|
|
666
|
+
}
|
|
667
|
+
function extractFromSchema(projectRoot) {
|
|
668
|
+
const schemaFiles = collectProjectFiles(projectRoot, {
|
|
669
|
+
extensions: ['.json', '.yaml', '.yml'],
|
|
670
|
+
fileNamePattern: OPENAPI_FILE_PATTERN,
|
|
671
|
+
});
|
|
672
|
+
const endpoints = [];
|
|
673
|
+
const sourceFiles = [];
|
|
674
|
+
for (const filePath of schemaFiles) {
|
|
675
|
+
if (path.extname(filePath).toLowerCase() !== '.json') {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const content = tryReadFile(filePath);
|
|
679
|
+
if (!content) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
let doc;
|
|
683
|
+
try {
|
|
684
|
+
doc = JSON.parse(content);
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!isPlainObject(doc) || !isPlainObject(doc.paths)) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const globalSecurity = extractSecuritySchemes(doc.security);
|
|
693
|
+
sourceFiles.push(getRelativePath(projectRoot, filePath));
|
|
694
|
+
for (const [rawPath, rawPathItem] of Object.entries(doc.paths)) {
|
|
695
|
+
if (!isPlainObject(rawPathItem)) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const pathLevelParameters = normalizeSchemaParameters(doc, Array.isArray(rawPathItem.parameters) ? rawPathItem.parameters : []);
|
|
699
|
+
const pathLevelSecurity = extractSecuritySchemes(rawPathItem.security);
|
|
700
|
+
for (const method of HTTP_METHODS) {
|
|
701
|
+
const operation = rawPathItem[method];
|
|
702
|
+
if (!isPlainObject(operation)) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const operationParameters = normalizeSchemaParameters(doc, Array.isArray(operation.parameters) ? operation.parameters : []);
|
|
706
|
+
const requestBodyParameters = extractRequestBodyParameter(doc, operation.requestBody);
|
|
707
|
+
const responses = extractSchemaResponses(doc, operation.responses);
|
|
708
|
+
const tags = Array.isArray(operation.tags)
|
|
709
|
+
? operation.tags.filter((tag) => typeof tag === 'string')
|
|
710
|
+
: [];
|
|
711
|
+
const auth = uniqueStrings([
|
|
712
|
+
...extractSecuritySchemes(operation.security),
|
|
713
|
+
...pathLevelSecurity,
|
|
714
|
+
...globalSecurity,
|
|
715
|
+
]);
|
|
716
|
+
endpoints.push({
|
|
717
|
+
method: method.toUpperCase(),
|
|
718
|
+
path: rawPath,
|
|
719
|
+
parameters: [...pathLevelParameters, ...operationParameters, ...requestBodyParameters],
|
|
720
|
+
responses,
|
|
721
|
+
responseType: renderResponseType(responses),
|
|
722
|
+
auth,
|
|
723
|
+
tags,
|
|
724
|
+
source: 'schema',
|
|
725
|
+
sourceFile: getRelativePath(projectRoot, filePath),
|
|
726
|
+
summary: typeof operation.summary === 'string' ? operation.summary : undefined,
|
|
727
|
+
operationId: typeof operation.operationId === 'string' ? operation.operationId : undefined,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const deduped = dedupeEndpoints(endpoints);
|
|
733
|
+
if (deduped.length === 0) {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
source: 'schema',
|
|
738
|
+
endpoints: deduped,
|
|
739
|
+
sourceFiles: uniqueStrings(sourceFiles).sort(),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
// ============================================================
|
|
743
|
+
// FastAPI introspection
|
|
744
|
+
// ============================================================
|
|
745
|
+
function parseFastApiRouteParameters(argsText, pathValue) {
|
|
746
|
+
const parameters = [];
|
|
747
|
+
const authHints = [];
|
|
748
|
+
const defMatch = argsText.match(/^\s*([A-Za-z_]\w*)\s*:\s*([^=,]+?)(?:\s*=\s*(.+))?$/);
|
|
749
|
+
if (!defMatch) {
|
|
750
|
+
return { parameters, auth: authHints };
|
|
751
|
+
}
|
|
752
|
+
const name = defMatch[1];
|
|
753
|
+
const annotation = normalizeTypeText(defMatch[2]);
|
|
754
|
+
const defaultExpr = defMatch[3]?.trim();
|
|
755
|
+
if (defaultExpr && /\b(?:Depends|Security)\(/.test(defaultExpr)) {
|
|
756
|
+
authHints.push(...extractDependencyNames(defaultExpr));
|
|
757
|
+
return { parameters, auth: authHints };
|
|
758
|
+
}
|
|
759
|
+
let location = 'query';
|
|
760
|
+
if (defaultExpr && /\bPath\(/.test(defaultExpr))
|
|
761
|
+
location = 'path';
|
|
762
|
+
else if (defaultExpr && /\bQuery\(/.test(defaultExpr))
|
|
763
|
+
location = 'query';
|
|
764
|
+
else if (defaultExpr && /\bBody\(/.test(defaultExpr))
|
|
765
|
+
location = 'body';
|
|
766
|
+
else if (defaultExpr && /\bHeader\(/.test(defaultExpr))
|
|
767
|
+
location = 'header';
|
|
768
|
+
else if (defaultExpr && /\bCookie\(/.test(defaultExpr))
|
|
769
|
+
location = 'cookie';
|
|
770
|
+
else if (defaultExpr && /\bForm\(/.test(defaultExpr))
|
|
771
|
+
location = 'form';
|
|
772
|
+
else if (new RegExp(`\\{${name}\\}`).test(pathValue))
|
|
773
|
+
location = 'path';
|
|
774
|
+
const required = location === 'path'
|
|
775
|
+
? true
|
|
776
|
+
: !defaultExpr || defaultExpr.includes('...');
|
|
777
|
+
parameters.push({
|
|
778
|
+
name,
|
|
779
|
+
in: location,
|
|
780
|
+
type: annotation,
|
|
781
|
+
required,
|
|
782
|
+
});
|
|
783
|
+
return { parameters, auth: authHints };
|
|
784
|
+
}
|
|
785
|
+
function analyzeFastApiFile(projectRoot, filePath, content) {
|
|
786
|
+
const routers = new Map();
|
|
787
|
+
const imports = new Map();
|
|
788
|
+
for (const line of content.split('\n')) {
|
|
789
|
+
const fromImportMatch = line.match(/^\s*from\s+([.\w]+)\s+import\s+(.+)\s*$/);
|
|
790
|
+
if (!fromImportMatch) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const modulePath = fromImportMatch[1];
|
|
794
|
+
const resolvedFile = resolvePythonModulePath(filePath, modulePath, projectRoot);
|
|
795
|
+
if (!resolvedFile) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
for (const entry of splitTopLevel(fromImportMatch[2])) {
|
|
799
|
+
const aliasMatch = entry.match(/^\s*([A-Za-z_]\w*)(?:\s+as\s+([A-Za-z_]\w*))?\s*$/);
|
|
800
|
+
if (!aliasMatch) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const importedName = aliasMatch[1];
|
|
804
|
+
const localName = aliasMatch[2] ?? importedName;
|
|
805
|
+
imports.set(localName, {
|
|
806
|
+
targetFile: resolvedFile,
|
|
807
|
+
exportName: importedName,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const routerDeclPattern = /\b([A-Za-z_]\w*)\s*=\s*(FastAPI|APIRouter)\s*\(/g;
|
|
812
|
+
let routerMatch;
|
|
813
|
+
while ((routerMatch = routerDeclPattern.exec(content)) !== null) {
|
|
814
|
+
const localName = routerMatch[1];
|
|
815
|
+
const kind = routerMatch[2] === 'FastAPI' ? 'app' : 'router';
|
|
816
|
+
const openParenIndex = routerDeclPattern.lastIndex - 1;
|
|
817
|
+
const balanced = extractBalancedContent(content, openParenIndex);
|
|
818
|
+
if (!balanced) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const argsText = balanced.content;
|
|
822
|
+
const prefix = stripWrappingQuotes(getNamedArgumentValue(argsText, 'prefix') ?? '') ?? '';
|
|
823
|
+
const tags = extractStringArray(getNamedArgumentValue(argsText, 'tags'));
|
|
824
|
+
const auth = extractDependencyNames(getNamedArgumentValue(argsText, 'dependencies') ?? '');
|
|
825
|
+
routers.set(localName, {
|
|
826
|
+
id: `${filePath}#${localName}`,
|
|
827
|
+
filePath,
|
|
828
|
+
localName,
|
|
829
|
+
kind,
|
|
830
|
+
prefix,
|
|
831
|
+
tags,
|
|
832
|
+
auth,
|
|
833
|
+
routes: [],
|
|
834
|
+
mounts: [],
|
|
835
|
+
});
|
|
836
|
+
routerDeclPattern.lastIndex = balanced.endIndex + 1;
|
|
837
|
+
}
|
|
838
|
+
const routePattern = /@([A-Za-z_]\w*)\.(get|post|put|patch|delete|options|head|trace)\s*\(/g;
|
|
839
|
+
let routeMatch;
|
|
840
|
+
while ((routeMatch = routePattern.exec(content)) !== null) {
|
|
841
|
+
const routerName = routeMatch[1];
|
|
842
|
+
const router = routers.get(routerName);
|
|
843
|
+
if (!router) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const balanced = extractBalancedContent(content, routePattern.lastIndex - 1);
|
|
847
|
+
if (!balanced) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const argsText = balanced.content;
|
|
851
|
+
const afterDecorator = content.slice(balanced.endIndex + 1);
|
|
852
|
+
const defMatch = afterDecorator.match(/^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)\(([\s\S]*?)\)(?:\s*->\s*([^:\n]+))?:/m);
|
|
853
|
+
if (!defMatch) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const positionalArgs = getPositionalArguments(argsText);
|
|
857
|
+
const rawPath = stripWrappingQuotes(positionalArgs[0] ?? '') ?? '/';
|
|
858
|
+
const routeTags = extractStringArray(getNamedArgumentValue(argsText, 'tags'));
|
|
859
|
+
const routeAuth = extractDependencyNames(argsText);
|
|
860
|
+
const responseModel = normalizeTypeText(getNamedArgumentValue(argsText, 'response_model') ?? defMatch[3]);
|
|
861
|
+
const parameters = [];
|
|
862
|
+
const functionAuth = [];
|
|
863
|
+
for (const rawParam of splitTopLevel(defMatch[2])) {
|
|
864
|
+
const trimmed = rawParam.trim();
|
|
865
|
+
if (!trimmed || trimmed === 'self' || trimmed === 'cls') {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
const parsed = parseFastApiRouteParameters(trimmed, rawPath);
|
|
869
|
+
parameters.push(...parsed.parameters);
|
|
870
|
+
functionAuth.push(...parsed.auth);
|
|
871
|
+
}
|
|
872
|
+
router.routes.push({
|
|
873
|
+
method: routeMatch[2].toUpperCase(),
|
|
874
|
+
path: rawPath,
|
|
875
|
+
parameters,
|
|
876
|
+
responseType: responseModel,
|
|
877
|
+
auth: uniqueStrings([...routeAuth, ...functionAuth]),
|
|
878
|
+
tags: routeTags,
|
|
879
|
+
summary: stripWrappingQuotes(getNamedArgumentValue(argsText, 'summary') ?? '') ?? undefined,
|
|
880
|
+
operationId: defMatch[1],
|
|
881
|
+
sourceFile: getRelativePath(projectRoot, filePath),
|
|
882
|
+
});
|
|
883
|
+
routePattern.lastIndex = balanced.endIndex + 1;
|
|
884
|
+
}
|
|
885
|
+
const includeRouterPattern = /\b([A-Za-z_]\w*)\.include_router\s*\(/g;
|
|
886
|
+
let includeMatch;
|
|
887
|
+
while ((includeMatch = includeRouterPattern.exec(content)) !== null) {
|
|
888
|
+
const parentName = includeMatch[1];
|
|
889
|
+
const parent = routers.get(parentName);
|
|
890
|
+
if (!parent) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
const balanced = extractBalancedContent(content, includeRouterPattern.lastIndex - 1);
|
|
894
|
+
if (!balanced) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const argsText = balanced.content;
|
|
898
|
+
const positionalArgs = getPositionalArguments(argsText);
|
|
899
|
+
const childName = positionalArgs[0]?.trim();
|
|
900
|
+
if (!childName || !/^[A-Za-z_]\w*$/.test(childName)) {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
parent.mounts.push({
|
|
904
|
+
targetLocalName: childName,
|
|
905
|
+
prefix: stripWrappingQuotes(getNamedArgumentValue(argsText, 'prefix') ?? '') ?? '',
|
|
906
|
+
tags: extractStringArray(getNamedArgumentValue(argsText, 'tags')),
|
|
907
|
+
auth: extractDependencyNames(getNamedArgumentValue(argsText, 'dependencies') ?? ''),
|
|
908
|
+
});
|
|
909
|
+
includeRouterPattern.lastIndex = balanced.endIndex + 1;
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
filePath,
|
|
913
|
+
routers,
|
|
914
|
+
imports,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function resolveFastApiRouters(analyses) {
|
|
918
|
+
const resolved = new Map();
|
|
919
|
+
const byFile = new Map(analyses.map((analysis) => [analysis.filePath, analysis]));
|
|
920
|
+
for (const analysis of analyses) {
|
|
921
|
+
for (const router of analysis.routers.values()) {
|
|
922
|
+
const mounts = [];
|
|
923
|
+
for (const mount of router.mounts) {
|
|
924
|
+
let targetId;
|
|
925
|
+
if (analysis.routers.has(mount.targetLocalName)) {
|
|
926
|
+
targetId = analysis.routers.get(mount.targetLocalName).id;
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
const binding = analysis.imports.get(mount.targetLocalName);
|
|
930
|
+
const importedAnalysis = binding ? byFile.get(binding.targetFile) : undefined;
|
|
931
|
+
const importedRouter = importedAnalysis?.routers.get(binding?.exportName ?? '');
|
|
932
|
+
if (importedRouter) {
|
|
933
|
+
targetId = importedRouter.id;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (targetId) {
|
|
937
|
+
mounts.push({
|
|
938
|
+
prefix: mount.prefix,
|
|
939
|
+
tags: mount.tags,
|
|
940
|
+
auth: mount.auth,
|
|
941
|
+
targetId,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
resolved.set(router.id, {
|
|
946
|
+
...router,
|
|
947
|
+
mounts,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return resolved;
|
|
952
|
+
}
|
|
953
|
+
function extractFromFastApi(projectRoot) {
|
|
954
|
+
const files = collectProjectFiles(projectRoot, { extensions: PYTHON_EXTENSIONS });
|
|
955
|
+
const analyses = files
|
|
956
|
+
.map((filePath) => {
|
|
957
|
+
const content = tryReadFile(filePath);
|
|
958
|
+
return content ? analyzeFastApiFile(projectRoot, filePath, content) : null;
|
|
959
|
+
})
|
|
960
|
+
.filter((item) => item !== null);
|
|
961
|
+
const routers = resolveFastApiRouters(analyses);
|
|
962
|
+
if (routers.size === 0) {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
const parentCounts = new Map();
|
|
966
|
+
for (const router of routers.values()) {
|
|
967
|
+
parentCounts.set(router.id, parentCounts.get(router.id) ?? 0);
|
|
968
|
+
for (const mount of router.mounts) {
|
|
969
|
+
parentCounts.set(mount.targetId, (parentCounts.get(mount.targetId) ?? 0) + 1);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const roots = [...routers.values()].filter((router) => router.kind === 'app' || (parentCounts.get(router.id) ?? 0) === 0);
|
|
973
|
+
const endpoints = [];
|
|
974
|
+
const sourceFiles = new Set();
|
|
975
|
+
const visited = new Set();
|
|
976
|
+
function walk(routerId, prefix, inheritedTags, inheritedAuth) {
|
|
977
|
+
const router = routers.get(routerId);
|
|
978
|
+
if (!router) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const combinedPrefix = joinUrlPaths(prefix, router.prefix);
|
|
982
|
+
const routerTags = uniqueStrings([...inheritedTags, ...router.tags]);
|
|
983
|
+
const routerAuth = uniqueStrings([...inheritedAuth, ...router.auth]);
|
|
984
|
+
for (const route of router.routes) {
|
|
985
|
+
const fullPath = joinUrlPaths(combinedPrefix, route.path);
|
|
986
|
+
const auth = collectAuthHints([...routerAuth, ...route.auth]);
|
|
987
|
+
const tags = uniqueStrings([...routerTags, ...route.tags]);
|
|
988
|
+
sourceFiles.add(route.sourceFile);
|
|
989
|
+
endpoints.push({
|
|
990
|
+
method: route.method,
|
|
991
|
+
path: fullPath,
|
|
992
|
+
parameters: route.parameters,
|
|
993
|
+
responses: [{
|
|
994
|
+
statusCode: '200',
|
|
995
|
+
type: normalizeTypeText(route.responseType),
|
|
996
|
+
}],
|
|
997
|
+
responseType: normalizeTypeText(route.responseType),
|
|
998
|
+
auth,
|
|
999
|
+
tags,
|
|
1000
|
+
source: 'introspection',
|
|
1001
|
+
sourceFile: route.sourceFile,
|
|
1002
|
+
summary: route.summary,
|
|
1003
|
+
operationId: route.operationId,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
for (const mount of router.mounts) {
|
|
1007
|
+
const visitKey = `${routerId}->${mount.targetId}:${mount.prefix}`;
|
|
1008
|
+
if (visited.has(visitKey)) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
visited.add(visitKey);
|
|
1012
|
+
walk(mount.targetId, joinUrlPaths(combinedPrefix, mount.prefix), [...routerTags, ...mount.tags], [...routerAuth, ...mount.auth]);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const rootRouters = roots.length > 0 ? roots : [...routers.values()];
|
|
1016
|
+
for (const root of rootRouters) {
|
|
1017
|
+
walk(root.id, '', [], []);
|
|
1018
|
+
}
|
|
1019
|
+
const deduped = dedupeEndpoints(endpoints);
|
|
1020
|
+
if (deduped.length === 0) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
source: 'introspection',
|
|
1025
|
+
endpoints: deduped,
|
|
1026
|
+
sourceFiles: [...sourceFiles].sort(),
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
// ============================================================
|
|
1030
|
+
// tsoa introspection
|
|
1031
|
+
// ============================================================
|
|
1032
|
+
function extractDecoratorStringArg(decorator) {
|
|
1033
|
+
if (!decorator || !Node.isDecorator(decorator)) {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
const firstArg = decorator.getArguments()[0];
|
|
1037
|
+
if (!firstArg) {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
return stripWrappingQuotes(firstArg.getText());
|
|
1041
|
+
}
|
|
1042
|
+
function extractDecoratorStringArgs(decorator) {
|
|
1043
|
+
if (!decorator || !Node.isDecorator(decorator)) {
|
|
1044
|
+
return [];
|
|
1045
|
+
}
|
|
1046
|
+
return decorator.getArguments()
|
|
1047
|
+
.map((arg) => stripWrappingQuotes(arg.getText()))
|
|
1048
|
+
.filter((item) => item !== null);
|
|
1049
|
+
}
|
|
1050
|
+
function extractTsoaSecurity(decorators) {
|
|
1051
|
+
const authHints = [];
|
|
1052
|
+
for (const decorator of decorators) {
|
|
1053
|
+
if (!Node.isDecorator(decorator) || decorator.getName() !== 'Security') {
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
const arg = decorator.getArguments()[0];
|
|
1057
|
+
const value = arg ? stripWrappingQuotes(arg.getText()) : null;
|
|
1058
|
+
if (value) {
|
|
1059
|
+
authHints.push(value);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return uniqueStrings(authHints).sort();
|
|
1063
|
+
}
|
|
1064
|
+
function parseTsoaParameter(parameter) {
|
|
1065
|
+
if (!Node.isParameterDeclaration(parameter)) {
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
const decorators = parameter.getDecorators();
|
|
1069
|
+
if (decorators.length === 0) {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
let location = null;
|
|
1073
|
+
let name = parameter.getName();
|
|
1074
|
+
for (const decorator of decorators) {
|
|
1075
|
+
const decoratorName = decorator.getName();
|
|
1076
|
+
if (decoratorName === 'Path')
|
|
1077
|
+
location = 'path';
|
|
1078
|
+
else if (decoratorName === 'Query')
|
|
1079
|
+
location = 'query';
|
|
1080
|
+
else if (decoratorName === 'Body' || decoratorName === 'BodyProp')
|
|
1081
|
+
location = 'body';
|
|
1082
|
+
else if (decoratorName === 'Header')
|
|
1083
|
+
location = 'header';
|
|
1084
|
+
else if (decoratorName === 'Cookie')
|
|
1085
|
+
location = 'cookie';
|
|
1086
|
+
else if (decoratorName === 'FormField')
|
|
1087
|
+
location = 'form';
|
|
1088
|
+
if (location) {
|
|
1089
|
+
const alias = extractDecoratorStringArg(decorator);
|
|
1090
|
+
if (alias) {
|
|
1091
|
+
name = alias;
|
|
1092
|
+
}
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (!location) {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
name,
|
|
1101
|
+
in: location,
|
|
1102
|
+
type: normalizeTypeText(parameter.getTypeNode()?.getText()),
|
|
1103
|
+
required: location === 'path' ? true : !parameter.isOptional(),
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function extractFromTsoa(projectRoot) {
|
|
1107
|
+
const files = collectProjectFiles(projectRoot, {
|
|
1108
|
+
extensions: TS_SOURCE_EXTENSIONS.filter((ext) => ext !== '.js' && ext !== '.jsx'),
|
|
1109
|
+
});
|
|
1110
|
+
if (files.length === 0) {
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
const project = new Project({
|
|
1114
|
+
skipAddingFilesFromTsConfig: true,
|
|
1115
|
+
compilerOptions: {
|
|
1116
|
+
allowJs: true,
|
|
1117
|
+
experimentalDecorators: true,
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
for (const filePath of files) {
|
|
1121
|
+
try {
|
|
1122
|
+
project.addSourceFileAtPath(filePath);
|
|
1123
|
+
}
|
|
1124
|
+
catch {
|
|
1125
|
+
// ignore parse failure
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const endpoints = [];
|
|
1129
|
+
const sourceFiles = new Set();
|
|
1130
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
1131
|
+
const relativePath = getRelativePath(projectRoot, sourceFile.getFilePath());
|
|
1132
|
+
for (const classDecl of sourceFile.getClasses()) {
|
|
1133
|
+
const routeDecorator = classDecl.getDecorators().find((decorator) => decorator.getName() === 'Route');
|
|
1134
|
+
if (!routeDecorator) {
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const classRoute = extractDecoratorStringArg(routeDecorator) ?? '/';
|
|
1138
|
+
const classTags = extractDecoratorStringArgs(classDecl.getDecorators().find((decorator) => decorator.getName() === 'Tags'));
|
|
1139
|
+
const classSecurity = extractTsoaSecurity(classDecl.getDecorators());
|
|
1140
|
+
for (const methodDecl of classDecl.getMethods()) {
|
|
1141
|
+
const httpDecorator = methodDecl.getDecorators().find((decorator) => TSOA_HTTP_DECORATORS.has(decorator.getName()));
|
|
1142
|
+
if (!httpDecorator) {
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
const method = TSOA_HTTP_DECORATORS.get(httpDecorator.getName());
|
|
1146
|
+
const methodRoute = extractDecoratorStringArg(httpDecorator) ?? '/';
|
|
1147
|
+
const methodTags = extractDecoratorStringArgs(methodDecl.getDecorators().find((decorator) => decorator.getName() === 'Tags'));
|
|
1148
|
+
const auth = uniqueStrings([
|
|
1149
|
+
...classSecurity,
|
|
1150
|
+
...extractTsoaSecurity(methodDecl.getDecorators()),
|
|
1151
|
+
]);
|
|
1152
|
+
const parameters = methodDecl.getParameters()
|
|
1153
|
+
.map((parameter) => parseTsoaParameter(parameter))
|
|
1154
|
+
.filter((parameter) => parameter !== null);
|
|
1155
|
+
const responseType = normalizeTypeText(methodDecl.getReturnTypeNode()?.getText());
|
|
1156
|
+
const fullPath = joinUrlPaths(classRoute, methodRoute);
|
|
1157
|
+
sourceFiles.add(relativePath);
|
|
1158
|
+
endpoints.push({
|
|
1159
|
+
method,
|
|
1160
|
+
path: fullPath,
|
|
1161
|
+
parameters,
|
|
1162
|
+
responses: [{
|
|
1163
|
+
statusCode: '200',
|
|
1164
|
+
type: responseType,
|
|
1165
|
+
}],
|
|
1166
|
+
responseType,
|
|
1167
|
+
auth,
|
|
1168
|
+
tags: uniqueStrings([...classTags, ...methodTags]),
|
|
1169
|
+
source: 'introspection',
|
|
1170
|
+
sourceFile: relativePath,
|
|
1171
|
+
operationId: methodDecl.getName(),
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const deduped = dedupeEndpoints(endpoints);
|
|
1177
|
+
if (deduped.length === 0) {
|
|
1178
|
+
return null;
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
source: 'introspection',
|
|
1182
|
+
endpoints: deduped,
|
|
1183
|
+
sourceFiles: [...sourceFiles].sort(),
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
function extractFromFrameworkIntrospection(projectRoot) {
|
|
1187
|
+
const fastApiResult = extractFromFastApi(projectRoot);
|
|
1188
|
+
const tsoaResult = extractFromTsoa(projectRoot);
|
|
1189
|
+
const endpoints = dedupeEndpoints([
|
|
1190
|
+
...(fastApiResult?.endpoints ?? []),
|
|
1191
|
+
...(tsoaResult?.endpoints ?? []),
|
|
1192
|
+
]);
|
|
1193
|
+
if (endpoints.length === 0) {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
source: 'introspection',
|
|
1198
|
+
endpoints,
|
|
1199
|
+
sourceFiles: uniqueStrings([
|
|
1200
|
+
...(fastApiResult?.sourceFiles ?? []),
|
|
1201
|
+
...(tsoaResult?.sourceFiles ?? []),
|
|
1202
|
+
]).sort(),
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
// ============================================================
|
|
1206
|
+
// Express AST fallback
|
|
1207
|
+
// ============================================================
|
|
1208
|
+
function isExpressRouterFactoryCall(node) {
|
|
1209
|
+
if (!Node.isCallExpression(node)) {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
const expr = node.getExpression();
|
|
1213
|
+
if (Node.isIdentifier(expr)) {
|
|
1214
|
+
return expr.getText() === 'Router';
|
|
1215
|
+
}
|
|
1216
|
+
if (Node.isPropertyAccessExpression(expr)) {
|
|
1217
|
+
return expr.getExpression().getText() === 'express' && expr.getName() === 'Router';
|
|
1218
|
+
}
|
|
1219
|
+
return false;
|
|
1220
|
+
}
|
|
1221
|
+
function isExpressAppFactoryCall(node) {
|
|
1222
|
+
return Node.isCallExpression(node) && Node.isIdentifier(node.getExpression()) && node.getExpression().getText() === 'express';
|
|
1223
|
+
}
|
|
1224
|
+
function extractLiteralPath(node) {
|
|
1225
|
+
if (!node) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
if (Node.isStringLiteral(node) ||
|
|
1229
|
+
Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
1230
|
+
return node.getLiteralText();
|
|
1231
|
+
}
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
function extractCallableName(node) {
|
|
1235
|
+
if (Node.isIdentifier(node)) {
|
|
1236
|
+
return node.getText();
|
|
1237
|
+
}
|
|
1238
|
+
if (Node.isPropertyAccessExpression(node)) {
|
|
1239
|
+
return node.getName();
|
|
1240
|
+
}
|
|
1241
|
+
if (Node.isCallExpression(node)) {
|
|
1242
|
+
return extractCallableName(node.getExpression());
|
|
1243
|
+
}
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
function extractMiddlewareNames(nodes) {
|
|
1247
|
+
const names = [];
|
|
1248
|
+
for (const node of nodes) {
|
|
1249
|
+
if (Node.isArrayLiteralExpression(node)) {
|
|
1250
|
+
names.push(...extractMiddlewareNames(node.getElements()));
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
const name = extractCallableName(node);
|
|
1254
|
+
if (name) {
|
|
1255
|
+
names.push(name);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return uniqueStrings(names);
|
|
1259
|
+
}
|
|
1260
|
+
function buildExportMap(sourceFile, routers) {
|
|
1261
|
+
let defaultExport;
|
|
1262
|
+
const namedExports = new Map();
|
|
1263
|
+
for (const statement of sourceFile.getStatements()) {
|
|
1264
|
+
if (Node.isVariableStatement(statement) && statement.hasExportKeyword()) {
|
|
1265
|
+
for (const declaration of statement.getDeclarations()) {
|
|
1266
|
+
const name = declaration.getName();
|
|
1267
|
+
if (routers.has(name)) {
|
|
1268
|
+
namedExports.set(name, name);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
for (const exportDecl of sourceFile.getExportDeclarations()) {
|
|
1274
|
+
for (const specifier of exportDecl.getNamedExports()) {
|
|
1275
|
+
const localName = specifier.getNameNode().getText();
|
|
1276
|
+
const exportName = specifier.getAliasNode()?.getText() ?? localName;
|
|
1277
|
+
if (routers.has(localName)) {
|
|
1278
|
+
namedExports.set(exportName, localName);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
for (const exportAssignment of sourceFile.getExportAssignments()) {
|
|
1283
|
+
const expr = exportAssignment.getExpression();
|
|
1284
|
+
if (Node.isIdentifier(expr) && routers.has(expr.getText())) {
|
|
1285
|
+
defaultExport = expr.getText();
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return { defaultExport, namedExports };
|
|
1289
|
+
}
|
|
1290
|
+
function parseExpressRouteChain(expr, routers, relativePath) {
|
|
1291
|
+
if (!Node.isCallExpression(expr)) {
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
const routes = [];
|
|
1295
|
+
let current = expr;
|
|
1296
|
+
while (Node.isCallExpression(current)) {
|
|
1297
|
+
const currentExpr = current.getExpression();
|
|
1298
|
+
if (!Node.isPropertyAccessExpression(currentExpr)) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
const methodName = currentExpr.getName();
|
|
1302
|
+
const targetExpr = currentExpr.getExpression();
|
|
1303
|
+
if (!HTTP_METHOD_SET.has(methodName.toLowerCase())) {
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
routes.unshift({
|
|
1307
|
+
method: methodName.toUpperCase(),
|
|
1308
|
+
path: '',
|
|
1309
|
+
middlewares: extractMiddlewareNames(current.getArguments()),
|
|
1310
|
+
sourceFile: relativePath,
|
|
1311
|
+
});
|
|
1312
|
+
if (!Node.isCallExpression(targetExpr)) {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
current = targetExpr;
|
|
1316
|
+
const routeExpr = current.getExpression();
|
|
1317
|
+
if (!Node.isPropertyAccessExpression(routeExpr) || routeExpr.getName() !== 'route') {
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const routerTarget = routeExpr.getExpression();
|
|
1321
|
+
if (!Node.isIdentifier(routerTarget) || !routers.has(routerTarget.getText())) {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
const routePath = extractLiteralPath(current.getArguments()[0]);
|
|
1325
|
+
if (!routePath) {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
return {
|
|
1329
|
+
routerName: routerTarget.getText(),
|
|
1330
|
+
routes: routes.map((route) => ({ ...route, path: routePath })),
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
function analyzeExpressFile(projectRoot, sourceFile) {
|
|
1336
|
+
const filePath = sourceFile.getFilePath();
|
|
1337
|
+
const relativePath = getRelativePath(projectRoot, filePath);
|
|
1338
|
+
const routers = new Map();
|
|
1339
|
+
const imports = new Map();
|
|
1340
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1341
|
+
const moduleSpecifier = importDecl.getModuleSpecifierValue();
|
|
1342
|
+
const resolved = resolveModulePath(filePath, moduleSpecifier, TS_SOURCE_EXTENSIONS);
|
|
1343
|
+
if (!resolved) {
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
const defaultImport = importDecl.getDefaultImport();
|
|
1347
|
+
if (defaultImport) {
|
|
1348
|
+
imports.set(defaultImport.getText(), {
|
|
1349
|
+
targetFile: resolved,
|
|
1350
|
+
exportName: 'default',
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
for (const namedImport of importDecl.getNamedImports()) {
|
|
1354
|
+
const localName = namedImport.getAliasNode()?.getText() ?? namedImport.getName();
|
|
1355
|
+
imports.set(localName, {
|
|
1356
|
+
targetFile: resolved,
|
|
1357
|
+
exportName: namedImport.getName(),
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
for (const declaration of sourceFile.getVariableDeclarations()) {
|
|
1362
|
+
const initializer = declaration.getInitializer();
|
|
1363
|
+
if (!initializer) {
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
const localName = declaration.getName();
|
|
1367
|
+
let kind = null;
|
|
1368
|
+
if (isExpressRouterFactoryCall(initializer)) {
|
|
1369
|
+
kind = 'router';
|
|
1370
|
+
}
|
|
1371
|
+
else if (isExpressAppFactoryCall(initializer)) {
|
|
1372
|
+
kind = 'app';
|
|
1373
|
+
}
|
|
1374
|
+
if (kind) {
|
|
1375
|
+
routers.set(localName, {
|
|
1376
|
+
id: `${filePath}#${localName}`,
|
|
1377
|
+
filePath,
|
|
1378
|
+
localName,
|
|
1379
|
+
kind,
|
|
1380
|
+
routes: [],
|
|
1381
|
+
mounts: [],
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
for (const statement of sourceFile.getStatements()) {
|
|
1386
|
+
if (!Node.isExpressionStatement(statement)) {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
const expr = statement.getExpression();
|
|
1390
|
+
const chain = parseExpressRouteChain(expr, routers, relativePath);
|
|
1391
|
+
if (chain) {
|
|
1392
|
+
const router = routers.get(chain.routerName);
|
|
1393
|
+
router.routes.push(...chain.routes);
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (!Node.isCallExpression(expr)) {
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
const callExpr = expr.getExpression();
|
|
1400
|
+
if (!Node.isPropertyAccessExpression(callExpr)) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const target = callExpr.getExpression();
|
|
1404
|
+
if (!Node.isIdentifier(target) || !routers.has(target.getText())) {
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
const router = routers.get(target.getText());
|
|
1408
|
+
const methodName = callExpr.getName();
|
|
1409
|
+
if (methodName === 'use') {
|
|
1410
|
+
const args = expr.getArguments();
|
|
1411
|
+
let prefix = '';
|
|
1412
|
+
let restArgs = args;
|
|
1413
|
+
const firstArgPath = extractLiteralPath(args[0]);
|
|
1414
|
+
if (firstArgPath !== null) {
|
|
1415
|
+
prefix = firstArgPath;
|
|
1416
|
+
restArgs = args.slice(1);
|
|
1417
|
+
}
|
|
1418
|
+
const routerTargets = restArgs.filter((arg) => Node.isIdentifier(arg) &&
|
|
1419
|
+
(routers.has(arg.getText()) || imports.has(arg.getText())));
|
|
1420
|
+
if (routerTargets.length === 0) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
const middlewareNames = extractMiddlewareNames(restArgs.filter((arg) => !routerTargets.includes(arg)));
|
|
1424
|
+
for (const routerTarget of routerTargets) {
|
|
1425
|
+
router.mounts.push({
|
|
1426
|
+
prefix,
|
|
1427
|
+
middlewares: middlewareNames,
|
|
1428
|
+
targetLocalName: routerTarget.getText(),
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (!HTTP_METHOD_SET.has(methodName.toLowerCase())) {
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
const routePath = extractLiteralPath(expr.getArguments()[0]);
|
|
1437
|
+
if (!routePath) {
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
router.routes.push({
|
|
1441
|
+
method: methodName.toUpperCase(),
|
|
1442
|
+
path: routePath,
|
|
1443
|
+
middlewares: extractMiddlewareNames(expr.getArguments().slice(1)),
|
|
1444
|
+
sourceFile: relativePath,
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
const exports = buildExportMap(sourceFile, routers);
|
|
1448
|
+
return {
|
|
1449
|
+
filePath,
|
|
1450
|
+
routers,
|
|
1451
|
+
defaultExport: exports.defaultExport,
|
|
1452
|
+
namedExports: exports.namedExports,
|
|
1453
|
+
imports,
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function resolveExpressRouters(analyses) {
|
|
1457
|
+
const resolved = new Map();
|
|
1458
|
+
const byFile = new Map(analyses.map((analysis) => [analysis.filePath, analysis]));
|
|
1459
|
+
for (const analysis of analyses) {
|
|
1460
|
+
for (const router of analysis.routers.values()) {
|
|
1461
|
+
const mounts = [];
|
|
1462
|
+
for (const mount of router.mounts) {
|
|
1463
|
+
let targetId;
|
|
1464
|
+
if (analysis.routers.has(mount.targetLocalName)) {
|
|
1465
|
+
targetId = analysis.routers.get(mount.targetLocalName).id;
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
const binding = analysis.imports.get(mount.targetLocalName);
|
|
1469
|
+
const importedAnalysis = binding ? byFile.get(binding.targetFile) : undefined;
|
|
1470
|
+
if (binding && importedAnalysis) {
|
|
1471
|
+
const localName = binding.exportName === 'default'
|
|
1472
|
+
? importedAnalysis.defaultExport
|
|
1473
|
+
: importedAnalysis.namedExports.get(binding.exportName);
|
|
1474
|
+
if (localName && importedAnalysis.routers.has(localName)) {
|
|
1475
|
+
targetId = importedAnalysis.routers.get(localName).id;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (targetId) {
|
|
1480
|
+
mounts.push({
|
|
1481
|
+
prefix: mount.prefix,
|
|
1482
|
+
middlewares: mount.middlewares,
|
|
1483
|
+
targetId,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
resolved.set(router.id, {
|
|
1488
|
+
...router,
|
|
1489
|
+
mounts,
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return resolved;
|
|
1494
|
+
}
|
|
1495
|
+
function extractFromExpressAst(projectRoot) {
|
|
1496
|
+
const files = collectProjectFiles(projectRoot, { extensions: TS_SOURCE_EXTENSIONS });
|
|
1497
|
+
if (files.length === 0) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
const project = new Project({
|
|
1501
|
+
skipAddingFilesFromTsConfig: true,
|
|
1502
|
+
compilerOptions: {
|
|
1503
|
+
allowJs: true,
|
|
1504
|
+
},
|
|
1505
|
+
});
|
|
1506
|
+
for (const filePath of files) {
|
|
1507
|
+
try {
|
|
1508
|
+
project.addSourceFileAtPath(filePath);
|
|
1509
|
+
}
|
|
1510
|
+
catch {
|
|
1511
|
+
// ignore parse failure
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const analyses = project.getSourceFiles()
|
|
1515
|
+
.map((sourceFile) => analyzeExpressFile(projectRoot, sourceFile))
|
|
1516
|
+
.filter((analysis) => analysis.routers.size > 0 ||
|
|
1517
|
+
analysis.imports.size > 0);
|
|
1518
|
+
const routers = resolveExpressRouters(analyses);
|
|
1519
|
+
if (routers.size === 0) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
const parentCounts = new Map();
|
|
1523
|
+
for (const router of routers.values()) {
|
|
1524
|
+
parentCounts.set(router.id, parentCounts.get(router.id) ?? 0);
|
|
1525
|
+
for (const mount of router.mounts) {
|
|
1526
|
+
parentCounts.set(mount.targetId, (parentCounts.get(mount.targetId) ?? 0) + 1);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
const roots = [...routers.values()].filter((router) => router.kind === 'app' || (parentCounts.get(router.id) ?? 0) === 0);
|
|
1530
|
+
const endpoints = [];
|
|
1531
|
+
const sourceFiles = new Set();
|
|
1532
|
+
const visited = new Set();
|
|
1533
|
+
function walk(routerId, prefix, inheritedMiddlewares) {
|
|
1534
|
+
const router = routers.get(routerId);
|
|
1535
|
+
if (!router) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
for (const route of router.routes) {
|
|
1539
|
+
const fullPath = joinUrlPaths(prefix, route.path);
|
|
1540
|
+
const auth = collectAuthHints([...inheritedMiddlewares, ...route.middlewares]);
|
|
1541
|
+
const tags = deriveTags(fullPath, route.sourceFile);
|
|
1542
|
+
sourceFiles.add(route.sourceFile);
|
|
1543
|
+
endpoints.push({
|
|
1544
|
+
method: route.method,
|
|
1545
|
+
path: fullPath,
|
|
1546
|
+
parameters: extractPathParameters(fullPath),
|
|
1547
|
+
responses: [{
|
|
1548
|
+
statusCode: '200',
|
|
1549
|
+
type: 'unknown',
|
|
1550
|
+
}],
|
|
1551
|
+
responseType: 'unknown',
|
|
1552
|
+
auth,
|
|
1553
|
+
tags,
|
|
1554
|
+
source: 'ast',
|
|
1555
|
+
sourceFile: route.sourceFile,
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
for (const mount of router.mounts) {
|
|
1559
|
+
const visitKey = `${routerId}->${mount.targetId}:${mount.prefix}`;
|
|
1560
|
+
if (visited.has(visitKey)) {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
visited.add(visitKey);
|
|
1564
|
+
walk(mount.targetId, joinUrlPaths(prefix, mount.prefix), [...inheritedMiddlewares, ...mount.middlewares]);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
const rootRouters = roots.length > 0 ? roots : [...routers.values()];
|
|
1568
|
+
for (const root of rootRouters) {
|
|
1569
|
+
walk(root.id, '', []);
|
|
1570
|
+
}
|
|
1571
|
+
const deduped = dedupeEndpoints(endpoints);
|
|
1572
|
+
if (deduped.length === 0) {
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
return {
|
|
1576
|
+
source: 'ast',
|
|
1577
|
+
endpoints: deduped,
|
|
1578
|
+
sourceFiles: [...sourceFiles].sort(),
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
// ============================================================
|
|
1582
|
+
// Generator 实现
|
|
1583
|
+
// ============================================================
|
|
1584
|
+
export class ApiSurfaceGenerator {
|
|
1585
|
+
id = 'api-surface';
|
|
1586
|
+
name = 'API Surface Reference 生成器';
|
|
1587
|
+
description = '按 schema -> introspection -> ast 优先级生成 API Surface Reference';
|
|
1588
|
+
async isApplicable(context) {
|
|
1589
|
+
if (collectProjectFiles(context.projectRoot, {
|
|
1590
|
+
extensions: ['.json', '.yaml', '.yml'],
|
|
1591
|
+
fileNamePattern: OPENAPI_FILE_PATTERN,
|
|
1592
|
+
}).length > 0) {
|
|
1593
|
+
return true;
|
|
1594
|
+
}
|
|
1595
|
+
const sourceFiles = collectProjectFiles(context.projectRoot, {
|
|
1596
|
+
extensions: [...TS_SOURCE_EXTENSIONS, ...PYTHON_EXTENSIONS],
|
|
1597
|
+
});
|
|
1598
|
+
for (const filePath of sourceFiles) {
|
|
1599
|
+
const content = tryReadFile(filePath);
|
|
1600
|
+
if (!content) {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (/\bFastAPI\s*\(|\bAPIRouter\s*\(|@Route\s*\(|\bexpress\.Router\s*\(|\bRouter\s*\(|\.route\s*\(/.test(content)) {
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
async extract(context) {
|
|
1610
|
+
const projectName = detectProjectName(context.projectRoot);
|
|
1611
|
+
const schemaResult = extractFromSchema(context.projectRoot);
|
|
1612
|
+
if (schemaResult) {
|
|
1613
|
+
return {
|
|
1614
|
+
projectName,
|
|
1615
|
+
source: schemaResult.source,
|
|
1616
|
+
endpoints: schemaResult.endpoints,
|
|
1617
|
+
sourceFiles: schemaResult.sourceFiles,
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
const introspectionResult = extractFromFrameworkIntrospection(context.projectRoot);
|
|
1621
|
+
if (introspectionResult) {
|
|
1622
|
+
return {
|
|
1623
|
+
projectName,
|
|
1624
|
+
source: introspectionResult.source,
|
|
1625
|
+
endpoints: introspectionResult.endpoints,
|
|
1626
|
+
sourceFiles: introspectionResult.sourceFiles,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
const astResult = extractFromExpressAst(context.projectRoot);
|
|
1630
|
+
if (astResult) {
|
|
1631
|
+
return {
|
|
1632
|
+
projectName,
|
|
1633
|
+
source: astResult.source,
|
|
1634
|
+
endpoints: astResult.endpoints,
|
|
1635
|
+
sourceFiles: astResult.sourceFiles,
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
return {
|
|
1639
|
+
projectName,
|
|
1640
|
+
source: 'ast',
|
|
1641
|
+
endpoints: [],
|
|
1642
|
+
sourceFiles: [],
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
async generate(input, _options) {
|
|
1646
|
+
const endpoints = dedupeEndpoints(input.endpoints);
|
|
1647
|
+
const byMethod = {};
|
|
1648
|
+
for (const endpoint of endpoints) {
|
|
1649
|
+
byMethod[endpoint.method] = (byMethod[endpoint.method] ?? 0) + 1;
|
|
1650
|
+
}
|
|
1651
|
+
const tags = uniqueStrings(endpoints.flatMap((endpoint) => endpoint.tags)).sort();
|
|
1652
|
+
const totalParameters = endpoints.reduce((sum, endpoint) => sum + endpoint.parameters.length, 0);
|
|
1653
|
+
return {
|
|
1654
|
+
title: `API Surface Reference: ${input.projectName}`,
|
|
1655
|
+
generatedAt: new Date().toISOString().split('T')[0],
|
|
1656
|
+
projectName: input.projectName,
|
|
1657
|
+
source: input.source,
|
|
1658
|
+
endpoints,
|
|
1659
|
+
totalEndpoints: endpoints.length,
|
|
1660
|
+
totalParameters,
|
|
1661
|
+
sourceFiles: uniqueStrings(input.sourceFiles).sort(),
|
|
1662
|
+
byMethod,
|
|
1663
|
+
tags,
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
render(output) {
|
|
1667
|
+
const template = loadTemplate('api-surface.hbs', import.meta.url);
|
|
1668
|
+
return template(output);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
//# sourceMappingURL=api-surface-generator.js.map
|