@cartanova/qgrid-cli 2.7.3 → 2.7.5

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.
Files changed (42) hide show
  1. package/bundle/dist/application/queries.generated.js +2 -1
  2. package/bundle/dist/application/request-log/json-schema-type-text.js +58 -2
  3. package/bundle/dist/application/request-log/request-log.model.js +44 -3
  4. package/bundle/dist/application/request-log/request-log.types.js +10 -4
  5. package/bundle/src/application/queries.generated.ts +3 -0
  6. package/bundle/src/application/request-log/json-schema-type-text.test.ts +115 -1
  7. package/bundle/src/application/request-log/json-schema-type-text.ts +91 -0
  8. package/bundle/src/application/request-log/request-log.model.test.ts +166 -0
  9. package/bundle/src/application/request-log/request-log.model.ts +52 -6
  10. package/bundle/src/application/request-log/request-log.types.ts +11 -2
  11. package/bundle/src/application/sonamu.generated.http +6 -2
  12. package/bundle/web-dist/client/assets/index-CwSSXuZv.js +106 -0
  13. package/bundle/web-dist/client/assets/index-D9xE_tO7.css +1 -0
  14. package/bundle/web-dist/client/assets/logs-BrAu0koY.js +1 -0
  15. package/bundle/web-dist/client/assets/{monit-BngOoU4n.js → monit-D6JEk7n7.js} +1 -1
  16. package/bundle/web-dist/client/assets/{routes-BnQE0rPF.js → routes-Dvu63qre.js} +1 -1
  17. package/bundle/web-dist/client/assets/{services.generated-CSW8bhCP.js → services.generated-CA7oRRYY.js} +4 -4
  18. package/bundle/web-dist/client/assets/settings-mwDGFwUi.js +1 -0
  19. package/bundle/web-dist/client/assets/show-BFGjYsWb.js +2 -0
  20. package/bundle/web-dist/client/assets/{tokens-LHoEigmF.js → tokens-D-P__jYk.js} +1 -1
  21. package/bundle/web-dist/client/assets/{use-oauth-login-flow-YyU4TDzU.js → use-oauth-login-flow-CJpoJD26.js} +1 -1
  22. package/bundle/web-dist/client/index.html +3 -3
  23. package/bundle/web-dist/server/assets/{lazyRouteComponent-0y-znAfz.js → lazyRouteComponent-tS-rTksX.js} +2 -2
  24. package/bundle/web-dist/server/assets/{logs-CJBbHM3_.js → logs-ttf79emP.js} +3 -14
  25. package/bundle/web-dist/server/assets/{monit-8WAsPETB.js → monit-CJBUH0l6.js} +2 -2
  26. package/bundle/web-dist/server/assets/{plus-9das21ju.js → plus-BuSuoeon.js} +1 -1
  27. package/bundle/web-dist/server/assets/{react-dom-zmEq8BVp.js → react-dom-7_SqhNWR.js} +1 -1
  28. package/bundle/web-dist/server/assets/{routes-B6bX3o41.js → routes-z79f1sOM.js} +3 -3
  29. package/bundle/web-dist/server/assets/{services.generated-DyPeBI00.js → services.generated-B5UkNmgc.js} +20 -38
  30. package/bundle/web-dist/server/assets/{settings-D1u5WdpP.js → settings-nCv71sax.js} +2 -2
  31. package/bundle/web-dist/server/assets/{show-Cr7vwR0l.js → show-CLrA4ZQ-.js} +158 -37
  32. package/bundle/web-dist/server/assets/{tokens-BumM0gnH.js → tokens-BwlkZELV.js} +3 -3
  33. package/bundle/web-dist/server/assets/{use-oauth-login-flow-DmsHGOlx.js → use-oauth-login-flow-B2QllVMi.js} +1 -1
  34. package/bundle/web-dist/server/entry-server.generated.js +10 -11
  35. package/package.json +3 -2
  36. package/skills/qgrid/references/request-log-run-lifecycle.md +9 -1
  37. package/skills/qgrid/references/sonamu-api-web-flow.md +3 -1
  38. package/bundle/web-dist/client/assets/index-BldeEvAK.css +0 -1
  39. package/bundle/web-dist/client/assets/index-QxWJK2qE.js +0 -106
  40. package/bundle/web-dist/client/assets/logs-DaNYzis5.js +0 -1
  41. package/bundle/web-dist/client/assets/settings-CPeAjsL3.js +0 -1
  42. package/bundle/web-dist/client/assets/show-DUOfuZj1.js +0 -2
@@ -1,12 +1,50 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { renderJsonSchemaTypeText } from "./json-schema-type-text";
3
+ import {
4
+ renderJsonSchemaZodShapeText,
5
+ renderJsonSchemaTypeText,
6
+ renderParsedJsonSchemaTypeText,
7
+ } from "./json-schema-type-text";
4
8
 
5
9
  function schema(value: unknown): string {
6
10
  return JSON.stringify(value);
7
11
  }
8
12
 
9
13
  describe("renderJsonSchemaTypeText", () => {
14
+ it("preserves the existing parsed-schema output across supported node kinds", () => {
15
+ expect(
16
+ renderParsedJsonSchemaTypeText({
17
+ type: "object",
18
+ properties: {
19
+ mode: { anyOf: [{ const: "fast" }, { const: "safe" }] },
20
+ attempts: { type: "integer" },
21
+ tag: { type: ["string", "null"] },
22
+ options: {
23
+ type: "object",
24
+ properties: {
25
+ enabled: { type: "boolean" },
26
+ opaque: { format: "custom" },
27
+ },
28
+ required: ["enabled"],
29
+ },
30
+ },
31
+ required: ["mode", "attempts", "tag", "options"],
32
+ }),
33
+ ).toBe(
34
+ [
35
+ "type Response = {",
36
+ ' mode: "fast" | "safe"',
37
+ " attempts: number",
38
+ " tag: string | null",
39
+ " options: {",
40
+ " enabled: boolean",
41
+ " opaque?: unknown",
42
+ " }",
43
+ "}",
44
+ ].join("\n"),
45
+ );
46
+ });
47
+
10
48
  it("renders a flat object with optionality from required", () => {
11
49
  const result = renderJsonSchemaTypeText(
12
50
  schema({
@@ -94,3 +132,79 @@ describe("renderJsonSchemaTypeText", () => {
94
132
  expect(renderJsonSchemaTypeText("{broken")).toBeNull();
95
133
  });
96
134
  });
135
+
136
+ describe("renderJsonSchemaZodShapeText", () => {
137
+ it("renders a compact object shape with required, optional, and default annotations", () => {
138
+ expect(
139
+ renderJsonSchemaZodShapeText(
140
+ {
141
+ type: "object",
142
+ properties: {
143
+ limit: { type: "integer", default: 10, maximum: 100 },
144
+ query: { type: "string", minLength: 1, pattern: "^[a-z]+$" },
145
+ },
146
+ required: ["query"],
147
+ },
148
+ 6,
149
+ ),
150
+ ).toBe(
151
+ [
152
+ "z.object({",
153
+ " query: z.string(),",
154
+ " limit: z.number().optional(), // default=10",
155
+ "})",
156
+ ].join("\n"),
157
+ );
158
+ });
159
+
160
+ it("shortens long string enums with an inline remainder comment", () => {
161
+ expect(
162
+ renderJsonSchemaZodShapeText(
163
+ {
164
+ type: "object",
165
+ properties: {
166
+ mode: { enum: ["one", "two", "three", "four", "five", "six", "seven", "eight"] },
167
+ },
168
+ required: ["mode"],
169
+ },
170
+ 6,
171
+ ),
172
+ ).toBe(
173
+ [
174
+ "z.object({",
175
+ ' mode: z.enum(["one", "two", "three", "four", "five", "six" /* +2 */]),',
176
+ "})",
177
+ ].join("\n"),
178
+ );
179
+ });
180
+
181
+ it("renders nested objects, arrays, unions, literals, and unknown nodes", () => {
182
+ expect(
183
+ renderJsonSchemaZodShapeText({
184
+ type: "object",
185
+ properties: {
186
+ filters: {
187
+ type: "object",
188
+ properties: {
189
+ tags: { type: "array", items: { type: "string" } },
190
+ value: { anyOf: [{ const: 1 }, { type: "boolean" }] },
191
+ },
192
+ required: ["tags"],
193
+ },
194
+ opaque: { format: "custom" },
195
+ },
196
+ required: ["filters", "opaque"],
197
+ }),
198
+ ).toBe(
199
+ [
200
+ "z.object({",
201
+ " filters: z.object({",
202
+ " tags: z.array(z.string()),",
203
+ " value: z.union([z.literal(1), z.boolean()]).optional(),",
204
+ " }),",
205
+ " opaque: z.unknown(),",
206
+ "})",
207
+ ].join("\n"),
208
+ );
209
+ });
210
+ });
@@ -25,6 +25,92 @@ function renderPrimitive(type: unknown): string {
25
25
  return "unknown";
26
26
  }
27
27
 
28
+ function renderZodPrimitive(type: unknown): string {
29
+ if (type === "string") return "z.string()";
30
+ if (type === "number" || type === "integer") return "z.number()";
31
+ if (type === "boolean") return "z.boolean()";
32
+ if (type === "null") return "z.null()";
33
+ return "z.unknown()";
34
+ }
35
+
36
+ function renderZodEnum(values: unknown[], enumValueLimit?: number): string {
37
+ if (values.length === 0) return "z.never()";
38
+
39
+ const visibleValues =
40
+ enumValueLimit === undefined ? values : values.slice(0, Math.max(0, enumValueLimit));
41
+ const omittedCount = values.length - visibleValues.length;
42
+ const suffix = omittedCount > 0 ? ` /* +${omittedCount} */` : "";
43
+
44
+ if (values.every((value) => typeof value === "string")) {
45
+ return `z.enum([${visibleValues.map((value) => JSON.stringify(value)).join(", ")}${suffix}])`;
46
+ }
47
+
48
+ const literals = visibleValues.map((value) => `z.literal(${JSON.stringify(value)})`);
49
+ if (literals.length === 1 && omittedCount === 0) return literals[0] ?? "z.never()";
50
+ return `z.union([${literals.join(", ")}${suffix}])`;
51
+ }
52
+
53
+ function renderZodNode(node: unknown, depth: number, enumValueLimit?: number): string {
54
+ if (node === true) return "z.unknown()";
55
+ if (!node || typeof node !== "object" || Array.isArray(node)) return "z.unknown()";
56
+ const schema = node as SchemaNode;
57
+
58
+ if (Array.isArray(schema.enum)) return renderZodEnum(schema.enum, enumValueLimit);
59
+ if (schema.const !== undefined) return `z.literal(${JSON.stringify(schema.const)})`;
60
+
61
+ const variants = (schema.anyOf ?? schema.oneOf) as unknown;
62
+ if (Array.isArray(variants) && variants.length > 0) {
63
+ const rendered = [
64
+ ...new Set(variants.map((variant) => renderZodNode(variant, depth, enumValueLimit))),
65
+ ];
66
+ return rendered.length === 1
67
+ ? (rendered[0] ?? "z.unknown()")
68
+ : `z.union([${rendered.join(", ")}])`;
69
+ }
70
+
71
+ const type = schema.type;
72
+ if (Array.isArray(type)) {
73
+ const rendered = [...new Set(type.map(renderZodPrimitive))];
74
+ return rendered.length === 1
75
+ ? (rendered[0] ?? "z.unknown()")
76
+ : `z.union([${rendered.join(", ")}])`;
77
+ }
78
+
79
+ if (type === "object" || (type === undefined && schema.properties)) {
80
+ const properties = (schema.properties ?? {}) as Record<string, unknown>;
81
+ const required = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []);
82
+ const keys = Object.keys(properties);
83
+ if (keys.length === 0) return "z.object({})";
84
+ const orderedKeys = [
85
+ ...keys.filter((key) => required.has(key)),
86
+ ...keys.filter((key) => !required.has(key)),
87
+ ];
88
+
89
+ const inner = " ".repeat(depth + 1);
90
+ const lines = orderedKeys.map((key) => {
91
+ const propertySchema = properties[key];
92
+ const rendered = renderZodNode(propertySchema, depth + 1, enumValueLimit);
93
+ const optional = required.has(key) ? rendered : `${rendered}.optional()`;
94
+ const defaultValue =
95
+ propertySchema &&
96
+ typeof propertySchema === "object" &&
97
+ !Array.isArray(propertySchema) &&
98
+ Object.hasOwn(propertySchema, "default")
99
+ ? JSON.stringify((propertySchema as SchemaNode).default)
100
+ : undefined;
101
+ const defaultComment = defaultValue === undefined ? "" : ` // default=${defaultValue}`;
102
+ return `${inner}${renderKey(key)}: ${optional},${defaultComment}`;
103
+ });
104
+ return `z.object({\n${lines.join("\n")}\n${" ".repeat(depth)}})`;
105
+ }
106
+
107
+ if (type === "array") {
108
+ return `z.array(${renderZodNode(schema.items, depth, enumValueLimit)})`;
109
+ }
110
+
111
+ return renderZodPrimitive(type);
112
+ }
113
+
28
114
  function renderNode(node: unknown, depth: number): string {
29
115
  if (node === true) return "unknown";
30
116
  if (!node || typeof node !== "object" || Array.isArray(node)) return "unknown";
@@ -73,6 +159,11 @@ export function renderParsedJsonSchemaTypeText(schema: unknown, name = "Response
73
159
  return `type ${name} = ${renderNode(schema, 0)}`;
74
160
  }
75
161
 
162
+ /** Tool input 계약용 표시 전용 Zod shape. 검증 제약은 의도적으로 생략한다. */
163
+ export function renderJsonSchemaZodShapeText(schema: unknown, enumValueLimit?: number): string {
164
+ return renderZodNode(schema, 0, enumValueLimit);
165
+ }
166
+
76
167
  /** 실패(파싱 불가 등) 시 null — 호출부가 표시 자체를 생략한다. */
77
168
  export function renderJsonSchemaTypeText(schemaJson: string, name = "Response"): string | null {
78
169
  try {
@@ -28,6 +28,16 @@ function mockTransactionalWritePuri(transactionPuri: unknown) {
28
28
  return transaction;
29
29
  }
30
30
 
31
+ function mockToolsViewQuery(row: { tools: unknown } | undefined) {
32
+ const chain = mockQueryBuilder(row === undefined ? [] : [row]);
33
+ const from = vi.fn(() => chain);
34
+ vi.spyOn(
35
+ RequestLogModel as unknown as { getPuri: () => { from: typeof from } },
36
+ "getPuri",
37
+ ).mockReturnValue({ from });
38
+ return { chain, from };
39
+ }
40
+
31
41
  describe("RequestLogModel TTFT", () => {
32
42
  afterEach(() => {
33
43
  vi.restoreAllMocks();
@@ -108,6 +118,162 @@ describe("RequestLogModel TTFT", () => {
108
118
  });
109
119
  });
110
120
 
121
+ describe("RequestLogModel toolsView", () => {
122
+ afterEach(() => {
123
+ vi.restoreAllMocks();
124
+ });
125
+
126
+ it("returns a display-ready required-first contract from the decoded JSONB value", async () => {
127
+ const tools = [
128
+ {
129
+ name: "search",
130
+ description: "Search indexed records",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ optionalEmpty: { type: "string", default: "" },
135
+ requiredEnum: {
136
+ enum: ["one", "two", "three", "four", "five", "six", "seven", "eight"],
137
+ description: "Search mode",
138
+ },
139
+ requiredFalse: { type: "boolean", default: false },
140
+ requiredZero: { type: "integer", default: 0 },
141
+ optionalNull: { type: ["string", "null"], default: null, description: 42 },
142
+ optionalAbsent: { type: "string", description: "Optional note" },
143
+ },
144
+ required: ["requiredEnum", "requiredFalse", "requiredZero"],
145
+ },
146
+ },
147
+ {
148
+ name: "ping",
149
+ inputSchema: {
150
+ type: "object",
151
+ properties: { message: { type: "string" } },
152
+ required: ["message"],
153
+ },
154
+ },
155
+ ];
156
+ const { chain, from } = mockToolsViewQuery({ tools });
157
+
158
+ await expect(RequestLogModel.toolsView(7)).resolves.toEqual([
159
+ {
160
+ name: "search",
161
+ description: "Search indexed records",
162
+ parameterCount: 6,
163
+ inputZod: [
164
+ "z.object({",
165
+ ' requiredEnum: z.enum(["one", "two", "three", "four", "five", "six" /* +2 */]),',
166
+ " requiredFalse: z.boolean(), // default=false",
167
+ " requiredZero: z.number(), // default=0",
168
+ ' optionalEmpty: z.string().optional(), // default=""',
169
+ " optionalNull: z.union([z.string(), z.null()]).optional(), // default=null",
170
+ " optionalAbsent: z.string().optional(),",
171
+ "})",
172
+ ].join("\n"),
173
+ fullInputZod: [
174
+ "z.object({",
175
+ ' requiredEnum: z.enum(["one", "two", "three", "four", "five", "six", "seven", "eight"]),',
176
+ " requiredFalse: z.boolean(), // default=false",
177
+ " requiredZero: z.number(), // default=0",
178
+ ' optionalEmpty: z.string().optional(), // default=""',
179
+ " optionalNull: z.union([z.string(), z.null()]).optional(), // default=null",
180
+ " optionalAbsent: z.string().optional(),",
181
+ "})",
182
+ ].join("\n"),
183
+ },
184
+ {
185
+ name: "ping",
186
+ parameterCount: 1,
187
+ inputZod: ["z.object({", " message: z.string(),", "})"].join("\n"),
188
+ },
189
+ ]);
190
+
191
+ expect(from).toHaveBeenCalledWith("request_logs");
192
+ expect(chain.select).toHaveBeenCalledWith({ tools: "request_logs.tools" });
193
+ expect(chain.where).toHaveBeenCalledWith("request_logs.id", 7);
194
+ expect(chain.first).toHaveBeenCalledTimes(1);
195
+ });
196
+
197
+ it("defensively decodes string storage without changing the view contract", async () => {
198
+ mockToolsViewQuery({
199
+ tools: JSON.stringify([
200
+ {
201
+ name: "lookup",
202
+ description: "Look up one record",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: { id: { type: "string" } },
206
+ required: ["id"],
207
+ },
208
+ },
209
+ ]),
210
+ });
211
+
212
+ await expect(RequestLogModel.toolsView(11)).resolves.toEqual([
213
+ {
214
+ name: "lookup",
215
+ description: "Look up one record",
216
+ parameterCount: 1,
217
+ inputZod: ["z.object({", " id: z.string(),", "})"].join("\n"),
218
+ },
219
+ ]);
220
+ });
221
+
222
+ // inputSchema 는 QgridTool 상 z.unknown() 이라 키 누락·null 도 wire 검증을 통과한다.
223
+ // 하나가 터지면 map 전체가 죽어 그 요청의 tool 계약이 통째로 사라지므로 개별 정규화한다.
224
+ it("renders unusable input schemas without failing the entire tool contract", async () => {
225
+ mockToolsViewQuery({
226
+ tools: [
227
+ { name: "unknown_input", inputSchema: {} },
228
+ { name: "empty_object_input", inputSchema: { type: "object" } },
229
+ { name: "string_input", inputSchema: { type: "string" } },
230
+ { name: "null_input", inputSchema: null },
231
+ { name: "absent_input" },
232
+ ],
233
+ });
234
+
235
+ await expect(RequestLogModel.toolsView(13)).resolves.toEqual([
236
+ {
237
+ name: "unknown_input",
238
+ parameterCount: 0,
239
+ inputZod: "z.unknown()",
240
+ },
241
+ {
242
+ name: "empty_object_input",
243
+ parameterCount: 0,
244
+ inputZod: "z.object({})",
245
+ },
246
+ {
247
+ name: "string_input",
248
+ parameterCount: 0,
249
+ inputZod: "z.string()",
250
+ },
251
+ {
252
+ name: "null_input",
253
+ parameterCount: 0,
254
+ inputZod: "z.unknown()",
255
+ },
256
+ {
257
+ name: "absent_input",
258
+ parameterCount: 0,
259
+ inputZod: "z.unknown()",
260
+ },
261
+ ]);
262
+ });
263
+
264
+ it.each([
265
+ ["a missing row", undefined],
266
+ ["null tools", { tools: null }],
267
+ ["an empty decoded array", { tools: [] }],
268
+ ["empty stored text", { tools: "" }],
269
+ ["invalid stored JSON", { tools: "{broken" }],
270
+ ])("returns an empty view for %s", async (_label, row) => {
271
+ mockToolsViewQuery(row);
272
+
273
+ await expect(RequestLogModel.toolsView(12)).resolves.toEqual([]);
274
+ });
275
+ });
276
+
111
277
  describe("RequestLogModel run lifecycle", () => {
112
278
  afterEach(() => {
113
279
  vi.restoreAllMocks();
@@ -14,17 +14,32 @@ import { SD } from "../../i18n/sd.generated";
14
14
  import { calculateCostUsd } from "../../utils/providers/common/model-cost";
15
15
  import { type RequestLogSubsetKey, type RequestLogSubsetMapping } from "../sonamu.generated";
16
16
  import { requestLogLoaderQueries, requestLogSubsetQueries } from "../sonamu.generated.sso";
17
- import { renderParsedJsonSchemaTypeText } from "./json-schema-type-text";
17
+ import {
18
+ renderJsonSchemaZodShapeText,
19
+ renderParsedJsonSchemaTypeText,
20
+ } from "./json-schema-type-text";
18
21
  import {
19
22
  type RequestLogListParams,
20
23
  type RequestLogSaveParams,
21
24
  type ToolDefinitions,
25
+ type ToolView,
22
26
  } from "./request-log.types";
23
27
  import { formatZodCode } from "./zod-code-format";
24
28
 
25
29
  // cost_usd는 정수 micro-USD로 저장. 실제 USD = cost_usd / MICRO_USD.
26
30
  export const MICRO_USD = 1_000_000;
27
31
  const REQUEST_LOG_RUN_LOCK_CLASS_ID = 718;
32
+ const TOOL_ENUM_VALUE_LIMIT = 6;
33
+
34
+ function decodeStoredTools(value: unknown): ToolDefinitions | null {
35
+ if (!value) return null;
36
+ if (typeof value !== "string") return value as ToolDefinitions;
37
+ try {
38
+ return JSON.parse(value) as ToolDefinitions;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
28
43
 
29
44
  type ToolResultContinuation = {
30
45
  toolCallId: string;
@@ -263,11 +278,6 @@ class RequestLogModelClass extends BaseModelClass<
263
278
  qb.where("request_logs.model_name", params.model_name);
264
279
  }
265
280
 
266
- // structured 응답이 JSON 파싱에 실패한 행만 — broken 배치 조사용 필터.
267
- if (params.response_json_broken) {
268
- qb.where("request_logs.response_json_ok", false);
269
- }
270
-
271
281
  if (params.search && params.keyword && params.keyword.length > 0) {
272
282
  if (params.search === "id") {
273
283
  qb.where("request_logs.id", Number(params.keyword));
@@ -426,6 +436,42 @@ class RequestLogModelClass extends BaseModelClass<
426
436
  return { typescript: renderParsedJsonSchemaTypeText(parsed), zod };
427
437
  }
428
438
 
439
+ // ── Tool Contract View
440
+
441
+ /**
442
+ * 요청에 장착된 tool 계약을 detail 화면용으로 축약한다. JSONB 는 이미 decode 된
443
+ * 배열로 오며 문자열인 경우만 방어적으로 parse 한다. 저장된 QgridTool 계약은
444
+ * 전수 검사로 확인됐으므로 항목별 보정이나 unreadable sentinel 은 만들지 않는다.
445
+ * 다만 `QgridTool.inputSchema` 는 `z.unknown()` 이라 키가 없거나 null 인 값도
446
+ * wire 검증을 통과하므로 빈 스키마로 정규화한다 — SDK 는 `{}` 로 채워 보낸다.
447
+ */
448
+ @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
449
+ async toolsView(id: number): Promise<ToolView[]> {
450
+ const row = (await this.getPuri("r")
451
+ .from("request_logs")
452
+ .select({ tools: "request_logs.tools" })
453
+ .where("request_logs.id", id)
454
+ .first()) as { tools: unknown } | undefined;
455
+ const tools = decodeStoredTools(row?.tools);
456
+ if (!tools) return [];
457
+
458
+ return tools.map((tool) => {
459
+ const inputSchema = (tool.inputSchema ?? {}) as {
460
+ properties?: Record<string, unknown>;
461
+ };
462
+ const inputZod = renderJsonSchemaZodShapeText(inputSchema, TOOL_ENUM_VALUE_LIMIT);
463
+ const fullInputZod = renderJsonSchemaZodShapeText(inputSchema);
464
+
465
+ return {
466
+ name: tool.name,
467
+ ...(tool.description !== undefined ? { description: tool.description } : {}),
468
+ parameterCount: Object.keys(inputSchema.properties ?? {}).length,
469
+ inputZod,
470
+ ...(inputZod !== fullInputZod ? { fullInputZod } : {}),
471
+ };
472
+ });
473
+ }
474
+
429
475
  // ── Run Lifecycle ──────────────────────────────────────────────
430
476
 
431
477
  async createRun(params: {
@@ -20,13 +20,22 @@ export type HistoryItems = z.infer<typeof HistoryItems>;
20
20
  export const ToolDefinitions = z.array(QgridTool);
21
21
  export type ToolDefinitions = z.infer<typeof ToolDefinitions>;
22
22
 
23
+ // request-log detail 화면이 소비하는 tool 계약 projection. API 반환 타입은 Sonamu
24
+ // syncer 가 모듈을 해석할 수 있도록 type-only 선언 대신 Zod schema 로 둔다.
25
+ export const ToolView = z.object({
26
+ name: z.string(),
27
+ description: z.string().optional(),
28
+ parameterCount: z.number(),
29
+ inputZod: z.string(),
30
+ fullInputZod: z.string().optional(),
31
+ });
32
+ export type ToolView = z.infer<typeof ToolView>;
33
+
23
34
  // RequestLog - ListParams
24
35
  // project_name NULL 분기: IS NULL만 조회(unassigned 필터) 또는 IS NOT NULL만(distinct 조회).
25
36
  export const RequestLogListParams = RequestLogBaseListParams.extend({
26
37
  project_name_is_null: z.boolean().optional(),
27
38
  project_name_is_not_null: z.boolean().optional(),
28
- // structured 응답이 깨진(JSON 파싱 실패) 행만 조회
29
- response_json_broken: z.boolean().optional(),
30
39
  });
31
40
  export type RequestLogListParams = z.infer<typeof RequestLogListParams>;
32
41
 
@@ -226,7 +226,6 @@ GET {{baseUrl}}/api/requestLog/findMany
226
226
  &rawParams[model_name]=
227
227
  &rawParams[project_name_is_null]=false
228
228
  &rawParams[project_name_is_not_null]=false
229
- &rawParams[response_json_broken]=false
230
229
  Content-Type: application/json
231
230
 
232
231
  ###
@@ -281,6 +280,12 @@ Content-Type: application/json
281
280
 
282
281
  ###
283
282
 
283
+ GET {{baseUrl}}/api/requestLog/toolsView
284
+ ?id=0
285
+ Content-Type: application/json
286
+
287
+ ###
288
+
284
289
  POST {{baseUrl}}/api/requestLog/del
285
290
  Content-Type: application/json
286
291
 
@@ -520,7 +525,6 @@ GET {{baseUrl}}/api/qgrid/totalCost
520
525
  &params[model_name]=
521
526
  &params[project_name_is_null]=false
522
527
  &params[project_name_is_not_null]=false
523
- &params[response_json_broken]=false
524
528
  Content-Type: application/json
525
529
 
526
530
  ###