gitnexus 1.6.11-rc.37 → 1.6.11-rc.38
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 +6 -0
- package/dist/cli/analyze-options.d.ts +7 -0
- package/dist/cli/analyze-watch.js +5 -0
- package/dist/cli/analyze.js +11 -0
- package/dist/cli/index.js +2 -0
- package/dist/core/analysis-feature-registry.d.ts +2 -0
- package/dist/core/analysis-feature-registry.js +15 -0
- package/dist/core/group/extractors/http-patterns/java.js +4 -4
- package/dist/core/group/extractors/http-patterns/kotlin.js +84 -31
- package/dist/core/ingestion/asyncapi/document.d.ts +226 -0
- package/dist/core/ingestion/asyncapi/document.js +758 -0
- package/dist/core/ingestion/asyncapi/protocol.d.ts +79 -0
- package/dist/core/ingestion/asyncapi/protocol.js +203 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.d.ts +5 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.js +9 -0
- package/dist/core/ingestion/frameworks/spring/destinations.d.ts +25 -0
- package/dist/core/ingestion/frameworks/spring/vendor-prefixes.d.ts +7 -0
- package/dist/core/ingestion/frameworks/spring/vendor-prefixes.js +22 -0
- package/dist/core/ingestion/pipeline-phases/spring-destinations.d.ts +43 -2
- package/dist/core/ingestion/pipeline-phases/spring-destinations.js +202 -3
- package/dist/core/ingestion/pipeline.d.ts +14 -0
- package/dist/core/ingestion/route-extractors/kotlin-spring.js +7 -14
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +30 -1
- package/dist/core/ingestion/route-extractors/spring-shared.js +77 -9
- package/dist/core/ingestion/route-extractors/spring.js +9 -5
- package/dist/core/run-analyze.d.ts +5 -0
- package/dist/core/run-analyze.js +49 -13
- package/dist/mcp/resources.js +7 -0
- package/dist/server/analyze-launch.d.ts +1 -0
- package/dist/server/analyze-launch.js +1 -0
- package/dist/server/api.js +7 -1
- package/dist/storage/repo-meta.d.ts +22 -0
- package/package.json +1 -1
- package/skills/gitnexus-cli.md +1 -0
package/README.md
CHANGED
|
@@ -349,6 +349,12 @@ anchors are deliberately omitted. Add common infrastructure fields such as `/hea
|
|
|
349
349
|
|
|
350
350
|
`--spring-actuator` is explicitly opt-in. The path may be a JSON bundle keyed by `mappings`, `beans`, `conditions`, `configprops`, and/or `env`, or a directory containing endpoint-named JSON files. Runtime mappings and beans confirm matching static nodes; conditions and configuration property keys enrich existing evidence, with conservative runtime-only nodes added when no match exists. The configured input is excluded from source scanning; only normalized repository-relative exclusions are retained for future scans, never absolute paths. Env/configprops values, origins, condition messages, and source names are never persisted or printed. Enabled runs always rebuild because runtime snapshots are external to git freshness; omitting the option later rebuilds once to remove runtime evidence. Project config can set the same path with `springActuator` in `.gitnexusrc`.
|
|
351
351
|
|
|
352
|
+
`--asyncapi-spec` is explicitly opt-in and accepts a directory of AsyncAPI documents or a single document; the path is resolved against the repository root, so a committed `docs/asyncapi` and an absolute cache written by something else both work. Each `operations[]` entry of an **AsyncAPI 3.x** document can contribute a `Destination` node keyed by broker and address, with `action: send` emitting `PUBLISHES_TO` and `action: receive` emitting `CONSUMES_FROM`, so a document and source code that name one address on one broker land on the same node. Edges start at the document, not at a callable — a document states that the service talks to an address, not which method does — and no address a document names is ever attached to an unresolved source site.
|
|
353
|
+
|
|
354
|
+
An operation must name a protocol, either through its own `bindings` or through the `servers[].protocol` of the servers its channel resolves to (a channel that lists no `servers` resolves to all of them); operations that name none are refused, as are operations whose two readings name different brokers, and channels that inherit a multi-protocol server set without choosing. HTTP and WebSocket documents are refused for destination minting: there the host rather than the address names the place, and an HTTP endpoint is already modelled as a `Route`. A parameterized address — a channel declaring `parameters`, or an address containing `{` — is refused rather than keyed: two services publishing `{env}.orders` share a pattern, not a queue. AsyncAPI **2.x is refused** under its own counted reason and never mapped, because its `publish`/`subscribe` are inverted relative to 3.x `send`/`receive` and a naive mapping would reverse the async graph while leaving it connected. Every refusal is counted, and a configured path that yields nothing is reported rather than passed over in silence.
|
|
355
|
+
|
|
356
|
+
Like Actuator snapshots, documents are external to git freshness — replacing one moves no commit and dirties no file — so an enabled run always rebuilds, and the first later run without the option rebuilds once to remove document-derived evidence. There is no glob-based auto-discovery, and the option is unsupported with `--watch`.
|
|
357
|
+
|
|
352
358
|
> **`gitnexus uninstall`** reverses `gitnexus setup` — it removes the GitNexus MCP entries, hooks, and skill directories it added to each detected editor. Skill directories are identified **by bundled gitnexus skill name** (e.g. `gitnexus-cli/`), so if you customized files inside an installed skill directory, back them up first. It is a dry-run preview by default and prints the exact paths it would remove; pass `--force` to apply. Per-repo indexes (`gitnexus clean --all`) and the global npm package (`npm uninstall -g gitnexus`) are left for you to remove.
|
|
353
359
|
|
|
354
360
|
## Remote Embeddings
|
|
@@ -129,6 +129,13 @@ export interface AnalyzeOptions {
|
|
|
129
129
|
* bundle or a directory containing endpoint JSON files. Disabled by default.
|
|
130
130
|
*/
|
|
131
131
|
springActuator?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Explicit local AsyncAPI 3.x document input. Accepts a directory of
|
|
134
|
+
* documents or a single document, resolved against the repository root so an
|
|
135
|
+
* out-of-band cache and a committed directory are equally usable. Disabled by
|
|
136
|
+
* default.
|
|
137
|
+
*/
|
|
138
|
+
asyncapiSpec?: string;
|
|
132
139
|
/** OpenAI-compatible embeddings base URL (incl. /v1). Overrides GITNEXUS_EMBEDDING_URL. */
|
|
133
140
|
embeddingBaseUrl?: string;
|
|
134
141
|
/** Embedding model name. Overrides GITNEXUS_EMBEDDING_MODEL. */
|
|
@@ -76,6 +76,11 @@ export async function resolveWatchOptions(repoPath, cli, baseline, reportIgnored
|
|
|
76
76
|
['--index-only', cli.indexOnly],
|
|
77
77
|
['--skip-git', cli.skipGit],
|
|
78
78
|
['--spring-actuator', cli.springActuator],
|
|
79
|
+
// Rejected under --watch for the same reason as --spring-actuator: the
|
|
80
|
+
// watcher reacts to source changes, and nothing watches an out-of-band
|
|
81
|
+
// document directory. Honouring the flag here would read the documents once
|
|
82
|
+
// and then quietly serve a stale answer for the rest of the session.
|
|
83
|
+
['--asyncapi-spec', cli.asyncapiSpec],
|
|
79
84
|
['walCheckpointThreshold', cli.walCheckpointThreshold],
|
|
80
85
|
['embeddingThreads', cli.embeddingThreads],
|
|
81
86
|
['embeddingBatchSize', cli.embeddingBatchSize],
|
package/dist/cli/analyze.js
CHANGED
|
@@ -786,6 +786,16 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
|
|
|
786
786
|
!setPositiveEnv('--embedding-sub-batch-size', 'GITNEXUS_EMBEDDING_SUB_BATCH_SIZE', options.embeddingSubBatchSize)) {
|
|
787
787
|
return;
|
|
788
788
|
}
|
|
789
|
+
// An empty value resolves to the repository root, so `--asyncapi-spec ""`
|
|
790
|
+
// walks the whole tree — defeating the module's own rule that there is no
|
|
791
|
+
// glob-based auto-discovery, and spending the walk budget on `node_modules`.
|
|
792
|
+
// The HTTP entry point already rejects exactly this value; two doors onto one
|
|
793
|
+
// option must not hold different rules.
|
|
794
|
+
if (options.asyncapiSpec !== undefined && options.asyncapiSpec.trim() === '') {
|
|
795
|
+
cliError(' --asyncapi-spec must be a non-empty path.\n');
|
|
796
|
+
process.exitCode = 1;
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
789
799
|
if (options.embeddingDevice) {
|
|
790
800
|
const allowed = new Set(['auto', 'cpu', 'dml', 'cuda', 'wasm']);
|
|
791
801
|
if (!allowed.has(options.embeddingDevice)) {
|
|
@@ -1115,6 +1125,7 @@ const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstr
|
|
|
1115
1125
|
// forwarded to the routes phase consumer scan.
|
|
1116
1126
|
fetchWrappers: options.fetchWrappers,
|
|
1117
1127
|
springActuatorPath: options.springActuator,
|
|
1128
|
+
asyncApiSpecPath: options.asyncapiSpec,
|
|
1118
1129
|
// The CLI always process.exit()s after this returns (success path at the
|
|
1119
1130
|
// end of analyzeCommandImpl, error/interrupt paths via process.exit too),
|
|
1120
1131
|
// so the finalize close skips the native conn/db close — it can double-free
|
package/dist/cli/index.js
CHANGED
|
@@ -85,6 +85,8 @@ program
|
|
|
85
85
|
.option('--workers <n>', 'Parse worker pool size (>=1). Default: cores-1 capped at 16, auto-sized to the repo.')
|
|
86
86
|
.option('--spring-actuator <path>', 'Import local Spring Boot Actuator JSON snapshots (mappings, beans, conditions, ' +
|
|
87
87
|
'configprops, env). Explicit opt-in; disabled by default.')
|
|
88
|
+
.option('--asyncapi-spec <path>', 'Read AsyncAPI 3.x documents from this directory or file and resolve broker ' +
|
|
89
|
+
'addresses from them. Explicit opt-in; disabled by default.')
|
|
88
90
|
.option('--embedding-threads <n>', 'Limit local ONNX embedding CPU threads')
|
|
89
91
|
.option('--embedding-batch-size <n>', 'Number of nodes per embedding batch')
|
|
90
92
|
.option('--embedding-sub-batch-size <n>', 'Number of chunks per embedding model call')
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Production registry of independently versioned analysis capabilities. */
|
|
2
|
+
export declare const ANALYSIS_FEATURES: readonly [import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor, import("./analysis-features.js").AnalysisFeatureDescriptor];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CLASS_FRAMEWORK_ANNOTATIONS_FEATURE } from './analysis-features.js';
|
|
2
|
+
import { SPRING_AOP_FEATURE, SPRING_BEAN_INVENTORY_FEATURE, SPRING_CONDITIONALS_FEATURE, SPRING_NON_HTTP_HANDLERS_FEATURE, SPRING_ROUTE_BINDINGS_FEATURE, } from './ingestion/frameworks/spring/analysis-features.js';
|
|
3
|
+
import { JAVA_ENUM_INTERFACE_HERITAGE_FEATURE, JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE, SPRING_CONFIG_BINDINGS_FEATURE, } from './ingestion/languages/java/analysis-features.js';
|
|
4
|
+
/** Production registry of independently versioned analysis capabilities. */
|
|
5
|
+
export const ANALYSIS_FEATURES = [
|
|
6
|
+
CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
|
|
7
|
+
SPRING_AOP_FEATURE,
|
|
8
|
+
SPRING_BEAN_INVENTORY_FEATURE,
|
|
9
|
+
SPRING_CONDITIONALS_FEATURE,
|
|
10
|
+
SPRING_NON_HTTP_HANDLERS_FEATURE,
|
|
11
|
+
SPRING_ROUTE_BINDINGS_FEATURE,
|
|
12
|
+
SPRING_CONFIG_BINDINGS_FEATURE,
|
|
13
|
+
JAVA_ENUM_INTERFACE_HERITAGE_FEATURE,
|
|
14
|
+
JAVA_RECORD_COMPONENT_ACCESSORS_FEATURE,
|
|
15
|
+
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Java from 'tree-sitter-java';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
|
-
import { springAnnotationHttpMethods, intersectSpringHttpMethods, isRouteMemberKey, findEnclosingClass, joinPath, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
3
|
+
import { springAnnotationHttpMethods, intersectSpringHttpMethods, isRouteMemberKey, findEnclosingClass, isClassLevelMappingAnnotation, joinPath, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
4
|
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
5
5
|
import { expandJavaWildcardStaticImports, extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, parseJavaConstOperands, prepareJavaRouteConstants, } from '../../../ingestion/route-extractors/java-const-resolver.js';
|
|
6
6
|
import { extractStaticPathExpression, inferOkHttpMethod, inferHttpClientMethod, okHttpUrlRootsAtBuilder, httpClientUriRootsAtNewBuilder, httpClientChainHasUriCall, } from './java-static-path.js';
|
|
@@ -394,12 +394,12 @@ function annotationHasRouteMember(annotation) {
|
|
|
394
394
|
return false;
|
|
395
395
|
}
|
|
396
396
|
function typeRequestMethods(typeNode) {
|
|
397
|
-
const mappings = declarationAnnotations(typeNode).filter((annotation) => simpleName(annotation.childForFieldName('name')?.text ?? '')
|
|
397
|
+
const mappings = declarationAnnotations(typeNode).filter((annotation) => isClassLevelMappingAnnotation(simpleName(annotation.childForFieldName('name')?.text ?? '')));
|
|
398
398
|
if (mappings.length === 0)
|
|
399
399
|
return ['*'];
|
|
400
400
|
if (mappings.length !== 1)
|
|
401
401
|
return [];
|
|
402
|
-
return springAnnotationHttpMethods('RequestMapping', mappings[0].text);
|
|
402
|
+
return springAnnotationHttpMethods(simpleName(mappings[0].childForFieldName('name')?.text ?? 'RequestMapping'), mappings[0].text);
|
|
403
403
|
}
|
|
404
404
|
function hasAnnotation(node, names) {
|
|
405
405
|
const modifiers = node.namedChildren.find((child) => child.type === 'modifiers');
|
|
@@ -551,7 +551,7 @@ function scanRouteAnnotations(tree) {
|
|
|
551
551
|
}
|
|
552
552
|
// Type-level (class or interface): a Spring `@RequestMapping` URL prefix, or
|
|
553
553
|
// — on an interface — an OpenFeign `@FeignClient(path = "...")` prefix.
|
|
554
|
-
if (ann
|
|
554
|
+
if (isClassLevelMappingAnnotation(ann)) {
|
|
555
555
|
if (!isRouteMemberKey(keyNode))
|
|
556
556
|
continue;
|
|
557
557
|
if (!valueNode) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { requireVendoredGrammar } from '../../../tree-sitter/vendored-grammars.js';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
|
-
import {
|
|
3
|
+
import { findEnclosingClass, intersectSpringHttpMethods, isClassLevelMappingAnnotation, joinPath, springAnnotationHttpMethods, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
4
|
import { buildKotlinConstantIndex, extractKotlinModuleConstants, foldKotlinOperands, isKotlinConstantFile, overlayKotlinConstantIndex, parseKotlinConstOperands, unfoldableDeclarationsOf, unquoteKotlinIdentifier, } from '../../../ingestion/route-extractors/kotlin-const-resolver.js';
|
|
5
5
|
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
6
6
|
/**
|
|
@@ -392,6 +392,13 @@ function inferKotlinOkHttpMethod(urlCall) {
|
|
|
392
392
|
}
|
|
393
393
|
return name === null ? 'GET' : name.toUpperCase();
|
|
394
394
|
}
|
|
395
|
+
function enclosingAnnotationText(node) {
|
|
396
|
+
for (let current = node; current; current = current.parent) {
|
|
397
|
+
if (current.type === 'annotation')
|
|
398
|
+
return current.text;
|
|
399
|
+
}
|
|
400
|
+
return node.text;
|
|
401
|
+
}
|
|
395
402
|
/**
|
|
396
403
|
* Build the plugin only if the Kotlin grammar is available. Compiling
|
|
397
404
|
* the queries against a null grammar would throw at module load time
|
|
@@ -429,7 +436,7 @@ function buildKotlinPlugin(language) {
|
|
|
429
436
|
(modifiers
|
|
430
437
|
(annotation
|
|
431
438
|
(constructor_invocation
|
|
432
|
-
(user_type (type_identifier) @ann (#
|
|
439
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
433
440
|
(value_arguments
|
|
434
441
|
(value_argument . [(string_literal) @prefix (collection_literal (string_literal) @prefix)])))))
|
|
435
442
|
(type_identifier) @cls) @class
|
|
@@ -442,7 +449,7 @@ function buildKotlinPlugin(language) {
|
|
|
442
449
|
(modifiers
|
|
443
450
|
(annotation
|
|
444
451
|
(constructor_invocation
|
|
445
|
-
(user_type (type_identifier) @ann (#
|
|
452
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
446
453
|
(value_arguments
|
|
447
454
|
(value_argument
|
|
448
455
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
@@ -457,7 +464,7 @@ function buildKotlinPlugin(language) {
|
|
|
457
464
|
(modifiers
|
|
458
465
|
(annotation
|
|
459
466
|
(constructor_invocation
|
|
460
|
-
(user_type (type_identifier) @ann (#
|
|
467
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
461
468
|
(value_arguments
|
|
462
469
|
(value_argument . ${arrayOfArg('@prefix')})))))
|
|
463
470
|
(type_identifier) @cls) @class
|
|
@@ -470,7 +477,7 @@ function buildKotlinPlugin(language) {
|
|
|
470
477
|
(modifiers
|
|
471
478
|
(annotation
|
|
472
479
|
(constructor_invocation
|
|
473
|
-
(user_type (type_identifier) @ann (#
|
|
480
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
474
481
|
(value_arguments
|
|
475
482
|
(value_argument
|
|
476
483
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
@@ -495,7 +502,7 @@ function buildKotlinPlugin(language) {
|
|
|
495
502
|
(modifiers
|
|
496
503
|
(annotation
|
|
497
504
|
(constructor_invocation
|
|
498
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
505
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
499
506
|
(value_arguments
|
|
500
507
|
(value_argument . [(string_literal) @path (collection_literal (string_literal) @path)])))))
|
|
501
508
|
(simple_identifier) @method_name) @method
|
|
@@ -508,7 +515,7 @@ function buildKotlinPlugin(language) {
|
|
|
508
515
|
(modifiers
|
|
509
516
|
(annotation
|
|
510
517
|
(constructor_invocation
|
|
511
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
518
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
512
519
|
(value_arguments
|
|
513
520
|
(value_argument
|
|
514
521
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
@@ -523,7 +530,7 @@ function buildKotlinPlugin(language) {
|
|
|
523
530
|
(modifiers
|
|
524
531
|
(annotation
|
|
525
532
|
(constructor_invocation
|
|
526
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
533
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
527
534
|
(value_arguments
|
|
528
535
|
(value_argument . ${arrayOfArg('@path')})))))
|
|
529
536
|
(simple_identifier) @method_name) @method
|
|
@@ -536,7 +543,7 @@ function buildKotlinPlugin(language) {
|
|
|
536
543
|
(modifiers
|
|
537
544
|
(annotation
|
|
538
545
|
(constructor_invocation
|
|
539
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
546
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
540
547
|
(value_arguments
|
|
541
548
|
(value_argument
|
|
542
549
|
(simple_identifier) @key (#match? @key "^(path|value)$")
|
|
@@ -571,7 +578,7 @@ function buildKotlinPlugin(language) {
|
|
|
571
578
|
(modifiers
|
|
572
579
|
(annotation
|
|
573
580
|
(constructor_invocation
|
|
574
|
-
(user_type (type_identifier) @ann (#
|
|
581
|
+
(user_type (type_identifier) @ann (#match? @ann "RequestMapping$"))
|
|
575
582
|
(value_arguments (value_argument) @arg))))
|
|
576
583
|
(type_identifier) @cls) @class
|
|
577
584
|
`,
|
|
@@ -589,7 +596,7 @@ function buildKotlinPlugin(language) {
|
|
|
589
596
|
(modifiers
|
|
590
597
|
(annotation
|
|
591
598
|
(constructor_invocation
|
|
592
|
-
(user_type (type_identifier) @ann (#match? @ann "
|
|
599
|
+
(user_type (type_identifier) @ann (#match? @ann "(Request|Get|Post|Put|Delete|Patch)Mapping$"))
|
|
593
600
|
(value_arguments (value_argument) @arg))))
|
|
594
601
|
(simple_identifier) @method_name) @method
|
|
595
602
|
`,
|
|
@@ -649,8 +656,11 @@ function buildKotlinPlugin(language) {
|
|
|
649
656
|
for (const match of runCompiledPatterns(SPRING_CONST_CLASS_PREFIX_PATTERNS, tree)) {
|
|
650
657
|
const argNode = match.captures.arg;
|
|
651
658
|
const classNode = match.captures.class;
|
|
659
|
+
const annNode = match.captures.ann;
|
|
652
660
|
if (!argNode || !classNode)
|
|
653
661
|
continue;
|
|
662
|
+
if (annNode && !isClassLevelMappingAnnotation(annNode.text))
|
|
663
|
+
continue;
|
|
654
664
|
if ((resolvedPrefixes.get(classNode.id) ?? []).length > 0)
|
|
655
665
|
continue;
|
|
656
666
|
const expr = kotlinRouteArgumentExpression(argNode);
|
|
@@ -1145,14 +1155,37 @@ function buildKotlinPlugin(language) {
|
|
|
1145
1155
|
return body.namedChildren.filter((c) => c.type === 'function_declaration');
|
|
1146
1156
|
};
|
|
1147
1157
|
const kotlinFunctionName = (fn) => fn.namedChildren.find((c) => c.type === 'simple_identifier')?.text ?? null;
|
|
1158
|
+
const kotlinTypeRequestMethods = (typeNode) => {
|
|
1159
|
+
const modifiers = typeNode.namedChildren.find((child) => child.type === 'modifiers');
|
|
1160
|
+
const mappings = (modifiers?.namedChildren ?? []).filter((annotation) => {
|
|
1161
|
+
if (annotation.type !== 'annotation')
|
|
1162
|
+
return false;
|
|
1163
|
+
return isClassLevelMappingAnnotation(kotlinAnnotationName(annotation) ?? '');
|
|
1164
|
+
});
|
|
1165
|
+
if (mappings.length === 0)
|
|
1166
|
+
return ['*'];
|
|
1167
|
+
if (mappings.length !== 1)
|
|
1168
|
+
return [];
|
|
1169
|
+
const mapping = mappings[0];
|
|
1170
|
+
const mappingName = kotlinAnnotationName(mapping);
|
|
1171
|
+
if (!mappingName)
|
|
1172
|
+
return [];
|
|
1173
|
+
return springAnnotationHttpMethods(mappingName, mapping.text);
|
|
1174
|
+
};
|
|
1175
|
+
const kotlinClassHttpMethodsById = (tree) => new Map(tree.rootNode
|
|
1176
|
+
.descendantsOfType('class_declaration')
|
|
1177
|
+
.map((typeNode) => [typeNode.id, kotlinTypeRequestMethods(typeNode)]));
|
|
1148
1178
|
const collectKotlinSpringTypes = (filePath, tree) => {
|
|
1149
1179
|
// Class-level @RequestMapping prefixes (reuse the provider class-prefix query).
|
|
1150
1180
|
const prefixByClassId = new Map();
|
|
1151
1181
|
for (const match of runCompiledPatterns(SPRING_CLASS_PREFIX_PATTERNS, tree)) {
|
|
1152
1182
|
const prefixNode = match.captures.prefix;
|
|
1153
1183
|
const classNode = match.captures.class;
|
|
1184
|
+
const annNode = match.captures.ann;
|
|
1154
1185
|
if (!prefixNode || !classNode)
|
|
1155
1186
|
continue;
|
|
1187
|
+
if (annNode && !isClassLevelMappingAnnotation(annNode.text))
|
|
1188
|
+
continue;
|
|
1156
1189
|
// An INTERPOLATED literal (`"${ApiPaths.BASE}"`) is not a path — unquoting
|
|
1157
1190
|
// its raw text would carry the source spelling into the shared type view
|
|
1158
1191
|
// as a served prefix. Refusing it here is also what lets the unfoldable
|
|
@@ -1172,6 +1205,7 @@ function buildKotlinPlugin(language) {
|
|
|
1172
1205
|
// noise into the shared type view, so it is left out — the same skip floor
|
|
1173
1206
|
// `java.ts`'s `collectSpringTypes` keeps.
|
|
1174
1207
|
const routesByMethodId = new Map();
|
|
1208
|
+
const classHttpMethodsById = kotlinClassHttpMethodsById(tree);
|
|
1175
1209
|
const unfoldablePrefixClassIds = collectUnfoldablePrefixClassIds(tree, prefixByClassId);
|
|
1176
1210
|
for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
|
|
1177
1211
|
const annNode = match.captures.ann;
|
|
@@ -1179,8 +1213,8 @@ function buildKotlinPlugin(language) {
|
|
|
1179
1213
|
const methodNode = match.captures.method;
|
|
1180
1214
|
if (!annNode || !pathNode || !methodNode)
|
|
1181
1215
|
continue;
|
|
1182
|
-
const
|
|
1183
|
-
if (
|
|
1216
|
+
const httpMethods = springAnnotationHttpMethods(annNode.text, enclosingAnnotationText(annNode));
|
|
1217
|
+
if (httpMethods.length === 0)
|
|
1184
1218
|
continue;
|
|
1185
1219
|
const rawPath = unquoteLiteral(pathNode.text);
|
|
1186
1220
|
if (rawPath === null)
|
|
@@ -1190,8 +1224,11 @@ function buildKotlinPlugin(language) {
|
|
|
1190
1224
|
const owner = findEnclosingClass(methodNode);
|
|
1191
1225
|
if (owner && unfoldablePrefixClassIds.has(owner.id))
|
|
1192
1226
|
continue;
|
|
1227
|
+
const constrainedMethods = intersectSpringHttpMethods(owner ? (classHttpMethodsById.get(owner.id) ?? ['*']) : ['*'], httpMethods);
|
|
1193
1228
|
const arr = routesByMethodId.get(methodNode.id) ?? [];
|
|
1194
|
-
|
|
1229
|
+
for (const httpMethod of constrainedMethods) {
|
|
1230
|
+
arr.push({ method: httpMethod, path: rawPath });
|
|
1231
|
+
}
|
|
1195
1232
|
routesByMethodId.set(methodNode.id, arr);
|
|
1196
1233
|
}
|
|
1197
1234
|
const out = [];
|
|
@@ -1319,8 +1356,11 @@ function buildKotlinPlugin(language) {
|
|
|
1319
1356
|
for (const match of runCompiledPatterns(SPRING_CLASS_PREFIX_PATTERNS, tree)) {
|
|
1320
1357
|
const prefixNode = match.captures.prefix;
|
|
1321
1358
|
const classNode = match.captures.class;
|
|
1359
|
+
const annNode = match.captures.ann;
|
|
1322
1360
|
if (!prefixNode || !classNode)
|
|
1323
1361
|
continue;
|
|
1362
|
+
if (annNode && !isClassLevelMappingAnnotation(annNode.text))
|
|
1363
|
+
continue;
|
|
1324
1364
|
// An INTERPOLATED literal (`"${ApiPaths.BASE}"`) is not a path — see
|
|
1325
1365
|
// `isPlainStringLiteral`. Refusing it here also lets the unfoldable
|
|
1326
1366
|
// analysis below mark such a class, since that skips classes whose
|
|
@@ -1366,24 +1406,27 @@ function buildKotlinPlugin(language) {
|
|
|
1366
1406
|
// Literal and constant-valued paths are normalized into one candidate list
|
|
1367
1407
|
// so both reach the same Feign/interface/prefix classification below.
|
|
1368
1408
|
const methodRoutes = [];
|
|
1409
|
+
const classHttpMethodsById = kotlinClassHttpMethodsById(tree);
|
|
1369
1410
|
for (const match of runCompiledPatterns(SPRING_METHOD_ROUTE_PATTERNS, tree)) {
|
|
1370
1411
|
const annNode = match.captures.ann;
|
|
1371
1412
|
const pathNode = match.captures.path;
|
|
1372
1413
|
const methodNode = match.captures.method;
|
|
1373
1414
|
if (!annNode || !pathNode || !methodNode)
|
|
1374
1415
|
continue;
|
|
1375
|
-
const
|
|
1376
|
-
if (
|
|
1416
|
+
const httpMethods = springAnnotationHttpMethods(annNode.text, enclosingAnnotationText(annNode));
|
|
1417
|
+
if (httpMethods.length === 0)
|
|
1377
1418
|
continue;
|
|
1378
1419
|
const rawPath = unquoteLiteral(pathNode.text);
|
|
1379
1420
|
if (rawPath === null)
|
|
1380
1421
|
continue;
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1422
|
+
for (const httpMethod of httpMethods) {
|
|
1423
|
+
methodRoutes.push({
|
|
1424
|
+
httpMethod,
|
|
1425
|
+
rawPath,
|
|
1426
|
+
nameNode: match.captures.method_name,
|
|
1427
|
+
methodNode,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1387
1430
|
}
|
|
1388
1431
|
for (const match of runCompiledPatterns(SPRING_CONST_METHOD_ROUTE_PATTERNS, tree)) {
|
|
1389
1432
|
const annNode = match.captures.ann;
|
|
@@ -1391,8 +1434,8 @@ function buildKotlinPlugin(language) {
|
|
|
1391
1434
|
const methodNode = match.captures.method;
|
|
1392
1435
|
if (!annNode || !argNode || !methodNode)
|
|
1393
1436
|
continue;
|
|
1394
|
-
const
|
|
1395
|
-
if (
|
|
1437
|
+
const httpMethods = springAnnotationHttpMethods(annNode.text, enclosingAnnotationText(annNode));
|
|
1438
|
+
if (httpMethods.length === 0)
|
|
1396
1439
|
continue;
|
|
1397
1440
|
const expr = kotlinRouteArgumentExpression(argNode);
|
|
1398
1441
|
if (!expr || !FOLDABLE_PATH_EXPRESSIONS.has(expr.type))
|
|
@@ -1413,14 +1456,24 @@ function buildKotlinPlugin(language) {
|
|
|
1413
1456
|
const rawPath = foldKotlinOperands(fileKey, operands, index.repo, kotlinEnclosingTypeNames(methodNode), index);
|
|
1414
1457
|
if (rawPath === null)
|
|
1415
1458
|
continue;
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1459
|
+
for (const httpMethod of httpMethods) {
|
|
1460
|
+
methodRoutes.push({
|
|
1461
|
+
httpMethod,
|
|
1462
|
+
rawPath,
|
|
1463
|
+
nameNode: match.captures.method_name,
|
|
1464
|
+
methodNode,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1422
1467
|
}
|
|
1423
|
-
|
|
1468
|
+
const constrainedMethodRoutes = methodRoutes.flatMap((route) => {
|
|
1469
|
+
const owner = findEnclosingClass(route.methodNode);
|
|
1470
|
+
const classMethods = owner ? (classHttpMethodsById.get(owner.id) ?? ['*']) : ['*'];
|
|
1471
|
+
return intersectSpringHttpMethods(classMethods, [route.httpMethod]).map((httpMethod) => ({
|
|
1472
|
+
...route,
|
|
1473
|
+
httpMethod,
|
|
1474
|
+
}));
|
|
1475
|
+
});
|
|
1476
|
+
for (const { httpMethod, rawPath, nameNode, methodNode } of constrainedMethodRoutes) {
|
|
1424
1477
|
const enclosingClass = findEnclosingClass(methodNode);
|
|
1425
1478
|
// A @(Get|...)Mapping inside a @FeignClient interface is an OpenFeign
|
|
1426
1479
|
// consumer (a remote call), not a route this service serves.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read AsyncAPI 3.x documents off disk and normalize their operations into
|
|
3
|
+
* broker addresses.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately OUTSIDE `frameworks/spring/`. An AsyncAPI document is a
|
|
6
|
+
* published artifact, not a Spring one: it is emitted by generators across
|
|
7
|
+
* Java, Kotlin, TypeScript, Go and Python toolchains, and it is written by hand
|
|
8
|
+
* as often as it is generated. The entry criterion here is therefore the
|
|
9
|
+
* DOCUMENT FORMAT — a root `asyncapi` key — and never the generator. Nothing in
|
|
10
|
+
* this module may branch on `x-generator`, on a vendor extension, or on the
|
|
11
|
+
* shape of an operation key: the moment it does, every service whose toolchain
|
|
12
|
+
* spells things differently stops being read, and the failure is silent.
|
|
13
|
+
*
|
|
14
|
+
* ── WHY THIS IS WORTH READING AT ALL ──────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* A `@KafkaListener(topics = "${app.topic.in}")` names a configuration key, not
|
|
17
|
+
* an address, and the address cascade correctly refuses to resolve it — two
|
|
18
|
+
* services that merely wrote the same placeholder have said nothing about each
|
|
19
|
+
* other. But the service's own published document states the address outright,
|
|
20
|
+
* fully resolved, because the generator ran with the configuration applied.
|
|
21
|
+
* That is a fact about the service that no amount of reading its source can
|
|
22
|
+
* recover.
|
|
23
|
+
*
|
|
24
|
+
* ── WHAT THIS MODULE IS AFRAID OF ─────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* Everything it emits becomes half of a JOIN KEY. A destination minted here
|
|
27
|
+
* meets every other site in every other repository that names the same address
|
|
28
|
+
* on the same broker — that is the whole value, and it is the whole hazard. A
|
|
29
|
+
* missing destination is a visible gap; a wrong one is reported as a fact. So
|
|
30
|
+
* the refusals below are not defensive clutter: each one is a case where the
|
|
31
|
+
* document says something that LOOKS like an address and is not one, and where
|
|
32
|
+
* accepting it would connect two services that have said nothing about each
|
|
33
|
+
* other. The taxonomy is closed and countable for the same reason the source
|
|
34
|
+
* cascade's is — a feature judged on its unresolved fraction needs the fraction
|
|
35
|
+
* broken down by cause, or nobody can tell it what to go and fix.
|
|
36
|
+
*
|
|
37
|
+
* ── VERSION 2.x IS REFUSED, NOT MAPPED ────────────────────────────────────
|
|
38
|
+
*
|
|
39
|
+
* AsyncAPI 2.x describes a channel from the READER's point of view: `publish`
|
|
40
|
+
* means "you may publish here", so the documenting application RECEIVES, and
|
|
41
|
+
* `subscribe` means the application SENDS. Version 3.0 renamed these to the
|
|
42
|
+
* application's own `receive` / `send`. Mapping 2.x naively therefore reverses
|
|
43
|
+
* every direction in the async graph — and reverses it INVISIBLY, because both
|
|
44
|
+
* roles still exist, every edge is still emitted, and the graph stays
|
|
45
|
+
* connected. Nothing fails; the arrows simply point the wrong way.
|
|
46
|
+
*
|
|
47
|
+
* The inversion is one line to write and impossible to test against a real
|
|
48
|
+
* corpus we do not have, and the 2.x wording confused implementers badly enough
|
|
49
|
+
* that some generators emitted it backwards. So 2.x is refused under its own
|
|
50
|
+
* countable reason instead. A silent skip would be indistinguishable from "this
|
|
51
|
+
* service publishes no document", which is the one thing the count has to be
|
|
52
|
+
* able to tell us: if the refusal tally shows 2.x documents in the field, the
|
|
53
|
+
* inversion earns its way in with evidence behind it.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Why a document, or one operation inside it, produced no address.
|
|
57
|
+
*
|
|
58
|
+
* A CLOSED, COUNTABLE set, and deliberately NOT `SpringDestinationRefusal`.
|
|
59
|
+
* That union is documented as the reasons a *source-level candidate* produced
|
|
60
|
+
* no address, and it is the denominator of the unresolved fraction the address
|
|
61
|
+
* work is judged on. Folding document-level failures into it would silently
|
|
62
|
+
* change what that number means — a repository whose specification directory
|
|
63
|
+
* was mistyped would report a worse SOURCE, which is the opposite of the truth.
|
|
64
|
+
*
|
|
65
|
+
* Members are split wherever two causes are different FACTS about the input.
|
|
66
|
+
* A tally whose member says "the document contradicts itself" when the document
|
|
67
|
+
* is merely multi-protocol sends an operator to fix the wrong thing, and this
|
|
68
|
+
* tally is the number the whole feature is judged on.
|
|
69
|
+
*/
|
|
70
|
+
export type AsyncApiRefusal =
|
|
71
|
+
/** The file parsed but has no root `asyncapi` key: not a document at all. */
|
|
72
|
+
'not-a-document'
|
|
73
|
+
/** Root `asyncapi: 2.x`. See the header — refused, never mapped. */
|
|
74
|
+
| 'asyncapi-2-unsupported'
|
|
75
|
+
/** A root `asyncapi` key naming a version this module does not read. */
|
|
76
|
+
| 'unsupported-version'
|
|
77
|
+
/** Malformed YAML/JSON, or a root that is not an object. */
|
|
78
|
+
| 'unparsable'
|
|
79
|
+
/** The file could not be read, or is not a regular file (a FIFO, a device). */
|
|
80
|
+
| 'unreadable'
|
|
81
|
+
/** A subdirectory could not be listed. Counted rather than skipped: under a
|
|
82
|
+
* mixed-permission cache half the documents can be invisible while the run
|
|
83
|
+
* otherwise reports a clean, complete read. */
|
|
84
|
+
| 'directory-unreadable'
|
|
85
|
+
/** Larger than {@link MAX_DOCUMENT_BYTES}. */
|
|
86
|
+
| 'oversized'
|
|
87
|
+
/** The document held more operations than one run will examine. */
|
|
88
|
+
| 'operation-cap'
|
|
89
|
+
/** The run as a whole reached {@link MAX_TOTAL_OPERATIONS}. */
|
|
90
|
+
| 'total-operation-cap'
|
|
91
|
+
/** The document declares more servers than the channel-inheritance rule will
|
|
92
|
+
* read. */
|
|
93
|
+
| 'server-cap'
|
|
94
|
+
/** The walk hit a bound before it finished, so the document set is a floor
|
|
95
|
+
* rather than the whole of what the configured path holds. A truncated read
|
|
96
|
+
* that reported nothing would be indistinguishable from a complete one. */
|
|
97
|
+
| 'walk-truncated'
|
|
98
|
+
/** `operations[].channel.$ref` is absent or not a local channel pointer. */
|
|
99
|
+
| 'no-channel-reference'
|
|
100
|
+
/** The `$ref` resolved to no channel in this document. */
|
|
101
|
+
| 'channel-not-found'
|
|
102
|
+
/** The channel entry is itself a Reference Object, which this module does not
|
|
103
|
+
* follow. Distinct from `no-address` on purpose: a `$ref`-ed channel HAS an
|
|
104
|
+
* address, somewhere this reader did not look, and filing it under
|
|
105
|
+
* `no-address` tells an operator their documents omit addresses when the
|
|
106
|
+
* real answer is that the reader stops one hop short. */
|
|
107
|
+
| 'unresolved-channel-reference'
|
|
108
|
+
/** The channel names no `address`, so there is nothing to key on. */
|
|
109
|
+
| 'no-address'
|
|
110
|
+
/**
|
|
111
|
+
* The address is a TEMPLATE, not an address: the channel declares non-empty
|
|
112
|
+
* `parameters`, or the address carries a `{…}` placeholder.
|
|
113
|
+
*
|
|
114
|
+
* This is the document-side twin of the source cascade's
|
|
115
|
+
* `overridable-config-default`, and it exists for the identical reason. Two
|
|
116
|
+
* services that both publish `{env}.orders` have named a pattern they share,
|
|
117
|
+
* not a queue they share — one deploys with `env=prod` and the other with
|
|
118
|
+
* `env=staging`, and keying on the template text merges them into a single
|
|
119
|
+
* node with a publisher on one side and a subscriber on the other. That is a
|
|
120
|
+
* false connection built entirely from conformant AsyncAPI: `parameters` and
|
|
121
|
+
* `{param}` are core 3.x vocabulary, not a vendor quirk.
|
|
122
|
+
*/
|
|
123
|
+
| 'templated-address'
|
|
124
|
+
/** Longer than {@link MAX_ADDRESS_LENGTH}. */
|
|
125
|
+
| 'address-too-long'
|
|
126
|
+
/** Longer than {@link MAX_OPERATION_ID_LENGTH}. */
|
|
127
|
+
| 'operation-id-too-long'
|
|
128
|
+
/** `action` is neither `send` nor `receive`. */
|
|
129
|
+
| 'unrecognized-action'
|
|
130
|
+
/** Neither the operation's bindings nor the servers its channel resolves to
|
|
131
|
+
* name a protocol. Silence about the broker is not a claim about it, but a
|
|
132
|
+
* `Destination` cannot be keyed without one. */
|
|
133
|
+
| 'protocol-unknown'
|
|
134
|
+
/** The operation's OWN two statements about its broker — its bindings and the
|
|
135
|
+
* servers its channel explicitly lists — name different brokers. The
|
|
136
|
+
* document contradicts itself, and a destination keyed on the wrong broker
|
|
137
|
+
* joins a stranger. */
|
|
138
|
+
| 'protocol-disagreement'
|
|
139
|
+
/** The channel lists no `servers`, so it inherits all of them, and they do
|
|
140
|
+
* not agree on one broker. The document does NOT contradict itself here —
|
|
141
|
+
* it is simply multi-protocol and this channel did not choose — which is why
|
|
142
|
+
* this is not `protocol-disagreement`. */
|
|
143
|
+
| 'ambiguous-server-default'
|
|
144
|
+
/**
|
|
145
|
+
* The channel inherits the document's servers, but that map was CAPPED at
|
|
146
|
+
* {@link MAX_SERVERS_PER_DOCUMENT}, so the brokers read are a subset.
|
|
147
|
+
*
|
|
148
|
+
* Distinct from `ambiguous-server-default`, and the distinction is the whole
|
|
149
|
+
* point: unanimity across a subset is not unanimity. A document whose first
|
|
150
|
+
* thousand servers are Kafka and whose thousand-and-first is JMS reads as
|
|
151
|
+
* unanimously Kafka, and every operation inheriting it would be attributed to
|
|
152
|
+
* a broker the complete set does not agree on.
|
|
153
|
+
*/
|
|
154
|
+
| 'capped-server-default'
|
|
155
|
+
/**
|
|
156
|
+
* A server this operation depends on is a Reference Object this reader could
|
|
157
|
+
* not resolve — a pointer outside `#/servers` and `#/components/servers`, a
|
|
158
|
+
* name that is absent, or a reference to another reference.
|
|
159
|
+
*
|
|
160
|
+
* Refused rather than skipped. Skipping one server of several silently
|
|
161
|
+
* narrows the evidence, and a narrowed set is what makes a mixed document
|
|
162
|
+
* look like it agrees with itself.
|
|
163
|
+
*/
|
|
164
|
+
| 'unresolved-server-reference'
|
|
165
|
+
/** The broker is HTTP or WebSocket, where the host rather than the address is
|
|
166
|
+
* the namespace. See `isNonDestinationBroker`. */
|
|
167
|
+
| 'not-a-destination-protocol';
|
|
168
|
+
export interface AsyncApiOperation {
|
|
169
|
+
/** Absolute path of the document this operation came from. */
|
|
170
|
+
readonly documentPath: string;
|
|
171
|
+
/** The `operations` map key, kept for provenance and carried into the edge
|
|
172
|
+
* `reason` so a reader can find the operation the edge came from. */
|
|
173
|
+
readonly operationId: string;
|
|
174
|
+
readonly action: 'send' | 'receive';
|
|
175
|
+
readonly address: string;
|
|
176
|
+
/** Normalized broker — the first half of the `Destination` key. */
|
|
177
|
+
readonly broker: string;
|
|
178
|
+
}
|
|
179
|
+
export interface AsyncApiReadResult {
|
|
180
|
+
readonly operations: readonly AsyncApiOperation[];
|
|
181
|
+
/** Files considered — every candidate extension under the configured path. */
|
|
182
|
+
readonly documentsScanned: number;
|
|
183
|
+
/** Files that parsed as an AsyncAPI 3.x document and yielded an operation. */
|
|
184
|
+
readonly documentsAccepted: number;
|
|
185
|
+
/** Entries skipped because they were symbolic links. Not a refusal — the skip
|
|
186
|
+
* is deliberate — but counted, because a cache written by other tooling is
|
|
187
|
+
* very often a symlink farm, and an operator whose whole cache was skipped
|
|
188
|
+
* would otherwise see a result identical to a wrong path. */
|
|
189
|
+
readonly symlinksSkipped: number;
|
|
190
|
+
/** True when a bound stopped the walk or the operation count, so every number
|
|
191
|
+
* here is a floor rather than a total. */
|
|
192
|
+
readonly truncated: boolean;
|
|
193
|
+
/** Every refusal, document-level and operation-level, by reason. */
|
|
194
|
+
readonly refusals: Readonly<Partial<Record<AsyncApiRefusal, number>>>;
|
|
195
|
+
}
|
|
196
|
+
export interface NormalizedDocument {
|
|
197
|
+
operations: AsyncApiOperation[];
|
|
198
|
+
refusals: Partial<Record<AsyncApiRefusal, number>>;
|
|
199
|
+
/** Operations EXAMINED, which is what the caps count. */
|
|
200
|
+
examined: number;
|
|
201
|
+
/** A bound stopped this document short. */
|
|
202
|
+
truncated: boolean;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Normalize one parsed document. Pure — no filesystem, so the whole refusal
|
|
206
|
+
* surface is testable from inline document literals.
|
|
207
|
+
*
|
|
208
|
+
* `budget` is the number of operations the RUN may still examine.
|
|
209
|
+
*/
|
|
210
|
+
export declare function normalizeAsyncApiDocument(parsed: unknown, documentPath: string, budget?: number): NormalizedDocument;
|
|
211
|
+
/**
|
|
212
|
+
* Read every AsyncAPI 3.x document under an explicitly configured path.
|
|
213
|
+
*
|
|
214
|
+
* `configuredPath` is resolved against the repository root, so an absolute path
|
|
215
|
+
* to a cache populated out of band and a repo-relative directory of committed
|
|
216
|
+
* documents are both natural — the same shape `springActuatorPath` offers for
|
|
217
|
+
* Actuator snapshots. The READ is wider than that neighbour's, and the
|
|
218
|
+
* difference is worth stating rather than glossed as "the same contract": the
|
|
219
|
+
* Actuator loader probes five fixed filenames in one directory, while this
|
|
220
|
+
* walks recursively under the caps above and opens every candidate it finds.
|
|
221
|
+
*
|
|
222
|
+
* There is deliberately NO glob-based auto-discovery. Scanning a repository for
|
|
223
|
+
* anything that parses as a document would make every existing index grow nodes
|
|
224
|
+
* on its next run with nobody having asked for it.
|
|
225
|
+
*/
|
|
226
|
+
export declare function readAsyncApiDocuments(repoPath: string, configuredPath: string): Promise<AsyncApiReadResult>;
|