From 558e28c6af6960ab329a67c0ebf260b8604a24a2 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 2 Sep 2026 04:45:42 +0000 Subject: [PATCH] refactor: eliminate duplicated logic in inspect, data-inspector, and simple-schema Extract shared helpers for copy-pasted logic surfaced by jscpd: - inspect RPC functions share one toInvokeResult() envelope helper - data-inspector runQuery/runQueryAtPath share executeQuery() - simple-schema record/array/object share pushFieldIssues() - AgentSmart onInvoke/onRead share runAction() --- packages/devframe/src/utils/simple-schema.ts | 36 ++++++++---------- .../data-inspector/src/engine/query-engine.ts | 37 ++++++++++--------- .../src/rpc/functions/_invoke-result.ts | 27 ++++++++++++++ .../src/rpc/functions/execute-command.ts | 19 +--------- .../src/rpc/functions/invoke-agent-tool.ts | 22 ++--------- plugins/inspect/src/rpc/functions/invoke.ts | 19 +--------- .../src/rpc/functions/read-agent-resource.ts | 22 ++--------- .../inspect/src/spa/components/AgentSmart.vue | 27 +++++--------- 8 files changed, 82 insertions(+), 127 deletions(-) create mode 100644 plugins/inspect/src/rpc/functions/_invoke-result.ts diff --git a/packages/devframe/src/utils/simple-schema.ts b/packages/devframe/src/utils/simple-schema.ts index 2124d94e0..f27c9cad4 100644 --- a/packages/devframe/src/utils/simple-schema.ts +++ b/packages/devframe/src/utils/simple-schema.ts @@ -79,6 +79,15 @@ function runSync( return result } +/** Validate a single field, appending any issues to `issues` prefixed by `key`. */ +function pushFieldIssues(issues: Issue[], key: PropertyKey, schema: StandardSchemaV1, value: unknown): void { + const result = runSync(schema, value) + if (result.issues) { + for (const issue of result.issues) + issues.push({ message: issue.message, path: [key, ...(issue.path ?? [])] }) + } +} + /** Any string. */ export function string(): SimpleSchema { return make('string', v => (typeof v === 'string' ? ok(v) : fail('Expected a string'))) @@ -147,13 +156,8 @@ export function record( return fail('Expected an object') const obj = v as Record const issues: Issue[] = [] - for (const key of Object.keys(obj)) { - const result = runSync(value, obj[key]) - if (result.issues) { - for (const issue of result.issues) - issues.push({ message: issue.message, path: [key, ...(issue.path ?? [])] }) - } - } + for (const key of Object.keys(obj)) + pushFieldIssues(issues, key, value, obj[key]) return issues.length ? { issues } : ok(v as any) }) } @@ -166,13 +170,8 @@ export function array( if (!Array.isArray(v)) return fail('Expected an array') const issues: Issue[] = [] - for (let i = 0; i < v.length; i++) { - const result = runSync(item, v[i]) - if (result.issues) { - for (const issue of result.issues) - issues.push({ message: issue.message, path: [i, ...(issue.path ?? [])] }) - } - } + for (let i = 0; i < v.length; i++) + pushFieldIssues(issues, i, item, v[i]) return issues.length ? { issues } : ok(v as any) }) } @@ -203,13 +202,8 @@ export function object>( return fail('Expected an object') const obj = v as Record const issues: Issue[] = [] - for (const [key, schema] of entries) { - const result = runSync(schema, obj[key]) - if (result.issues) { - for (const issue of result.issues) - issues.push({ message: issue.message, path: [key, ...(issue.path ?? [])] }) - } - } + for (const [key, schema] of entries) + pushFieldIssues(issues, key, schema, obj[key]) // Guard-only: return the original object so extra keys survive. return issues.length ? { issues } : ok(v as any) }) diff --git a/plugins/data-inspector/src/engine/query-engine.ts b/plugins/data-inspector/src/engine/query-engine.ts index 5cd7c4732..2909a122a 100644 --- a/plugins/data-inspector/src/engine/query-engine.ts +++ b/plugins/data-inspector/src/engine/query-engine.ts @@ -107,13 +107,25 @@ function getCreateQuery(): Promise { })) } -export async function runQuery(target: unknown, query: string, options?: NormalizeOptions): Promise { +/** + * Run the query against the target, select the node to return from its raw + * result, and normalize that node into a wire-safe {@link QueryOutcome}. + * The `select` step runs inside the timed section so re-descent costs (used + * by {@link runQueryAtPath}) are reflected in `queryMs`. + */ +async function executeQuery( + target: unknown, + query: string, + options: NormalizeOptions | undefined, + select: (raw: unknown) => unknown, +): Promise { try { const started = performance.now() const createQuery = await getCreateQuery() const raw = createQuery(query)(target) + const node = select(raw) const queryMs = Math.round((performance.now() - started) * 100) / 100 - const { data, stats } = normalize(raw, options) + const { data, stats } = normalize(node, options) // The normalizer guarantees plain JSON, so this measures the actual wire payload. const payloadBytes = new TextEncoder().encode(JSON.stringify(data) ?? '').length return { ok: true, result: data, stats: { queryMs, normalize: stats, payloadBytes } } @@ -124,6 +136,10 @@ export async function runQuery(target: unknown, query: string, options?: Normali } } +export function runQuery(target: unknown, query: string, options?: NormalizeOptions): Promise { + return executeQuery(target, query, options, raw => raw) +} + /** * Lazy-expand a depth-truncated node: re-run the base query against the live * object, re-descend to the node the `NodePath` addresses, and normalize just @@ -131,21 +147,8 @@ export async function runQuery(target: unknown, query: string, options?: Normali * 'depth'` marker the client is expanding, so the same filter options must be * threaded through (they shift array indices and drop keys). */ -export async function runQueryAtPath(target: unknown, query: string, path: NodePath, options?: NormalizeOptions): Promise { - try { - const started = performance.now() - const createQuery = await getCreateQuery() - const raw = createQuery(query)(target) - const node = navigate(raw, path, options) - const queryMs = Math.round((performance.now() - started) * 100) / 100 - const { data, stats } = normalize(node, options) - const payloadBytes = new TextEncoder().encode(JSON.stringify(data) ?? '').length - return { ok: true, result: data, stats: { queryMs, normalize: stats, payloadBytes } } - } - catch (error) { - const e = error instanceof Error ? error : new Error(String(error)) - return { ok: false, error: { message: e.message, name: e.name } } - } +export function runQueryAtPath(target: unknown, query: string, path: NodePath, options?: NormalizeOptions): Promise { + return executeQuery(target, query, options, raw => navigate(raw, path, options)) } interface JoraStatEntry { diff --git a/plugins/inspect/src/rpc/functions/_invoke-result.ts b/plugins/inspect/src/rpc/functions/_invoke-result.ts new file mode 100644 index 000000000..de921f555 --- /dev/null +++ b/plugins/inspect/src/rpc/functions/_invoke-result.ts @@ -0,0 +1,27 @@ +import type { InvokeResult } from '../../types' + +/** + * Run an async operation and normalize it into an {@link InvokeResult} + * envelope: time the call, and capture a thrown error into a serializable + * shape rather than propagating it, so the inspector UI can render failures + * inline alongside successes. + */ +export async function toInvokeResult(run: () => Promise): Promise { + const start = Date.now() + try { + const result = await run() + return { ok: true, result, durationMs: Date.now() - start } + } + catch (error) { + const e = error as Error + return { + ok: false, + error: { + name: e?.name ?? 'Error', + message: e?.message ?? String(error), + stack: e?.stack, + }, + durationMs: Date.now() - start, + } + } +} diff --git a/plugins/inspect/src/rpc/functions/execute-command.ts b/plugins/inspect/src/rpc/functions/execute-command.ts index f00bdff5a..2976a6f04 100644 --- a/plugins/inspect/src/rpc/functions/execute-command.ts +++ b/plugins/inspect/src/rpc/functions/execute-command.ts @@ -2,6 +2,7 @@ import type { InvokeResult } from '../../types' import { diagnostics } from '../../diagnostics' import { defineInspectRpc } from './_define' import { resolveHubCommands } from './_hub-commands' +import { toInvokeResult } from './_invoke-result' /** * Execute a hub command by id and return a result envelope, mirroring @@ -24,23 +25,7 @@ export const executeCommand = defineInspectRpc({ if (!host) throw diagnostics.DP_INSPECT_0003({ id }) - const start = Date.now() - try { - const result = await host.execute(id, ...args) - return { ok: true, result, durationMs: Date.now() - start } - } - catch (error) { - const e = error as Error - return { - ok: false, - error: { - name: e?.name ?? 'Error', - message: e?.message ?? String(error), - stack: e?.stack, - }, - durationMs: Date.now() - start, - } - } + return toInvokeResult(() => host.execute(id, ...args)) }, }), }) diff --git a/plugins/inspect/src/rpc/functions/invoke-agent-tool.ts b/plugins/inspect/src/rpc/functions/invoke-agent-tool.ts index 6907907df..71d19fb1c 100644 --- a/plugins/inspect/src/rpc/functions/invoke-agent-tool.ts +++ b/plugins/inspect/src/rpc/functions/invoke-agent-tool.ts @@ -1,28 +1,12 @@ import type { InvokeResult } from '../../types' import { defineInspectRpc } from './_define' +import { toInvokeResult } from './_invoke-result' export const invokeAgentTool = defineInspectRpc({ name: 'devframes:plugin:inspect:invoke-agent-tool', type: 'action', setup: ctx => ({ - handler: async (id: string, args: unknown): Promise => { - const start = Date.now() - try { - const result = await ctx.agent.invoke(id, args) - return { ok: true, result, durationMs: Date.now() - start } - } - catch (error) { - const e = error as Error - return { - ok: false, - error: { - name: e?.name ?? 'Error', - message: e?.message ?? String(error), - stack: e?.stack, - }, - durationMs: Date.now() - start, - } - } - }, + handler: (id: string, args: unknown): Promise => + toInvokeResult(() => ctx.agent.invoke(id, args)), }), }) diff --git a/plugins/inspect/src/rpc/functions/invoke.ts b/plugins/inspect/src/rpc/functions/invoke.ts index 2bc1348b7..f6b520dc6 100644 --- a/plugins/inspect/src/rpc/functions/invoke.ts +++ b/plugins/inspect/src/rpc/functions/invoke.ts @@ -1,6 +1,7 @@ import type { InvokeResult } from '../../types' import { diagnostics } from '../../diagnostics' import { defineInspectRpc } from './_define' +import { toInvokeResult } from './_invoke-result' const INVOKABLE_TYPES = new Set(['query', 'static']) @@ -26,23 +27,7 @@ export const invoke = defineInspectRpc({ if (!INVOKABLE_TYPES.has(type)) throw diagnostics.DP_INSPECT_0002({ name, type }) - const start = Date.now() - try { - const result = await ctx.rpc.invokeLocal(name as any, ...(args as any)) - return { ok: true, result, durationMs: Date.now() - start } - } - catch (error) { - const e = error as Error - return { - ok: false, - error: { - name: e?.name ?? 'Error', - message: e?.message ?? String(error), - stack: e?.stack, - }, - durationMs: Date.now() - start, - } - } + return toInvokeResult(() => ctx.rpc.invokeLocal(name as any, ...(args as any))) }, }), }) diff --git a/plugins/inspect/src/rpc/functions/read-agent-resource.ts b/plugins/inspect/src/rpc/functions/read-agent-resource.ts index 3ae7e86f9..6411faca1 100644 --- a/plugins/inspect/src/rpc/functions/read-agent-resource.ts +++ b/plugins/inspect/src/rpc/functions/read-agent-resource.ts @@ -1,28 +1,12 @@ import type { InvokeResult } from '../../types' import { defineInspectRpc } from './_define' +import { toInvokeResult } from './_invoke-result' export const readAgentResource = defineInspectRpc({ name: 'devframes:plugin:inspect:read-agent-resource', type: 'action', setup: ctx => ({ - handler: async (id: string): Promise => { - const start = Date.now() - try { - const result = await ctx.agent.read(id) - return { ok: true, result, durationMs: Date.now() - start } - } - catch (error) { - const e = error as Error - return { - ok: false, - error: { - name: e?.name ?? 'Error', - message: e?.message ?? String(error), - stack: e?.stack, - }, - durationMs: Date.now() - start, - } - } - }, + handler: (id: string): Promise => + toInvokeResult(() => ctx.agent.read(id)), }), }) diff --git a/plugins/inspect/src/spa/components/AgentSmart.vue b/plugins/inspect/src/spa/components/AgentSmart.vue index b72846a1a..358996b15 100644 --- a/plugins/inspect/src/spa/components/AgentSmart.vue +++ b/plugins/inspect/src/spa/components/AgentSmart.vue @@ -19,12 +19,13 @@ async function fetchData(): Promise { useRefreshProvider(fetchData) onMounted(fetchData) -async function onInvoke(id: string, parsedArgs: unknown) { - if (!rpc.value) +async function runAction(id: string, call: (rpc: NonNullable) => Promise) { + const client = rpc.value + if (!client) return pending[id] = true try { - results[id] = await rpc.value.call('devframes:plugin:inspect:invoke-agent-tool', id, parsedArgs) + results[id] = await call(client) } catch (e) { const err = e as Error @@ -35,20 +36,12 @@ async function onInvoke(id: string, parsedArgs: unknown) { } } -async function onRead(id: string) { - if (!rpc.value) - return - pending[id] = true - try { - results[id] = await rpc.value.call('devframes:plugin:inspect:read-agent-resource', id) - } - catch (e) { - const err = e as Error - results[id] = { ok: false, error: { name: err?.name ?? 'Error', message: err?.message ?? String(e) } } - } - finally { - pending[id] = false - } +function onInvoke(id: string, parsedArgs: unknown) { + return runAction(id, client => client.call('devframes:plugin:inspect:invoke-agent-tool', id, parsedArgs)) +} + +function onRead(id: string) { + return runAction(id, client => client.call('devframes:plugin:inspect:read-agent-resource', id)) }