@autoview/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +407 -0
- package/lib/AutoViewAgent.d.ts +109 -0
- package/lib/AutoViewAgent.js +123 -0
- package/lib/AutoViewAgent.js.map +1 -0
- package/lib/agent/emitMcpServer.d.ts +15 -0
- package/lib/agent/emitMcpServer.js +157 -0
- package/lib/agent/emitMcpServer.js.map +1 -0
- package/lib/agent/emitReport.d.ts +14 -0
- package/lib/agent/emitReport.js +85 -0
- package/lib/agent/emitReport.js.map +1 -0
- package/lib/agent/toolSurface.d.ts +130 -0
- package/lib/agent/toolSurface.js +342 -0
- package/lib/agent/toolSurface.js.map +1 -0
- package/lib/agent/verifyAgentTasks.d.ts +87 -0
- package/lib/agent/verifyAgentTasks.js +126 -0
- package/lib/agent/verifyAgentTasks.js.map +1 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.js +295 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
- package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
- package/lib/constants/AutoViewFrontendTemplate.js +46 -0
- package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
- package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
- package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
- package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
- package/lib/context/IAutoViewAgentContext.d.ts +60 -0
- package/lib/context/IAutoViewAgentContext.js +3 -0
- package/lib/context/IAutoViewAgentContext.js.map +1 -0
- package/lib/fromSwagger.d.ts +53 -0
- package/lib/fromSwagger.js +513 -0
- package/lib/fromSwagger.js.map +1 -0
- package/lib/generateDeterministic.d.ts +26 -0
- package/lib/generateDeterministic.js +75 -0
- package/lib/generateDeterministic.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +41 -0
- package/lib/index.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
- package/lib/orchestrate/orchestrateAutoView.js +491 -0
- package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
- package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
- package/lib/orchestrate/utils/HistoryMessage.js +25 -0
- package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
- package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
- package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
- package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
- package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
- package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
- package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
- package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
- package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
- package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
- package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
- package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
- package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
- package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
- package/lib/preview/renderPreview.d.ts +22 -0
- package/lib/preview/renderPreview.js +198 -0
- package/lib/preview/renderPreview.js.map +1 -0
- package/lib/typings/compiler.d.ts +39 -0
- package/lib/typings/compiler.js +3 -0
- package/lib/typings/compiler.js.map +1 -0
- package/lib/typings/events.d.ts +106 -0
- package/lib/typings/events.js +3 -0
- package/lib/typings/events.js.map +1 -0
- package/lib/typings/index.d.ts +10 -0
- package/lib/typings/index.js +27 -0
- package/lib/typings/index.js.map +1 -0
- package/lib/typings/misc.d.ts +78 -0
- package/lib/typings/misc.js +3 -0
- package/lib/typings/misc.js.map +1 -0
- package/lib/utils/ArrayUtil.d.ts +8 -0
- package/lib/utils/ArrayUtil.js +30 -0
- package/lib/utils/ArrayUtil.js.map +1 -0
- package/lib/utils/StringUtil.d.ts +11 -0
- package/lib/utils/StringUtil.js +28 -0
- package/lib/utils/StringUtil.js.map +1 -0
- package/lib/utils/classifyEndpoints.d.ts +62 -0
- package/lib/utils/classifyEndpoints.js +216 -0
- package/lib/utils/classifyEndpoints.js.map +1 -0
- package/lib/utils/endpointFilter.d.ts +26 -0
- package/lib/utils/endpointFilter.js +0 -0
- package/lib/utils/endpointFilter.js.map +1 -0
- package/lib/utils/extractFields.d.ts +85 -0
- package/lib/utils/extractFields.js +231 -0
- package/lib/utils/extractFields.js.map +1 -0
- package/lib/utils/index.d.ts +13 -0
- package/lib/utils/index.js +30 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/normalizeForNestia.d.ts +34 -0
- package/lib/utils/normalizeForNestia.js +133 -0
- package/lib/utils/normalizeForNestia.js.map +1 -0
- package/lib/utils/resourcePlan.d.ts +39 -0
- package/lib/utils/resourcePlan.js +95 -0
- package/lib/utils/resourcePlan.js.map +1 -0
- package/lib/utils/sliceDocument.d.ts +17 -0
- package/lib/utils/sliceDocument.js +114 -0
- package/lib/utils/sliceDocument.js.map +1 -0
- package/lib/utils/toEndpoints.d.ts +90 -0
- package/lib/utils/toEndpoints.js +227 -0
- package/lib/utils/toEndpoints.js.map +1 -0
- package/lib/verify/runWorkflows.d.ts +25 -0
- package/lib/verify/runWorkflows.js +366 -0
- package/lib/verify/runWorkflows.js.map +1 -0
- package/lib/verify/workflows.d.ts +53 -0
- package/lib/verify/workflows.js +107 -0
- package/lib/verify/workflows.js.map +1 -0
- package/package.json +82 -0
- package/prompts/AUTOVIEW_RENDER.md +398 -0
- package/prompts/AUTOVIEW_REVIEW.md +60 -0
- package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
- package/src/AutoViewAgent.ts +222 -0
- package/src/agent/emitMcpServer.integration.test.ts +168 -0
- package/src/agent/emitMcpServer.test.ts +51 -0
- package/src/agent/emitMcpServer.ts +178 -0
- package/src/agent/emitReport.ts +117 -0
- package/src/agent/toolSurface.test.ts +243 -0
- package/src/agent/toolSurface.ts +501 -0
- package/src/agent/verifyAgentTasks.test.ts +106 -0
- package/src/agent/verifyAgentTasks.ts +171 -0
- package/src/cli/main.ts +363 -0
- package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
- package/src/constants/AutoViewFrontendTemplate.ts +42 -0
- package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
- package/src/context/IAutoViewAgentContext.ts +84 -0
- package/src/fromSwagger.test.ts +269 -0
- package/src/fromSwagger.ts +500 -0
- package/src/generateDeterministic.test.ts +39 -0
- package/src/generateDeterministic.ts +77 -0
- package/src/index.ts +30 -0
- package/src/orchestrate/orchestrateAutoView.ts +590 -0
- package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
- package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
- package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
- package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
- package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
- package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
- package/src/orchestrate/renderNavTs.test.ts +74 -0
- package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
- package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
- package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
- package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
- package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
- package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
- package/src/orchestrate/utils/HistoryMessage.ts +41 -0
- package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
- package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
- package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
- package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
- package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
- package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
- package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
- package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
- package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
- package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
- package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
- package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
- package/src/preview/renderPreview.ts +273 -0
- package/src/typings/compiler.ts +47 -0
- package/src/typings/events.ts +155 -0
- package/src/typings/index.ts +10 -0
- package/src/typings/misc.ts +93 -0
- package/src/utils/ArrayUtil.ts +16 -0
- package/src/utils/StringUtil.ts +29 -0
- package/src/utils/classifyEndpoints.test.ts +86 -0
- package/src/utils/classifyEndpoints.ts +291 -0
- package/src/utils/endpointFilter.test.ts +50 -0
- package/src/utils/endpointFilter.ts +0 -0
- package/src/utils/extractFields.test.ts +82 -0
- package/src/utils/extractFields.ts +306 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/normalizeForNestia.test.ts +93 -0
- package/src/utils/normalizeForNestia.ts +139 -0
- package/src/utils/resourcePlan.test.ts +104 -0
- package/src/utils/resourcePlan.ts +180 -0
- package/src/utils/sliceDocument.test.ts +85 -0
- package/src/utils/sliceDocument.ts +119 -0
- package/src/utils/toEndpoints.test.ts +251 -0
- package/src/utils/toEndpoints.ts +343 -0
- package/src/verify/runWorkflows.ts +403 -0
- package/src/verify/workflows.test.ts +117 -0
- package/src/verify/workflows.ts +154 -0
- package/template/CLAUDE.md +140 -0
- package/template/Dockerfile +31 -0
- package/template/PROMPT.md +80 -0
- package/template/SANDBOX.md +70 -0
- package/template/app/api/health/route.ts +10 -0
- package/template/app/globals.css +97 -0
- package/template/app/layout.tsx +30 -0
- package/template/app/page.tsx +19 -0
- package/template/components/AppShell.tsx +114 -0
- package/template/components/auto/CatalogGrid.tsx +159 -0
- package/template/components/auto/ConfirmButton.tsx +67 -0
- package/template/components/auto/EmbeddedCollection.tsx +144 -0
- package/template/components/auto/ResourceDashboard.tsx +104 -0
- package/template/components/auto/ResourceDetail.tsx +93 -0
- package/template/components/auto/ResourceForm.tsx +235 -0
- package/template/components/auto/ResourceIcon.tsx +88 -0
- package/template/components/auto/ResourceLanding.tsx +155 -0
- package/template/components/auto/ResourceTable.tsx +223 -0
- package/template/components/auto/formatValue.tsx +186 -0
- package/template/components/auto/types.ts +42 -0
- package/template/components/ui/badge.tsx +40 -0
- package/template/components/ui/button.tsx +57 -0
- package/template/components/ui/card.tsx +86 -0
- package/template/components/ui/dialog.tsx +119 -0
- package/template/components/ui/input.tsx +23 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/pagination.tsx +117 -0
- package/template/components/ui/select.tsx +92 -0
- package/template/components/ui/sheet.tsx +135 -0
- package/template/components/ui/skeleton.tsx +15 -0
- package/template/components/ui/table.tsx +120 -0
- package/template/components/ui/tabs.tsx +55 -0
- package/template/lib/utils.ts +35 -0
- package/template/next.config.mjs +52 -0
- package/template/package.json +46 -0
- package/template/postcss.config.js +6 -0
- package/template/scripts/start-shopping-backend.sh +56 -0
- package/template/tailwind.config.ts +96 -0
- package/template/tsconfig.json +29 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { OpenApi } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
classifyEndpoints,
|
|
5
|
+
EndpointRole,
|
|
6
|
+
} from "../utils/classifyEndpoints";
|
|
7
|
+
import {
|
|
8
|
+
COLLECTION_FIELD_NAMES,
|
|
9
|
+
extractFields,
|
|
10
|
+
findCollectionField,
|
|
11
|
+
resolveProperties,
|
|
12
|
+
} from "../utils/extractFields";
|
|
13
|
+
import { IAutoViewEndpoint, toEndpoints } from "../utils/toEndpoints";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Agent tool surface — the deterministic projection of the API's semantic model
|
|
17
|
+
* (IR) into a set of agent-callable tools (MCP / function-calling shape).
|
|
18
|
+
*
|
|
19
|
+
* The same IR that drives the human frontend drives this: resource roles
|
|
20
|
+
* (read vs write), the resource hierarchy, and the producer→consumer chain (which
|
|
21
|
+
* tool's output supplies a given tool's id input). A naive OpenAPI→tools dump
|
|
22
|
+
* has none of that — it emits a flat bag of operations. This emits tools that
|
|
23
|
+
* carry:
|
|
24
|
+
*
|
|
25
|
+
* - `annotations.readOnlyHint` / `destructiveHint` — safety, so an agent (or its
|
|
26
|
+
* guardrails) can tell a read from a mutation without parsing prose.
|
|
27
|
+
* - per-input producer hints — "this `saleId` comes from the `sales.index`
|
|
28
|
+
* tool's `id` field" — so the chain is explicit, not inferred.
|
|
29
|
+
* - only renderable operations (path fragments like `/x#y` that nestia cannot
|
|
30
|
+
* express as a typed call are dropped — `toEndpoints` already excludes them).
|
|
31
|
+
*
|
|
32
|
+
* Pure and deterministic: same document → same tool surface.
|
|
33
|
+
*/
|
|
34
|
+
export interface IAgentTool {
|
|
35
|
+
/** Dotted accessor, unique and a valid tool name (`shoppings.sales.index`). */
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
/** JSON Schema for the call arguments. */
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object";
|
|
41
|
+
properties: Record<string, unknown>;
|
|
42
|
+
required: string[];
|
|
43
|
+
};
|
|
44
|
+
/** MCP-style behavior hints. */
|
|
45
|
+
annotations: {
|
|
46
|
+
readOnlyHint: boolean;
|
|
47
|
+
destructiveHint: boolean;
|
|
48
|
+
idempotentHint: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** Execution descriptor — how an MCP server / executor invokes the real API. */
|
|
51
|
+
operation: {
|
|
52
|
+
method: string;
|
|
53
|
+
path: string;
|
|
54
|
+
pathParams: string[];
|
|
55
|
+
hasQuery: boolean;
|
|
56
|
+
hasBody: boolean;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Producer links: for each id-like path param, the tool + field that produces
|
|
60
|
+
* its value. Empty when the surface could not resolve a producer.
|
|
61
|
+
*/
|
|
62
|
+
producers: Array<{ param: string; tool: string; field: string }>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const READ_ROLES = new Set<EndpointRole>(["list", "search", "detail"]);
|
|
66
|
+
|
|
67
|
+
/** Field names that identify a record (the value a path param consumes). */
|
|
68
|
+
const ID_NAMES = ["id", "uuid"];
|
|
69
|
+
|
|
70
|
+
/** The first identifying field name present (`id` preferred over `uuid`), or null. */
|
|
71
|
+
const firstIdName = (names: Iterable<string>): string | null => {
|
|
72
|
+
const set = new Set([...names].map((n) => n.toLowerCase()));
|
|
73
|
+
return ID_NAMES.find((n) => set.has(n)) ?? null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a read endpoint's response element carries an identifying field
|
|
78
|
+
* (`id` / `uuid`) — i.e. whether it can PRODUCE an id for a consumer. Handles
|
|
79
|
+
* both named component responses and INLINE response schemas, and collection
|
|
80
|
+
* wrappers named generically (`data`/`entries`) OR after the resource
|
|
81
|
+
* (`{ databases: [...] }`, DigitalOcean), including inline array items.
|
|
82
|
+
*/
|
|
83
|
+
function producedIdField(
|
|
84
|
+
op: IAutoViewEndpoint,
|
|
85
|
+
doc: OpenApi.IDocument,
|
|
86
|
+
): string | null {
|
|
87
|
+
// Named-type path (nestia/AutoBE convention).
|
|
88
|
+
if (op.responseBody !== null) {
|
|
89
|
+
const elementFields = op.responseBody.isArray
|
|
90
|
+
? extractFields(op.responseBody.typeName, doc)
|
|
91
|
+
: (() => {
|
|
92
|
+
const top = extractFields(op.responseBody.typeName, doc);
|
|
93
|
+
// The collection field is either generically named (`data`/`entries`)
|
|
94
|
+
// or named after the resource (`{ namespaces: [...] }`, common on
|
|
95
|
+
// DigitalOcean once nestia promotes the inline response to a named
|
|
96
|
+
// type). Fall back to the first array field so the latter is found —
|
|
97
|
+
// producedIdField only runs on list/search reads, so an array IS the
|
|
98
|
+
// collection.
|
|
99
|
+
const col =
|
|
100
|
+
findCollectionField(top) ??
|
|
101
|
+
top.find((f) => f.kind === "array" && f.ref !== undefined);
|
|
102
|
+
return col?.ref !== undefined ? extractFields(col.ref, doc) : top;
|
|
103
|
+
})();
|
|
104
|
+
const named = firstIdName(elementFields.map((f) => f.name));
|
|
105
|
+
if (named !== null) return named;
|
|
106
|
+
// else: the promoted type was a dead/empty ref (nestia emits a `*.GetResponse`
|
|
107
|
+
// not registered in components, just like `*.GetQuery`) — fall through to the
|
|
108
|
+
// raw inline response schema, which still carries the real array.
|
|
109
|
+
}
|
|
110
|
+
// Inline response schema (DigitalOcean): introspect the raw schema.
|
|
111
|
+
if (op.responseSchema === null) return null;
|
|
112
|
+
const { properties } = resolveProperties(op.responseSchema, doc);
|
|
113
|
+
// The collection array: a known-named one, else the sole/first array property.
|
|
114
|
+
const arrayEntries = Object.entries(properties).filter(
|
|
115
|
+
([, p]) => (p as { type?: unknown }).type === "array",
|
|
116
|
+
);
|
|
117
|
+
const collection =
|
|
118
|
+
arrayEntries.find(([n]) => COLLECTION_FIELD_NAMES.has(n.toLowerCase())) ??
|
|
119
|
+
arrayEntries[0];
|
|
120
|
+
if (collection !== undefined) {
|
|
121
|
+
const items = (collection[1] as { items?: OpenApi.IJsonSchema }).items;
|
|
122
|
+
if (items !== undefined) {
|
|
123
|
+
return firstIdName(Object.keys(resolveProperties(items, doc).properties));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// A single object (detail) returning an inline schema.
|
|
127
|
+
return firstIdName(Object.keys(properties));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Roles whose response is a browsable collection — the only legitimate id
|
|
131
|
+
* PRODUCERS. A `detail` read returns one object but needs the id to be called,
|
|
132
|
+
* so it cannot produce that id (circular); excluding it keeps navigability honest. */
|
|
133
|
+
const COLLECTION_ROLES = new Set<EndpointRole>(["list", "search"]);
|
|
134
|
+
|
|
135
|
+
interface IProducer {
|
|
136
|
+
tool: string;
|
|
137
|
+
field: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The shared producer model used by BOTH the tool surface and the consumability
|
|
142
|
+
* report, so they never disagree on what is navigable. For each resource, the
|
|
143
|
+
* collection read (list/search) whose element exposes an id — keyed by the
|
|
144
|
+
* endpoint's CLASSIFIED resource (so a verb path like `/pet/findByStatus` is
|
|
145
|
+
* bucketed under `pet`, not `findByStatus`).
|
|
146
|
+
*/
|
|
147
|
+
function producerIndex(document: OpenApi.IDocument): {
|
|
148
|
+
endpoints: IAutoViewEndpoint[];
|
|
149
|
+
roleOf: Map<IAutoViewEndpoint, EndpointRole>;
|
|
150
|
+
producerOf: Map<string, IProducer>;
|
|
151
|
+
readableResources: Set<string>;
|
|
152
|
+
nested: Map<string, INestedProducer>;
|
|
153
|
+
} {
|
|
154
|
+
const endpoints = toEndpoints(document);
|
|
155
|
+
const roleOf = new Map<IAutoViewEndpoint, EndpointRole>();
|
|
156
|
+
const resourceOf = new Map<IAutoViewEndpoint, string>();
|
|
157
|
+
for (const g of classifyEndpoints(endpoints, document)) {
|
|
158
|
+
for (const c of g.endpoints) {
|
|
159
|
+
roleOf.set(c.endpoint, c.role);
|
|
160
|
+
resourceOf.set(c.endpoint, c.chain.at(-1)?.name ?? c.resource);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const producerOf = new Map<string, IProducer>();
|
|
164
|
+
const readableResources = new Set<string>();
|
|
165
|
+
for (const e of endpoints) {
|
|
166
|
+
const role = roleOf.get(e);
|
|
167
|
+
const resource = resourceOf.get(e);
|
|
168
|
+
if (role === undefined || resource === undefined) continue;
|
|
169
|
+
if (READ_ROLES.has(role)) readableResources.add(resource);
|
|
170
|
+
if (!COLLECTION_ROLES.has(role)) continue;
|
|
171
|
+
// A genuine producer ENUMERATES the resource. One that is itself scoped by
|
|
172
|
+
// that resource's own id (`/files/{file_id}/trash` → resource `files`, but
|
|
173
|
+
// it requires `file_id`) cannot produce that id — it is circular, so skip it.
|
|
174
|
+
const selfScoped = e.parameters.some(
|
|
175
|
+
(p) => resourceOfParam(e.path, p.name) === resource,
|
|
176
|
+
);
|
|
177
|
+
if (selfScoped) continue;
|
|
178
|
+
const field = producedIdField(e, document);
|
|
179
|
+
if (field !== null && !producerOf.has(resource)) {
|
|
180
|
+
producerOf.set(resource, { tool: e.accessor.join("."), field });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const nested = nestedProducers(endpoints, document, roleOf);
|
|
184
|
+
return { endpoints, roleOf, producerOf, readableResources, nested };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The named element type of a read endpoint's response (for recursion). */
|
|
188
|
+
function responseElementTypeName(
|
|
189
|
+
op: IAutoViewEndpoint,
|
|
190
|
+
doc: OpenApi.IDocument,
|
|
191
|
+
): string | null {
|
|
192
|
+
if (op.responseBody === null) return null;
|
|
193
|
+
if (op.responseBody.isArray) return op.responseBody.typeName;
|
|
194
|
+
const col = findCollectionField(extractFields(op.responseBody.typeName, doc));
|
|
195
|
+
return col?.ref ?? op.responseBody.typeName;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Where a nested id is obtainable: a read tool, the array path to it, and its id field. */
|
|
199
|
+
interface INestedProducer {
|
|
200
|
+
tool: string;
|
|
201
|
+
/** Nested access path to the id-bearing element, e.g. `units[].stocks[]`. */
|
|
202
|
+
path: string;
|
|
203
|
+
field: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resources whose ids are obtainable NESTED inside a read response, not from a
|
|
208
|
+
* dedicated endpoint — `order` detail returns `goods: IShoppingOrderGood[]`,
|
|
209
|
+
* `sale` returns `units: IShoppingSaleUnit[]` (each unit holds `stocks: […]`).
|
|
210
|
+
* Walks each read's response type graph (depth-bounded) and, for every
|
|
211
|
+
* array-field whose element carries an id, records WHICH read tool exposes it
|
|
212
|
+
* and the nested path — so the tool surface can hand the agent a real producer
|
|
213
|
+
* hint ("stockId ← sale.get, at units[].stocks[].id"), not just say it exists.
|
|
214
|
+
*/
|
|
215
|
+
function nestedProducers(
|
|
216
|
+
endpoints: IAutoViewEndpoint[],
|
|
217
|
+
doc: OpenApi.IDocument,
|
|
218
|
+
roleOf: Map<IAutoViewEndpoint, EndpointRole>,
|
|
219
|
+
): Map<string, INestedProducer> {
|
|
220
|
+
const out = new Map<string, INestedProducer>();
|
|
221
|
+
const visit = (
|
|
222
|
+
typeName: string,
|
|
223
|
+
tool: string,
|
|
224
|
+
parts: string[],
|
|
225
|
+
depth: number,
|
|
226
|
+
seen: Set<string>,
|
|
227
|
+
): void => {
|
|
228
|
+
if (depth > 3 || seen.has(typeName)) return;
|
|
229
|
+
seen.add(typeName);
|
|
230
|
+
for (const f of extractFields(typeName, doc)) {
|
|
231
|
+
if (f.kind === "array" && f.ref !== undefined) {
|
|
232
|
+
const childParts = [...parts, f.name];
|
|
233
|
+
const idField = firstIdName(
|
|
234
|
+
extractFields(f.ref, doc).map((x) => x.name),
|
|
235
|
+
);
|
|
236
|
+
const key = f.name.toLowerCase();
|
|
237
|
+
if (idField !== null) {
|
|
238
|
+
// Prefer the SHALLOWEST path (fewest array hops) — `sale → units[]`
|
|
239
|
+
// beats `order → goods[].units[]` for the same `units` id, so the
|
|
240
|
+
// agent is pointed at the most direct producer.
|
|
241
|
+
const existing = out.get(key);
|
|
242
|
+
if (existing === undefined || childParts.length < existing.path.split(".").length) {
|
|
243
|
+
out.set(key, {
|
|
244
|
+
tool,
|
|
245
|
+
path: childParts.map((p) => `${p}[]`).join("."),
|
|
246
|
+
field: idField,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
visit(f.ref, tool, childParts, depth + 1, seen); // recurse: units → stocks
|
|
251
|
+
} else if (f.kind === "ref" && f.ref !== undefined) {
|
|
252
|
+
visit(f.ref, tool, parts, depth + 1, seen);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
for (const e of endpoints) {
|
|
257
|
+
const role = roleOf.get(e);
|
|
258
|
+
if (role === undefined || !READ_ROLES.has(role)) continue;
|
|
259
|
+
const el = responseElementTypeName(e, doc);
|
|
260
|
+
if (el !== null) visit(el, e.accessor.join("."), [], 0, new Set());
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The static path segment that an `{param}` addresses (`/sales/{saleId}` → `sales`). */
|
|
266
|
+
function resourceOfParam(path: string, param: string): string | null {
|
|
267
|
+
const segs = path.split("/").filter((s) => s.length > 0);
|
|
268
|
+
for (let i = 0; i < segs.length; i++) {
|
|
269
|
+
if (segs[i] === `{${param}}`) return i > 0 ? segs[i - 1]! : null;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function paramType(schema: OpenApi.IJsonSchema | undefined): "number" | "string" {
|
|
275
|
+
const type = (schema as { type?: unknown } | undefined)?.type;
|
|
276
|
+
return type === "integer" || type === "number" ? "number" : "string";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Build the agent tool surface from an OpenAPI document.
|
|
281
|
+
*/
|
|
282
|
+
export function buildToolSurface(document: OpenApi.IDocument): IAgentTool[] {
|
|
283
|
+
const { endpoints, roleOf, producerOf, nested } = producerIndex(document);
|
|
284
|
+
|
|
285
|
+
return endpoints.map((e): IAgentTool => {
|
|
286
|
+
const role = roleOf.get(e);
|
|
287
|
+
const readOnly = role !== undefined && READ_ROLES.has(role);
|
|
288
|
+
const method = e.method.toUpperCase();
|
|
289
|
+
const name = e.accessor.join(".");
|
|
290
|
+
|
|
291
|
+
const properties: Record<string, unknown> = {};
|
|
292
|
+
const required: string[] = [];
|
|
293
|
+
const producers: IAgentTool["producers"] = [];
|
|
294
|
+
for (const p of e.parameters) {
|
|
295
|
+
const resource = resourceOfParam(e.path, p.name);
|
|
296
|
+
const direct = resource !== null ? producerOf.get(resource) : undefined;
|
|
297
|
+
const nestedP =
|
|
298
|
+
resource !== null ? nested.get(resource.toLowerCase()) : undefined;
|
|
299
|
+
let description = p.description || `path parameter ${p.name}`;
|
|
300
|
+
if (direct !== undefined && direct.tool !== name) {
|
|
301
|
+
// a dedicated list/search produces this id directly
|
|
302
|
+
producers.push({ param: p.name, tool: direct.tool, field: direct.field });
|
|
303
|
+
description = `the ${direct.field} of a ${resource}; obtain it from the \`${direct.tool}\` tool — use each item's \`${direct.field}\` field`;
|
|
304
|
+
} else if (nestedP !== undefined && nestedP.tool !== name) {
|
|
305
|
+
// the id lives nested inside a parent read's response
|
|
306
|
+
producers.push({
|
|
307
|
+
param: p.name,
|
|
308
|
+
tool: nestedP.tool,
|
|
309
|
+
field: `${nestedP.path}.${nestedP.field}`,
|
|
310
|
+
});
|
|
311
|
+
description = `the ${nestedP.field} of a ${resource}; obtain it from the \`${nestedP.tool}\` tool's response at \`${nestedP.path}.${nestedP.field}\``;
|
|
312
|
+
}
|
|
313
|
+
properties[p.name] = { type: paramType(p.schema), description };
|
|
314
|
+
required.push(p.name);
|
|
315
|
+
}
|
|
316
|
+
if (e.query !== null) {
|
|
317
|
+
properties.query = {
|
|
318
|
+
type: "object",
|
|
319
|
+
description: "optional search / filter / pagination",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (e.requestBody !== null) {
|
|
323
|
+
properties.body = {
|
|
324
|
+
type: "object",
|
|
325
|
+
description: `request body (${e.requestBody.typeName})`,
|
|
326
|
+
};
|
|
327
|
+
required.push("body");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const tag = readOnly ? "[READ]" : "[WRITE — mutates server state]";
|
|
331
|
+
const chainNote =
|
|
332
|
+
producers.length > 0
|
|
333
|
+
? ` Inputs come from: ${producers.map((p) => `${p.param} ← ${p.tool}.${p.field}`).join("; ")}.`
|
|
334
|
+
: "";
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
name,
|
|
338
|
+
description: `${tag} ${e.description || `${method} ${e.path}`}`.trim() + chainNote,
|
|
339
|
+
inputSchema: { type: "object", properties, required },
|
|
340
|
+
annotations: {
|
|
341
|
+
readOnlyHint: readOnly,
|
|
342
|
+
destructiveHint: method === "DELETE",
|
|
343
|
+
idempotentHint: method === "GET" || method === "PUT" || method === "DELETE",
|
|
344
|
+
},
|
|
345
|
+
operation: {
|
|
346
|
+
method,
|
|
347
|
+
path: e.path,
|
|
348
|
+
pathParams: e.parameters.map((p) => p.name),
|
|
349
|
+
hasQuery: e.query !== null,
|
|
350
|
+
hasBody: e.requestBody !== null,
|
|
351
|
+
},
|
|
352
|
+
producers,
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Honest consumability analysis with a 4-way split per path-param input, so the
|
|
359
|
+
* report never accuses an API of a design gap it cannot actually prove:
|
|
360
|
+
*
|
|
361
|
+
* - `resolved` — a collection read (list/search) produces the id; the tool
|
|
362
|
+
* surface declares that producer. Directly navigable, and the agent is told
|
|
363
|
+
* exactly where the id comes from.
|
|
364
|
+
* - `nested` — the id is obtainable nested inside a parent read's response
|
|
365
|
+
* (`order` → `goods[].id`, `sale` → `units[].stocks[].id`), but no dedicated
|
|
366
|
+
* list tool produces it. Navigable, just not via a single tool — counted
|
|
367
|
+
* honestly, separate from `resolved` so the report matches the tool surface.
|
|
368
|
+
* - `untraceable`— the resource HAS a read endpoint, but no id could be found in
|
|
369
|
+
* its response (e.g. an inline schema this analysis can't fully introspect).
|
|
370
|
+
* Undetermined, not blamed.
|
|
371
|
+
* - `userInput` — the param is not an entity id but a human-known key
|
|
372
|
+
* (`repository_name`, `template_key`, a `scope`/`slug`), supplied by the
|
|
373
|
+
* caller — a missing producer is expected, NOT a defect.
|
|
374
|
+
* - `orphan` — an entity id (`*_id` / `uuid`) that NO endpoint produces. A
|
|
375
|
+
* genuine consumability defect.
|
|
376
|
+
*/
|
|
377
|
+
export interface IConsumabilityAnalysis {
|
|
378
|
+
tools: number;
|
|
379
|
+
readTools: number;
|
|
380
|
+
writeTools: number;
|
|
381
|
+
referenceInputs: number;
|
|
382
|
+
resolved: number;
|
|
383
|
+
nested: number;
|
|
384
|
+
untraceable: Array<{ tool: string; param: string; resource: string }>;
|
|
385
|
+
userInputs: Array<{ tool: string; param: string; resource: string }>;
|
|
386
|
+
orphan: Array<{ tool: string; param: string; resource: string }>;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* True when a path param names an entity identifier (`id`, `*_id`, `*Id`,
|
|
391
|
+
* `uuid`) — the kind that must be produced by another endpoint. A param that is
|
|
392
|
+
* a human-known key instead (`repository_name`, `template_key`, `scope`, a
|
|
393
|
+
* `slug`/`code`) is caller-supplied, so its absence of a producer is not a gap.
|
|
394
|
+
*/
|
|
395
|
+
function isIdLikeParam(name: string): boolean {
|
|
396
|
+
return /(^id$)|(_id$)|([a-z]Id$)|(^uuid$)|(_uuid$)|([a-z]Uuid$)/.test(name);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function analyzeConsumability(
|
|
400
|
+
document: OpenApi.IDocument,
|
|
401
|
+
): IConsumabilityAnalysis {
|
|
402
|
+
// ONE producer model, shared with the tool surface — so the report never
|
|
403
|
+
// claims an input is navigable that the tools give no producer hint for.
|
|
404
|
+
const { endpoints, roleOf, producerOf, readableResources, nested } =
|
|
405
|
+
producerIndex(document);
|
|
406
|
+
|
|
407
|
+
const isReadTool = (e: IAutoViewEndpoint): boolean => {
|
|
408
|
+
const role = roleOf.get(e);
|
|
409
|
+
return role !== undefined && READ_ROLES.has(role);
|
|
410
|
+
};
|
|
411
|
+
// Classify every id input once, then aggregate purely (no mutable counters).
|
|
412
|
+
const inputs = endpoints.flatMap((e) =>
|
|
413
|
+
e.parameters.map((p) => {
|
|
414
|
+
const resource = resourceOfParam(e.path, p.name);
|
|
415
|
+
const ref = { tool: e.accessor.join("."), param: p.name, resource: resource ?? "?" };
|
|
416
|
+
const status =
|
|
417
|
+
resource !== null && producerOf.has(resource)
|
|
418
|
+
? "resolved"
|
|
419
|
+
: resource !== null && nested.has(resource.toLowerCase())
|
|
420
|
+
? "nested"
|
|
421
|
+
: // a non-id param (`username`, `scope`, `*_name`) is a caller-known
|
|
422
|
+
// key regardless of whether its resource is readable — not a gap.
|
|
423
|
+
!isIdLikeParam(p.name)
|
|
424
|
+
? "userInput"
|
|
425
|
+
: // an entity id: readable resource but no producer traced =
|
|
426
|
+
// undetermined; never read at all = a genuine orphan.
|
|
427
|
+
resource !== null && readableResources.has(resource)
|
|
428
|
+
? "untraceable"
|
|
429
|
+
: "orphan";
|
|
430
|
+
return { ...ref, status };
|
|
431
|
+
}),
|
|
432
|
+
);
|
|
433
|
+
const readTools = endpoints.filter(isReadTool).length;
|
|
434
|
+
return {
|
|
435
|
+
tools: endpoints.length,
|
|
436
|
+
readTools,
|
|
437
|
+
writeTools: endpoints.length - readTools,
|
|
438
|
+
referenceInputs: inputs.length,
|
|
439
|
+
resolved: inputs.filter((i) => i.status === "resolved").length,
|
|
440
|
+
nested: inputs.filter((i) => i.status === "nested").length,
|
|
441
|
+
untraceable: inputs
|
|
442
|
+
.filter((i) => i.status === "untraceable")
|
|
443
|
+
.map(({ tool, param, resource }) => ({ tool, param, resource })),
|
|
444
|
+
userInputs: inputs
|
|
445
|
+
.filter((i) => i.status === "userInput")
|
|
446
|
+
.map(({ tool, param, resource }) => ({ tool, param, resource })),
|
|
447
|
+
orphan: inputs
|
|
448
|
+
.filter((i) => i.status === "orphan")
|
|
449
|
+
.map(({ tool, param, resource }) => ({ tool, param, resource })),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Coverage summary of a tool surface — how navigable it is for an agent. */
|
|
454
|
+
export interface IToolSurfaceCoverage {
|
|
455
|
+
tools: number;
|
|
456
|
+
/** Tools tagged read vs write. */
|
|
457
|
+
readTools: number;
|
|
458
|
+
writeTools: number;
|
|
459
|
+
/** id-like path-param inputs across the surface. */
|
|
460
|
+
referenceInputs: number;
|
|
461
|
+
/** reference inputs with a declared producer (the navigable fraction). */
|
|
462
|
+
resolvedInputs: number;
|
|
463
|
+
/** reference inputs with no producer — an agent must guess where they come from. */
|
|
464
|
+
orphanInputs: number;
|
|
465
|
+
/**
|
|
466
|
+
* The orphan inputs in detail: a required id with no tool in the API that
|
|
467
|
+
* produces it. An agent (or a human) cannot obtain this value from any
|
|
468
|
+
* endpoint — a consumability defect in the API design.
|
|
469
|
+
*/
|
|
470
|
+
orphans: Array<{ tool: string; param: string }>;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Deterministic structural coverage of a tool surface — the LLM-independent
|
|
475
|
+
* verification: is the tool graph navigable (every id input has a declared
|
|
476
|
+
* producer)? Reported as a report card, distinct from running an agent.
|
|
477
|
+
*/
|
|
478
|
+
export function coverToolSurface(tools: IAgentTool[]): IToolSurfaceCoverage {
|
|
479
|
+
// Classify every path param once, then aggregate purely (no mutable counters).
|
|
480
|
+
const inputs = tools.flatMap((t) => {
|
|
481
|
+
const producerParams = new Set(t.producers.map((p) => p.param));
|
|
482
|
+
return t.operation.pathParams.map((param) => ({
|
|
483
|
+
tool: t.name,
|
|
484
|
+
param,
|
|
485
|
+
resolved: producerParams.has(param),
|
|
486
|
+
}));
|
|
487
|
+
});
|
|
488
|
+
const readTools = tools.filter((t) => t.annotations.readOnlyHint).length;
|
|
489
|
+
const resolvedInputs = inputs.filter((i) => i.resolved).length;
|
|
490
|
+
return {
|
|
491
|
+
tools: tools.length,
|
|
492
|
+
readTools,
|
|
493
|
+
writeTools: tools.length - readTools,
|
|
494
|
+
referenceInputs: inputs.length,
|
|
495
|
+
resolvedInputs,
|
|
496
|
+
orphanInputs: inputs.length - resolvedInputs,
|
|
497
|
+
orphans: inputs
|
|
498
|
+
.filter((i) => !i.resolved)
|
|
499
|
+
.map(({ tool, param }) => ({ tool, param })),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
|
|
3
|
+
import { OpenApiConverter } from "@typia/utils";
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { buildToolSurface } from "./toolSurface";
|
|
7
|
+
import {
|
|
8
|
+
executeToolCall,
|
|
9
|
+
functionName,
|
|
10
|
+
IChatClient,
|
|
11
|
+
toFunctionTools,
|
|
12
|
+
verifyAgentTasks,
|
|
13
|
+
} from "./verifyAgentTasks";
|
|
14
|
+
|
|
15
|
+
function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
|
|
16
|
+
return OpenApiConverter.upgradeDocument({
|
|
17
|
+
openapi: "3.0.0", info: { title: "Shop", version: "1.0.0" }, paths, components: { schemas },
|
|
18
|
+
} as never);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fixture = doc(
|
|
22
|
+
{
|
|
23
|
+
"/sales": { get: { operationId: "index", summary: "List sales.", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Sale" } } } } } } } },
|
|
24
|
+
"/sales/{saleId}": { get: { operationId: "at", summary: "Get a sale.", parameters: [{ name: "saleId", in: "path", required: true, schema: { type: "string" } }], responses: { 200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Sale" } } } } } } },
|
|
25
|
+
},
|
|
26
|
+
{ Sale: { type: "object", properties: { id: { type: "string" }, title: { type: "string" } }, required: ["id"] } },
|
|
27
|
+
);
|
|
28
|
+
const tools = buildToolSurface(fixture);
|
|
29
|
+
|
|
30
|
+
const SALES = [{ id: "sale_1", title: "MacBook" }, { id: "sale_2", title: "iPhone" }];
|
|
31
|
+
let server: http.Server;
|
|
32
|
+
let base = "";
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
server = http.createServer((req, res) => {
|
|
35
|
+
const u = (req.url ?? "").split("?")[0];
|
|
36
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
37
|
+
if (u === "/sales") return res.end(JSON.stringify({ data: SALES }));
|
|
38
|
+
const m = u!.match(/^\/sales\/([^/]+)$/);
|
|
39
|
+
return res.end(JSON.stringify(m ? SALES.find((s) => s.id === m[1]) ?? {} : {}));
|
|
40
|
+
});
|
|
41
|
+
await new Promise<void>((ok) => server.listen(0, "127.0.0.1", () => ok()));
|
|
42
|
+
base = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
|
|
43
|
+
});
|
|
44
|
+
afterAll(() => server.close());
|
|
45
|
+
|
|
46
|
+
describe("toFunctionTools", () => {
|
|
47
|
+
it("converts the surface to OpenAI function-calling tools", () => {
|
|
48
|
+
const fns = toFunctionTools(tools);
|
|
49
|
+
expect(fns).toHaveLength(tools.length);
|
|
50
|
+
expect(fns[0]!.type).toBe("function");
|
|
51
|
+
// dotted accessor names are sanitized to OpenAI's `[a-zA-Z0-9_-]` charset
|
|
52
|
+
expect(fns[0]!.function.name).toBe(functionName(tools[0]!.name));
|
|
53
|
+
expect(fns[0]!.function.name).not.toContain(".");
|
|
54
|
+
expect(fns[0]!.function.parameters).toBe(tools[0]!.inputSchema);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("executeToolCall", () => {
|
|
59
|
+
it("maps a list call to the right HTTP request and returns the body", async () => {
|
|
60
|
+
const list = tools.find((t) => t.name === "sales.get")!;
|
|
61
|
+
const res = await executeToolCall(tools, list.name, {}, { baseUrl: base });
|
|
62
|
+
expect(res.ok).toBe(true);
|
|
63
|
+
expect(res.text).toContain("MacBook");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("substitutes path params for a detail call", async () => {
|
|
67
|
+
const detail = tools.find((t) => t.name === "sales.getBySaleid")!;
|
|
68
|
+
const res = await executeToolCall(tools, detail.name, { saleId: "sale_2" }, { baseUrl: base });
|
|
69
|
+
expect(res.text).toContain("iPhone");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns an error for an unknown tool", async () => {
|
|
73
|
+
const res = await executeToolCall(tools, "nope", {}, { baseUrl: base });
|
|
74
|
+
expect(res.ok).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("verifyAgentTasks", () => {
|
|
79
|
+
it("drives the agent loop (scripted client), executes tools, and grades the task", async () => {
|
|
80
|
+
// a fake client: turn 1 → call the sales list tool; turn 2 → final answer
|
|
81
|
+
let turn = 0;
|
|
82
|
+
const client: IChatClient = {
|
|
83
|
+
chat: {
|
|
84
|
+
completions: {
|
|
85
|
+
create: async () => {
|
|
86
|
+
turn++;
|
|
87
|
+
if (turn === 1) {
|
|
88
|
+
return { choices: [{ message: { content: null, tool_calls: [{ id: "c1", function: { name: "sales.get", arguments: "{}" } }] } }] };
|
|
89
|
+
}
|
|
90
|
+
return { choices: [{ message: { content: "There are 2 sales: MacBook and iPhone." } }] };
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const results = await verifyAgentTasks(fixture, {
|
|
96
|
+
client,
|
|
97
|
+
model: "fake",
|
|
98
|
+
baseUrl: base,
|
|
99
|
+
tasks: [{ id: "count", prompt: "How many sales?", expect: (a) => a.includes("2") }],
|
|
100
|
+
});
|
|
101
|
+
expect(results).toHaveLength(1);
|
|
102
|
+
expect(results[0]!.toolCalls).toEqual(["sales.get"]);
|
|
103
|
+
expect(results[0]!.passed).toBe(true);
|
|
104
|
+
expect(results[0]!.answer).toContain("MacBook");
|
|
105
|
+
});
|
|
106
|
+
});
|