dsh-model-everything 0.3.0 → 0.4.1

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.
@@ -1,7 +1,7 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools'
2
2
  import type { JsonValue } from './json-value.js'
3
3
  import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, validateModel, verdictOf } from './engine.js'
4
- import { reportCard, reportResultView } from './presentation.js'
4
+ import { cardWithSource, reportCallView, reportCard, reportResultView } from './presentation.js'
5
5
  import { ExportError, exportModel, type ExportFormat } from './exporters.js'
6
6
  import { renderReportText } from './report-view.js'
7
7
 
@@ -52,9 +52,12 @@ export const modelExportTool = defineTool({
52
52
  },
53
53
  // The card a UI shows, separated from the text the model reads.
54
54
  presentationMeta(_args, value) {
55
- return reportCard(value, EXPORT_TOOL_NAME) as unknown as JsonValue
55
+ const report = value as Record<string, unknown>
56
+ return cardWithSource(value, EXPORT_TOOL_NAME, typeof report.primary === 'string' ? report.primary : undefined, typeof report.format === 'string' ? 'artifact: ' + report.format : 'artifact') as unknown as JsonValue
56
57
  },
57
- }, presentResult: reportResultView(EXPORT_TOOL_NAME),
58
+ },
59
+ presentCall: reportCallView(EXPORT_TOOL_NAME),
60
+ presentResult: reportResultView(EXPORT_TOOL_NAME),
58
61
 
59
62
  timeoutMs: 10_000,
60
63
  isConcurrencySafe: () => true,
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { ContentBlock } from '@deepseek-ai/dsh-llm'
18
18
  import { routeFor } from './routing.js'
19
- import type { ToolResultView } from '@deepseek-ai/dsh-tools'
19
+ import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools'
20
20
 
21
21
  /** One finding, as a card needs it: a code to name it and a sentence to read. */
22
22
  export interface CardFinding {
@@ -39,6 +39,22 @@ export interface ReportCard {
39
39
  family: string
40
40
  /** What this family owns, and where the case just outside it goes. One table, not a second prose copy. */
41
41
  route?: { owns: string; instead: string }
42
+ /** `model_edit`: the changes this call applied, as the report lists them. */
43
+ changes?: Array<{ kind: string; what: string; detail?: string }>
44
+ /** `model_edit` and `report_diff`: how the findings moved, when either report could compare. */
45
+ delta?: { added: number; removed: number; changed: number; addedErrors: number }
46
+ /** `report_diff`: the first finding the change adds, so a card can lead with it. */
47
+ firstAdded?: CardFinding
48
+ /** `report_diff`: the current report's own verdict, which differs from the delta's when a clean change lands on a failing report. */
49
+ currentVerdict?: string
50
+ /** The artifact a translation or a rendering produced, as the first lines of it. */
51
+ source?: { label: string; lines: string[]; total: number }
52
+ /** `uml_structure_verify`: the graph's own counts, which are measurements rather than findings. */
53
+ graph?: { nodes: number; edges: number; cycles: number; isolated: number; dangling: number }
54
+ /** `model_design_review`: three levels, because a signal is not a violation and a fact is neither. */
55
+ levels?: { violations: number; signals: number; facts: number; skipped: number }
56
+ /** `model_interleaving_scan`: how many claims were found, and in which families. */
57
+ claims?: { absolute: number; keywords: number; families: Array<{ family: string; count: number }> }
42
58
  }
43
59
 
44
60
  const SHORT = 160
@@ -67,10 +83,34 @@ export function reportCard(value: unknown, family: string): ReportCard {
67
83
  const findings = array(report.findings)
68
84
  .map((entry) => record(entry))
69
85
  .filter((entry): entry is Record<string, unknown> => entry !== undefined)
86
+ // Two shapes live under one payload: the families with a top-level `findings` array, and the verify
87
+ // family, whose findings sit inside `checks` and whose counts live in `summary`. Reading only the first
88
+ // shape made a failing verification card say "0 error(s)" while its own verdict said fail.
89
+ const violations = array(report.violations)
90
+ const signals = array(report.signals)
91
+ const facts = array(report.facts)
92
+ if (findings.length === 0 && violations.length > 0) {
93
+ // The design family keeps three levels apart on purpose: a violation decides the verdict, a signal is a
94
+ // structural fact, and a fact is a measurement. Only the violations are findings here, or the card would
95
+ // show a signal with an error mark.
96
+ for (const entry of violations) {
97
+ const finding = record(entry)
98
+ if (finding !== undefined) findings.push({ ...finding, severity: 'error' })
99
+ }
100
+ } else if (findings.length === 0 && violations.length === 0 && array(report.violations).length === 0 && !Array.isArray(report.violations)) {
101
+ for (const check of array(report.checks)) {
102
+ for (const entry of array(record(check)?.findings)) {
103
+ const finding = record(entry)
104
+ if (finding !== undefined) findings.push(finding)
105
+ }
106
+ }
107
+ }
70
108
  const severity = (entry: Record<string, unknown>): string => String(entry.severity ?? 'info')
71
109
  const errors = findings.filter((entry) => severity(entry) === 'error')
72
110
  const warnings = findings.filter((entry) => severity(entry) === 'warning')
73
111
  const summary = record(report.summary)
112
+ const summaryErrors = typeof summary?.errors === 'number' ? summary.errors : undefined
113
+ const summaryWarnings = typeof summary?.warnings === 'number' ? summary.warnings : undefined
74
114
  const checksRun = typeof summary?.checksRun === 'number'
75
115
  ? summary.checksRun
76
116
  : array(report.checks).length > 0
@@ -78,20 +118,47 @@ export function reportCard(value: unknown, family: string): ReportCard {
78
118
  : undefined
79
119
  const first = errors[0] ?? warnings[0]
80
120
  const next = array(report.nextSteps).map((step) => String(step))[0]
121
+ const route = routeFor(family)
122
+ const deltaSummary = record(record(report.reportDelta)?.summary) ?? record(report.summary)
123
+ const delta = deltaSummary === undefined
124
+ ? undefined
125
+ : {
126
+ added: Number(deltaSummary.added ?? 0),
127
+ removed: Number(deltaSummary.removed ?? 0),
128
+ changed: Number(deltaSummary.changed ?? 0),
129
+ addedErrors: Number(deltaSummary.addedErrors ?? 0),
130
+ }
131
+ const changes = array(report.changes)
132
+ .map((entry) => record(entry))
133
+ .filter((entry): entry is Record<string, unknown> => entry !== undefined)
134
+ .map((entry) => ({ kind: String(entry.kind ?? '?'), what: String(entry.what ?? ''), ...(typeof entry.detail === 'string' && entry.detail !== '' ? { detail: entry.detail } : {}) }))
135
+ const firstAddedRaw = (array(report.added).map((entry) => record(entry)).find((entry) => entry !== undefined && String(entry.severity ?? '') === 'error')
136
+ ?? array(report.added).map((entry) => record(entry)).find((entry) => entry !== undefined))
81
137
  return {
82
138
  ...(typeof report.verdict === 'string' ? { verdict: report.verdict } : {}),
83
139
  ...(typeof report.verdictReason === 'string' ? { verdictReason: shorten(report.verdictReason) } : {}),
84
- errors: errors.length,
85
- warnings: warnings.length,
140
+ errors: summaryErrors ?? errors.length,
141
+ warnings: summaryWarnings ?? warnings.length,
86
142
  ...(checksRun === undefined ? {} : { checksRun }),
87
143
  first: first === undefined
88
144
  ? null
89
145
  : { code: String(first.code ?? 'UNKNOWN'), severity: severity(first), message: shorten(String(first.message ?? '')) },
90
146
  next: next === undefined ? null : shorten(next),
91
147
  family,
92
- ...(routeFor(family) === undefined
148
+ ...(route === undefined ? {} : { route: { owns: route.owns, instead: route.notFor.tool } }),
149
+ ...(changes.length === 0 ? {} : { changes }),
150
+ ...(delta === undefined ? {} : { delta }),
151
+ ...(firstAddedRaw === undefined ? {} : { firstAdded: { code: String(firstAddedRaw.code ?? '?'), severity: String(firstAddedRaw.severity ?? 'info'), message: shorten(String(firstAddedRaw.message ?? '')) } }),
152
+ ...(typeof report.currentVerdict === 'string' ? { currentVerdict: report.currentVerdict } : {}),
153
+ ...(typeof summary?.nodes === 'number' && typeof summary?.edges === 'number'
154
+ ? { graph: { nodes: summary.nodes, edges: summary.edges, cycles: Number(summary.cycles ?? 0), isolated: Number(summary.isolated ?? 0), dangling: Number(summary.dangling ?? 0) } }
155
+ : {}),
156
+ ...(violations.length === 0 && signals.length === 0 && facts.length === 0
93
157
  ? {}
94
- : { route: { owns: routeFor(family)?.owns ?? '', instead: routeFor(family)?.notFor.tool ?? '' } }),
158
+ : { levels: { violations: violations.length, signals: signals.length, facts: facts.length, skipped: array(report.checksSkipped).length } }),
159
+ ...(typeof summary?.absoluteClaims === 'number'
160
+ ? { claims: { absolute: summary.absoluteClaims, keywords: Number(summary.keywords ?? 0), families: claimFamilies(findings) } }
161
+ : {}),
95
162
  }
96
163
  }
97
164
 
@@ -104,10 +171,19 @@ export function cardBlocks(card: ReportCard): ContentBlock[] {
104
171
  : `${String(card.checksRun)} checks · ${String(card.errors)} error(s) · ${String(card.warnings)} warning(s)`
105
172
  lines.push(head + ' — ' + counts)
106
173
  if (card.verdictReason !== undefined) lines.push(card.verdictReason)
107
- if (card.first !== null) {
108
- lines.push((card.first.severity === 'error' ? '✗ ' : '⚠ ') + card.first.code + ': ' + card.first.message)
174
+ if (card.first != null) {
175
+ // A finding with no message prints its code and stops: a colon with nothing after it reads as a
176
+ // truncated message rather than as an absent one.
177
+ const mark = card.first.severity === 'error' ? '✗ ' : '⚠ '
178
+ lines.push(mark + card.first.code + (card.first.message.trim() === '' ? '' : ': ' + card.first.message))
179
+ }
180
+ if (card.next != null) lines.push('next: ' + card.next)
181
+ if (card.source !== undefined) {
182
+ lines.push('')
183
+ const shown = card.source.lines.length
184
+ lines.push(card.source.label + (card.source.total > shown ? ' (first ' + String(shown) + ' of ' + String(card.source.total) + ' lines)' : ''))
185
+ lines.push(...card.source.lines)
109
186
  }
110
- if (card.next !== null) lines.push('next: ' + card.next)
111
187
  return [{ type: 'text', text: lines.join('\n') }]
112
188
  }
113
189
 
@@ -115,11 +191,175 @@ export function cardBlocks(card: ReportCard): ContentBlock[] {
115
191
  * `presentResult` for a report tool: render the card from the persisted payload, and fall back to the
116
192
  * raw content by returning undefined when there is none.
117
193
  */
194
+ /** The last path segment, for a title. No `node:path` here: this runs on replay too. */
195
+ function lastSegment(path: string): string {
196
+ const parts = path.split(/[\\/]/).filter((part) => part !== '' && part !== '.')
197
+ return parts[parts.length - 1] ?? path
198
+ }
199
+
200
+ /**
201
+ * The pending card for a report tool: a title naming the artifact, and the file to follow.
202
+ *
203
+ * From the arguments only, because a UI calls this during streaming and again on replay; the file has not
204
+ * been opened at that point and must not be.
205
+ */
206
+
207
+ /**
208
+ * The card payload with the artifact previewed.
209
+ *
210
+ * A translation and a rendering both produce text a reader wants to see the shape of, and neither should
211
+ * paste the whole file into a card: the preview is bounded, and it says how much it left out. The report
212
+ * keeps the artifact in full for whoever needs it.
213
+ */
214
+ export function cardWithSource(value: unknown, family: string, text: string | undefined, label: string, limit = 14): ReportCard {
215
+ const card = reportCard(value, family)
216
+ if (text === undefined || text.trim() === '') return card
217
+ const all = text.split(String.fromCharCode(10))
218
+ const lines = all.slice(0, limit)
219
+ return { ...card, source: { label, lines, total: all.length } }
220
+ }
221
+
222
+ export function reportCallView(family: string) {
223
+ return (args: unknown): ToolCallView | undefined => {
224
+ const a = record(args) ?? {}
225
+ const path = ['modelPath', 'repoPath', 'path']
226
+ .map((key) => a[key])
227
+ .find((value): value is string => typeof value === 'string' && value !== '')
228
+ const kind = family === 'model_edit' ? 'edit' : family.startsWith('uml_') ? 'search' : 'other'
229
+ const title = path === undefined ? family : family + ' ' + lastSegment(path)
230
+ return { card: 'generic', title, kind, ...(path === undefined ? {} : { locations: [{ path }] }) }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * The card for `model_edit`: what changed, and what the change did to the findings.
236
+ *
237
+ * Both lists are already in the report, so this is a rendering of the same value rather than a second
238
+ * computation - the rule every card here follows.
239
+ */
240
+ export function editCardBlocks(card: ReportCard): ContentBlock[] {
241
+ const changes = card.changes ?? []
242
+ const lines: string[] = []
243
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase()
244
+ lines.push(head + (changes.length === 0 ? ' — no change was applied' : ' — ' + String(changes.length) + ' change(s)'))
245
+ for (const change of changes.slice(0, 6)) {
246
+ lines.push('· ' + change.kind + ' ' + change.what + (change.detail === undefined ? '' : ' (' + change.detail + ')'))
247
+ }
248
+ if (changes.length > 6) lines.push('· … ' + String(changes.length - 6) + ' more in the report')
249
+ if (card.delta !== undefined) {
250
+ lines.push('findings: added ' + String(card.delta.added) + ' (' + String(card.delta.addedErrors) + ' error) · removed ' + String(card.delta.removed) + ' · changed ' + String(card.delta.changed))
251
+ }
252
+ if (card.next != null) lines.push('next: ' + card.next)
253
+ return [{ type: 'text', text: lines.join('\n') }]
254
+ }
255
+
256
+ /**
257
+ * The card for `report_diff`: does this change add anything, read as counts and the first added error.
258
+ *
259
+ * A diff's `verdict` is about the delta, so the card leads with the delta and never with the current
260
+ * report's own verdict - the two differ exactly when a clean change lands on an already-failing report.
261
+ */
262
+ export function diffCardBlocks(card: ReportCard): ContentBlock[] {
263
+ const delta = card.delta
264
+ const lines: string[] = []
265
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase()
266
+ lines.push(head + ' — added ' + String(delta?.added ?? 0) + ' (' + String(delta?.addedErrors ?? 0) + ' error) · removed ' + String(delta?.removed ?? 0) + ' · changed ' + String(delta?.changed ?? 0))
267
+ if (card.currentVerdict !== undefined) lines.push('the current report on its own: ' + card.currentVerdict)
268
+ if (card.firstAdded !== undefined) lines.push('✗ ' + card.firstAdded.code + ': ' + card.firstAdded.message)
269
+ if ((delta?.added ?? 0) === 0) lines.push('nothing was added: this change is accepted by the "no new findings" rule')
270
+ if (card.next != null) lines.push('next: ' + card.next)
271
+ return [{ type: 'text', text: lines.join('\n') }]
272
+ }
273
+
274
+ /** `presentResult` for `model_edit`, falling back to the generic card when the payload has no changes. */
275
+ export function editResultView(family: string) {
276
+ return (_args: unknown, result: { content: ContentBlock[]; meta?: unknown }): ToolResultView | undefined => {
277
+ const card = record(result.meta) as unknown as ReportCard | undefined
278
+ if (card === undefined || card === null) return undefined
279
+ const changeCount = card.changes === undefined ? '' : ' · ' + String(card.changes.length) + ' change(s)'
280
+ return { card: 'generic', title: family + ' ' + (card.verdict ?? '') + changeCount, content: editCardBlocks(card) }
281
+ }
282
+ }
283
+
118
284
  export function reportResultView(family: string) {
119
285
  return (_args: unknown, result: { content: ContentBlock[]; meta?: unknown }): ToolResultView | undefined => {
120
286
  const card = record(result.meta) as unknown as ReportCard | undefined
121
287
  if (card === undefined || card === null) return undefined
122
- if (card.verdict === undefined && card.next === null && card.first === null) return undefined
288
+ if (card.verdict === undefined && card.next == null && card.first == null) return undefined
123
289
  return { card: 'generic', title: family + ' ' + (card.verdict ?? '') , content: cardBlocks(card) }
124
290
  }
125
291
  }
292
+
293
+
294
+ /** How many findings per family, for the scan card: the reader wants the shape, not the list. */
295
+ function claimFamilies(findings: Array<Record<string, unknown>>): Array<{ family: string; count: number }> {
296
+ const counts = new Map<string, number>()
297
+ for (const finding of findings) {
298
+ const family = typeof finding.family === 'string' ? finding.family : 'other'
299
+ counts.set(family, (counts.get(family) ?? 0) + 1)
300
+ }
301
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([family, count]) => ({ family, count }))
302
+ }
303
+
304
+ /** The card for `uml_structure_verify`: the graph's shape first, then the first finding. */
305
+ export function structureCardBlocks(card: ReportCard): ContentBlock[] {
306
+ const lines: string[] = []
307
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase()
308
+ lines.push(head + ' — ' + String(card.errors) + ' error(s) · ' + String(card.warnings) + ' warning(s)')
309
+ if (card.graph !== undefined) {
310
+ lines.push('graph: ' + String(card.graph.nodes) + ' nodes · ' + String(card.graph.edges) + ' edges · ' + String(card.graph.cycles) + ' cycle(s) · ' + String(card.graph.isolated) + ' isolated')
311
+ }
312
+ if (card.first != null) lines.push((card.first.severity === 'error' ? '✗ ' : '⚠ ') + card.first.code + ': ' + card.first.message)
313
+ if (card.next != null) lines.push('next: ' + card.next)
314
+ return [{ type: 'text', text: lines.join(String.fromCharCode(10)) }]
315
+ }
316
+
317
+ /** The card for `model_design_review`: violations, then signals, then facts - never one number. */
318
+ export function designCardBlocks(card: ReportCard): ContentBlock[] {
319
+ const lines: string[] = []
320
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase()
321
+ lines.push(head + ' — ' + String(card.errors) + ' violation(s) · ' + String(card.warnings) + ' signal(s)')
322
+ if (card.levels !== undefined) {
323
+ lines.push('levels: ' + String(card.levels.violations) + ' violation · ' + String(card.levels.signals) + ' signal · ' + String(card.levels.facts) + ' fact · ' + String(card.levels.skipped) + ' check(s) skipped')
324
+ }
325
+ if (card.first != null) lines.push('✗ ' + card.first.code + ': ' + card.first.message)
326
+ if (card.levels !== undefined && card.levels.skipped > 0) lines.push('a skipped check examined nothing: silence there is not a pass')
327
+ if (card.next != null) lines.push('next: ' + card.next)
328
+ return [{ type: 'text', text: lines.join(String.fromCharCode(10)) }]
329
+ }
330
+
331
+ /** The card for `model_interleaving_scan`: claims by family, and the reminder that nothing was proven. */
332
+ export function scanCardBlocks(card: ReportCard): ContentBlock[] {
333
+ const lines: string[] = []
334
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase()
335
+ lines.push(head + ' — ' + String(card.errors) + ' absolute claim(s) · ' + String(card.warnings) + ' risk(s)')
336
+ if (card.claims !== undefined) {
337
+ const families = card.claims.families.map((entry) => entry.family + ' ' + String(entry.count)).join(' · ')
338
+ lines.push('claims: ' + String(card.claims.absolute) + ' absolute, ' + String(card.claims.keywords) + ' keyword(s) hit' + (families === '' ? '' : ' · ' + families))
339
+ }
340
+ if (card.first != null) lines.push('✗ ' + card.first.code + ': ' + card.first.message)
341
+ lines.push('this plugin routed the claims; it proved none of them')
342
+ return [{ type: 'text', text: lines.join(String.fromCharCode(10)) }]
343
+ }
344
+
345
+ /** One `presentResult` per card body, since the shapes differ but the fallback does not. */
346
+ export function familyResultView(family: string, body: (card: ReportCard) => ContentBlock[]) {
347
+ return (_args: unknown, result: { content: ContentBlock[]; meta?: unknown }): ToolResultView | undefined => {
348
+ const card = record(result.meta) as unknown as ReportCard | undefined
349
+ if (card === undefined || card === null) return undefined
350
+ return { card: 'generic', title: family + ' ' + (card.verdict ?? ''), content: body(card) }
351
+ }
352
+ }
353
+
354
+ export const structureResultView = familyResultView('uml_structure_verify', structureCardBlocks)
355
+ export const designResultView = familyResultView('model_design_review', designCardBlocks)
356
+ export const scanResultView = familyResultView('model_interleaving_scan', scanCardBlocks)
357
+
358
+ /** `presentResult` for `report_diff`: the delta leads, never the current report's own verdict. */
359
+ export function diffResultView(family: string) {
360
+ return (_args: unknown, result: { content: ContentBlock[]; meta?: unknown }): ToolResultView | undefined => {
361
+ const card = record(result.meta) as unknown as ReportCard | undefined
362
+ if (card === undefined || card === null) return undefined
363
+ return { card: 'generic', title: family + ' ' + (card.verdict ?? ''), content: diffCardBlocks(card) }
364
+ }
365
+ }
@@ -2,7 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
2
2
  import type { JsonValue } from './json-value.js'
3
3
  import { diffReports } from './baseline.js'
4
4
  import { refusalVerdict } from './engine.js'
5
- import { reportCard, reportResultView } from './presentation.js'
5
+ import { diffResultView, reportCallView, reportCard } from './presentation.js'
6
6
  import { renderReportText } from './report-view.js'
7
7
 
8
8
  export const REPORT_DIFF_TOOL_NAME = 'report_diff'
@@ -59,7 +59,9 @@ export const reportDiffTool = defineTool({
59
59
  presentationMeta(_args, value) {
60
60
  return reportCard(value, REPORT_DIFF_TOOL_NAME) as unknown as JsonValue
61
61
  },
62
- }, presentResult: reportResultView(REPORT_DIFF_TOOL_NAME),
62
+ },
63
+ presentCall: reportCallView(REPORT_DIFF_TOOL_NAME),
64
+ presentResult: diffResultView(REPORT_DIFF_TOOL_NAME),
63
65
 
64
66
  timeoutMs: 10_000,
65
67
  isConcurrencySafe: () => true,
@@ -6,7 +6,7 @@ import { scanDependencyGraph } from './scan.js'
6
6
  import { reviewGranularity, type GranularityDiagram } from './granularity.js'
7
7
  import { OptionShapeError, optionRefusal, requireArrayOption } from './option-shape.js'
8
8
  import { renderReportText } from './report-view.js'
9
- import { reportCard, reportResultView } from './presentation.js'
9
+ import { reportCallView, reportCard, structureResultView } from './presentation.js'
10
10
 
11
11
  export const STRUCTURE_TOOL_NAME = 'uml_structure_verify'
12
12
 
@@ -63,7 +63,8 @@ export const umlStructureTool = defineTool({
63
63
  return reportCard(value, STRUCTURE_TOOL_NAME) as unknown as JsonValue
64
64
  },
65
65
  },
66
- presentResult: reportResultView(STRUCTURE_TOOL_NAME),
66
+ presentCall: reportCallView(STRUCTURE_TOOL_NAME),
67
+ presentResult: structureResultView,
67
68
  timeoutMs: 10_000,
68
69
  isConcurrencySafe: () => true,
69
70
  async execute(args) {
package/src/tool.ts CHANGED
@@ -2,7 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
2
2
  import type { JsonValue } from './json-value.js'
3
3
  import { runVerification } from './engine.js'
4
4
  import { renderReportText } from './report-view.js'
5
- import { reportCard, reportResultView } from './presentation.js'
5
+ import { reportCallView, reportCard, reportResultView } from './presentation.js'
6
6
  import { OptionShapeError, optionRefusal, requireObjectOption } from './option-shape.js'
7
7
  import { oneOf } from './model-file.js'
8
8
 
@@ -65,10 +65,11 @@ export const modelVerifyTool = defineTool({
65
65
  return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }]
66
66
  },
67
67
  // The card a UI shows, separated from the text the model reads.
68
- presentationMeta(_args, value) {
68
+ presentationMeta(args, value) {
69
69
  return reportCard(value, MODEL_VERIFY_TOOL_NAME) as unknown as JsonValue
70
70
  },
71
71
  },
72
+ presentCall: reportCallView(MODEL_VERIFY_TOOL_NAME),
72
73
  presentResult: reportResultView(MODEL_VERIFY_TOOL_NAME),
73
74
  timeoutMs: 10_000,
74
75
  isConcurrencySafe: () => true,
package/src/uml-tool.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  type UmlDiagram, type UmlFinding, type UmlIntent, type UmlNotation, type UmlParseResult,
8
8
  } from './uml.js'
9
9
  import { renderReportText } from './report-view.js'
10
- import { reportCard, reportResultView } from './presentation.js'
10
+ import { cardWithSource, reportCallView, reportCard, reportResultView } from './presentation.js'
11
11
 
12
12
  /**
13
13
  * The UML front end as four tools, one per job.
@@ -63,7 +63,8 @@ const reportOutput = (family: string) => ({
63
63
  // The card a UI shows, separated from the text the model reads. The family is the tool's own name, so
64
64
  // one shared output does not have to guess which tool is calling it.
65
65
  presentationMeta(_args: unknown, value: unknown) {
66
- return reportCard(value, family) as unknown as JsonValue
66
+ const report = value as Record<string, unknown>
67
+ return cardWithSource(value, family, typeof report.diagram === 'string' ? report.diagram : undefined, 'diagram source') as unknown as JsonValue
67
68
  },
68
69
  })
69
70
 
@@ -81,6 +82,7 @@ export const umlRenderTool = defineTool({
81
82
  reportView: reportViewParameter,
82
83
  },
83
84
  output: reportOutput(UML_RENDER_TOOL_NAME),
85
+ presentCall: reportCallView(UML_RENDER_TOOL_NAME),
84
86
  presentResult: reportResultView(UML_RENDER_TOOL_NAME),
85
87
  timeoutMs: 10_000,
86
88
  isConcurrencySafe: () => true,
@@ -119,6 +121,7 @@ export const umlParseTool = defineTool({
119
121
  reportView: reportViewParameter,
120
122
  },
121
123
  output: reportOutput(UML_PARSE_TOOL_NAME),
124
+ presentCall: reportCallView(UML_PARSE_TOOL_NAME),
122
125
  presentResult: reportResultView(UML_PARSE_TOOL_NAME),
123
126
  timeoutMs: 10_000,
124
127
  isConcurrencySafe: () => true,
@@ -171,6 +174,7 @@ export const umlReviewTool = defineTool({
171
174
  reportView: reportViewParameter,
172
175
  },
173
176
  output: reportOutput(UML_REVIEW_TOOL_NAME),
177
+ presentCall: reportCallView(UML_REVIEW_TOOL_NAME),
174
178
  presentResult: reportResultView(UML_REVIEW_TOOL_NAME),
175
179
  timeoutMs: 10_000,
176
180
  isConcurrencySafe: () => true,
@@ -203,6 +207,7 @@ export const umlExplainLabelsTool = defineTool({
203
207
  reportView: reportViewParameter,
204
208
  },
205
209
  output: reportOutput(UML_LABELS_TOOL_NAME),
210
+ presentCall: reportCallView(UML_LABELS_TOOL_NAME),
206
211
  presentResult: reportResultView(UML_LABELS_TOOL_NAME),
207
212
  timeoutMs: 10_000,
208
213
  isConcurrencySafe: () => true,