dsh-model-everything 0.3.0 → 0.4.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/README.en-US.md CHANGED
@@ -44,6 +44,21 @@ reduce the model first, and if it still explodes the property needs a tool that
44
44
  for partial-order reduction, NuSMV for symbolic search, CBMC for bounded SAT. This plugin builds no
45
45
  abstraction and reads no program source.
46
46
 
47
+ ## Report cards
48
+
49
+ A report still prints in full, and gains a card where the UI can render one. The card is not a second
50
+ conclusion: it is the same computation, arranged for a reader.
51
+
52
+ - **The verdict first**, then the first finding, then the next step;
53
+ - **A diff keeps two questions apart**: what this change added, and what the current report decides on its
54
+ own - they differ exactly when a clean change lands on an already-failing report;
55
+ - **A design review keeps three levels apart**: a violation decides the verdict, a signal is a structural
56
+ fact, a fact is a measurement;
57
+ - **A concurrency scan** says it routed the claims and **proved none of them**;
58
+ - Without a card the raw text is what shows, so an unsupported UI loses nothing.
59
+
60
+ A card **saves no tokens**: the model still gets the full text.
61
+
47
62
  ## What it covers
48
63
 
49
64
  | Domain | What it does |
@@ -215,12 +230,17 @@ The plugin ships as a cordis bundle, declared by `dsh.bundle` in the root `packa
215
230
  ```bash
216
231
  # from a source checkout (development)
217
232
  dsh plugin --profile <profile> add "file:$PWD"
218
- # the published version, from npm
219
- dsh plugin --profile <profile> add dsh-model-everything
233
+ # the published version, from npm: pin it
234
+ dsh plugin --profile <profile> add dsh-model-everything@<version>
220
235
  # without a global dsh
221
- npx -p @deepseek-ai/dsh dsh plugin --profile <profile> add dsh-model-everything
236
+ npx -p @deepseek-ai/dsh dsh plugin --profile <profile> add dsh-model-everything@<version>
222
237
  ```
223
238
 
239
+ **Why pin it.** Right after a release, an unpinned install resolves the previous version: the mirror's
240
+ `latest` lags, and pnpm's `minimumReleaseAge` refuses a version that was published minutes ago. With the
241
+ version named, the dsh installer records the exception in the profile's `minimumReleaseAgeExclude` itself;
242
+ check the result with `--dump-config` and the installed copy's `package.json`.
243
+
224
244
  Restart the profile afterwards; `dsh --profile <profile> --dump-config` should list this plugin. Remove it with the same plugin manager.
225
245
 
226
246
  It registers its skills through the standard DSH filesystem skill provider and its tools on `ctx.tools`, plus one model-visible status entry that reports registration state and the model schema versions the bundled engines accept.
package/README.md CHANGED
@@ -35,6 +35,18 @@ DeepSeek Harness 原生插件。把设计与代码里**关于行为的断言**
35
35
 
36
36
  **它判定的是小模型,不是程序。** 引擎显式枚举状态,模型炸开时会报 `truncated` 并点名未决的检查。那不是重试一个更大的 `maxStates` 就能解决的:先减模型(去掉性质不读的变量、合并对称状态),仍然炸开就换不靠枚举的工具——SPIN 的偏序归约、NuSMV 的符号化、CBMC 的有界 SAT。本插件不建立抽象,也不读程序源码。
37
37
 
38
+ ## 报告卡片
39
+
40
+ 每份报告在终端里照常显示全文,同时在支持渲染的界面里给一张卡片。这不是第二份结论:卡片是**同一次计算**的另一种排布。
41
+
42
+ - **先给判定**,再给首条问题,最后给下一步;
43
+ - **diff 把两件事分开写**:这次改动新增了什么,以及当前报告本身是什么判定——它们只会在「干净改动落到一份本来就失败的报告上」时不一致;
44
+ - **设计审阅分三级**:violation 决定判定,signal 是结构事实,fact 是测量;三者不会合并成一个数字;
45
+ - **交错扫描**读完只说明「已把断言路由给能判定它的工具」,卡片上写着它**没有证明任何一条**;
46
+ - 没有卡片时退回原始文本,不会因为界面不支持而丢掉结论。
47
+
48
+ 卡片**不节省任何 token**:模型看到的仍是全文。
49
+
38
50
  ## 能力范围
39
51
 
40
52
  | 域 | 做什么 |
@@ -206,12 +218,14 @@ model_verify model=examples/control-dispatcher.json
206
218
  ```bash
207
219
  # 从源码目录安装(开发中)
208
220
  dsh plugin --profile <profile> add "file:$PWD"
209
- # 从 npm 安装(发布版)
210
- dsh plugin --profile <profile> add dsh-model-everything
221
+ # 从 npm 安装(发布版):**钉住版本**
222
+ dsh plugin --profile <profile> add dsh-model-everything@<version>
211
223
  # 未全局安装 dsh 时
212
- npx -p @deepseek-ai/dsh dsh plugin --profile <profile> add dsh-model-everything
224
+ npx -p @deepseek-ai/dsh dsh plugin --profile <profile> add dsh-model-everything@<version>
213
225
  ```
214
226
 
227
+ **为什么要钉版本**:刚发布的版本,不钉版本会装到上一个。两个原因叠加——镜像源的 `latest` 同步有延迟,而 pnpm 的 `minimumReleaseAge` 会拒收刚发布的版本。钉住版本时 dsh 的安装器会自动把这一条写进 profile 的 `minimumReleaseAgeExclude`,装完用 `--dump-config` 与安装副本的 `package.json` 核对版本。
228
+
215
229
  装完重启该 profile,`dsh --profile <profile> --dump-config` 里应能看到本插件的行。卸载同样走插件管理器,从目标 profile 移除。
216
230
 
217
231
  技能走 DSH 标准的 filesystem skill provider 注册,工具挂在 `ctx.tools` 上。另有一条模型可见的状态目录条目,可查注册状态与引擎接受的 schema 版本。
@@ -9,7 +9,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
9
9
  import { verifyAutomaton } from './automaton.js';
10
10
  import { optionRefusal } from './option-shape.js';
11
11
  import { renderReportText } from './report-view.js';
12
- import { reportCard, reportResultView } from './presentation.js';
12
+ import { reportCallView, reportCard, reportResultView } from './presentation.js';
13
13
  export const MODEL_AUTOMATON_TOOL_NAME = 'model_automaton_verify';
14
14
  export const modelAutomatonTool = defineTool({
15
15
  name: MODEL_AUTOMATON_TOOL_NAME,
@@ -48,10 +48,11 @@ export const modelAutomatonTool = defineTool({
48
48
  },
49
49
  // The card a UI shows, separated from the text the model reads: a verdict and the first thing wrong,
50
50
  // never the whole report.
51
- presentationMeta(_args, value) {
51
+ presentationMeta(args, value) {
52
52
  return reportCard(value, MODEL_AUTOMATON_TOOL_NAME);
53
53
  },
54
54
  },
55
+ presentCall: reportCallView(MODEL_AUTOMATON_TOOL_NAME),
55
56
  presentResult: reportResultView(MODEL_AUTOMATON_TOOL_NAME),
56
57
  timeoutMs: 10_000,
57
58
  isConcurrencySafe: () => true,
package/lib/automaton.js CHANGED
@@ -234,9 +234,6 @@ export function verifyAutomaton(input, options = {}) {
234
234
  for (const [index, label] of dfa.labels.entries()) {
235
235
  if (label.length === 0)
236
236
  continue;
237
- if (label.length === 1 && !reach[index]) {
238
- findings.push({ code: 'AUT001_UNREACHABLE_STATE', severity: 'warning', message: `state "${label[0]}" cannot be reached from init` });
239
- }
240
237
  if (!dfa.accept[index] && reach[index] && !canAccept[index]) {
241
238
  findings.push({ code: 'AUT002_TRAP_STATE', severity: 'warning', message: `state "${label[0]}" is reachable but can never reach an accepting state` });
242
239
  }
@@ -273,10 +270,13 @@ export function verifyAutomaton(input, options = {}) {
273
270
  findings.push({ code: 'AUT009_UNDECIDED', severity: 'warning', message: 'the product search stopped at its state cap before deciding the relation, so equality is neither proven nor refuted', detail: 'the product of two automata can be exponential; reduce one of them, or decide the relation with a tool that searches symbolically' });
274
271
  }
275
272
  else if (relation === 'equivalent') {
276
- if (missing !== undefined || extra !== undefined) {
277
- const word = missing ?? extra;
278
- witness = { word: word ?? [], reason: missing !== undefined ? 'accepted by the first automaton, rejected by the second' : 'accepted by the second automaton, rejected by the first' };
279
- findings.push({ code: 'AUT006_NOT_EQUIVALENT', severity: 'error', message: 'the two automata accept different languages', detail: `shortest witness: ${word === undefined || word.length === 0 ? 'the empty word' : word.join(' ')}; ${witness.reason}` });
273
+ if (missing !== undefined) {
274
+ witness = { word: missing, reason: 'accepted by the first automaton, rejected by the second' };
275
+ findings.push({ code: 'AUT006_NOT_EQUIVALENT', severity: 'error', message: 'the two automata accept different languages', detail: `shortest witness: ${missing.length === 0 ? 'the empty word' : missing.join(' ')}; ${witness.reason}` });
276
+ }
277
+ else if (extra !== undefined) {
278
+ witness = { word: extra, reason: 'accepted by the second automaton, rejected by the first' };
279
+ findings.push({ code: 'AUT006_NOT_EQUIVALENT', severity: 'error', message: 'the two automata accept different languages', detail: `shortest witness: ${extra.length === 0 ? 'the empty word' : extra.join(' ')}; ${witness.reason}` });
280
280
  }
281
281
  }
282
282
  else if (relation === 'subset') {
@@ -291,11 +291,12 @@ export function verifyAutomaton(input, options = {}) {
291
291
  }
292
292
  }
293
293
  }
294
- const errors = findings.filter((finding) => finding.severity === 'error').length;
294
+ const errorFindings = findings.filter((finding) => finding.severity === 'error');
295
+ const errors = errorFindings.length;
295
296
  const warnings = findings.filter((finding) => finding.severity === 'warning').length;
296
297
  const verdict = errors > 0 ? 'fail' : warnings > 0 ? 'pass_with_findings' : 'pass';
297
298
  const reason = errors > 0
298
- ? `${String(errors)} error finding(s) (first: ${findings.find((finding) => finding.severity === 'error')?.code ?? 'unknown'})`
299
+ ? `${String(errors)} error finding(s) (first: ${errorFindings[0].code})`
299
300
  : warnings > 0
300
301
  ? `no error findings; ${String(warnings)} warning finding(s)`
301
302
  : 'every check passed';
@@ -1,6 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { runCompositionVerification } from './engine.js';
3
- import { reportCard, reportResultView } from './presentation.js';
3
+ import { reportCallView, reportCard, reportResultView } from './presentation.js';
4
4
  import { OptionShapeError, optionRefusal, requireArrayOption } from './option-shape.js';
5
5
  import { renderReportText } from './report-view.js';
6
6
  export const COMPOSE_TOOL_NAME = 'model_compose_verify';
@@ -56,7 +56,9 @@ export const modelComposeTool = defineTool({
56
56
  presentationMeta(_args, value) {
57
57
  return reportCard(value, COMPOSE_TOOL_NAME);
58
58
  },
59
- }, presentResult: reportResultView(COMPOSE_TOOL_NAME),
59
+ },
60
+ presentCall: reportCallView(COMPOSE_TOOL_NAME),
61
+ presentResult: reportResultView(COMPOSE_TOOL_NAME),
60
62
  timeoutMs: 10_000,
61
63
  isConcurrencySafe: () => true,
62
64
  async execute(args) {
@@ -1,7 +1,7 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { runConcurrencyScan } from './concurrency.js';
3
3
  import { renderReportText } from './report-view.js';
4
- import { reportCard, reportResultView } from './presentation.js';
4
+ import { reportCallView, reportCard, scanResultView } from './presentation.js';
5
5
  export const INTERLEAVING_SCAN_TOOL_NAME = 'model_interleaving_scan';
6
6
  /**
7
7
  * Model-visible DSH tool that mines design documents/plans for concurrency-related
@@ -36,7 +36,9 @@ export const modelConcurrencyScanTool = defineTool({
36
36
  presentationMeta(_args, value) {
37
37
  return reportCard(value, INTERLEAVING_SCAN_TOOL_NAME);
38
38
  },
39
- }, presentResult: reportResultView(INTERLEAVING_SCAN_TOOL_NAME),
39
+ },
40
+ presentCall: reportCallView(INTERLEAVING_SCAN_TOOL_NAME),
41
+ presentResult: scanResultView,
40
42
  timeoutMs: 10_000,
41
43
  isConcurrencySafe: () => true,
42
44
  async execute(args) {
package/lib/data-tool.js CHANGED
@@ -3,7 +3,7 @@ import { runDataVerification, DATA_ENGINE_SCHEMA_VERSION } from './data-engine.j
3
3
  import { oneOf } from './model-file.js';
4
4
  import { optionRefusal, requireArrayOption, requireObjectOption } from './option-shape.js';
5
5
  import { renderReportText } from './report-view.js';
6
- import { reportCard, reportResultView } from './presentation.js';
6
+ import { reportCallView, reportCard, reportResultView } from './presentation.js';
7
7
  export const DATAMODEL_VERIFY_TOOL_NAME = 'model_datamodel_verify';
8
8
  /**
9
9
  * Model-visible DSH tool wrapping the bundled data-model verification engine.
@@ -65,7 +65,9 @@ export const modelDataModelVerifyTool = defineTool({
65
65
  presentationMeta(_args, value) {
66
66
  return reportCard(value, DATAMODEL_VERIFY_TOOL_NAME);
67
67
  },
68
- }, presentResult: reportResultView(DATAMODEL_VERIFY_TOOL_NAME),
68
+ },
69
+ presentCall: reportCallView(DATAMODEL_VERIFY_TOOL_NAME),
70
+ presentResult: reportResultView(DATAMODEL_VERIFY_TOOL_NAME),
69
71
  timeoutMs: 10_000,
70
72
  isConcurrencySafe: () => true,
71
73
  async execute(args) {
@@ -1,6 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { REPORT_SCHEMAS } from './engine.js';
3
- import { reportCard, reportResultView } from './presentation.js';
3
+ import { designResultView, reportCallView, reportCard } from './presentation.js';
4
4
  import { oneOf } from './model-file.js';
5
5
  import { runDesignReview } from './design-engine.js';
6
6
  import { renderReportText } from './report-view.js';
@@ -48,7 +48,9 @@ export const designTool = defineTool({
48
48
  presentationMeta(_args, value) {
49
49
  return reportCard(value, DESIGN_TOOL_NAME);
50
50
  },
51
- }, presentResult: reportResultView(DESIGN_TOOL_NAME),
51
+ },
52
+ presentCall: reportCallView(DESIGN_TOOL_NAME),
53
+ presentResult: designResultView,
52
54
  timeoutMs: 10_000,
53
55
  isConcurrencySafe: () => true,
54
56
  async execute(args) {
package/lib/edit-tool.js CHANGED
@@ -13,7 +13,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
13
13
  import { diffReports } from './baseline.js';
14
14
  import { applyEdits, readModelFile } from './editor.js';
15
15
  import { REPORT_SCHEMAS, runVerification } from './engine.js';
16
- import { reportCard, reportResultView } from './presentation.js';
16
+ import { editResultView, reportCallView, reportCard } from './presentation.js';
17
17
  import { optionRefusal } from './option-shape.js';
18
18
  import { renderReportText } from './report-view.js';
19
19
  export const MODEL_EDIT_TOOL_NAME = 'model_edit';
@@ -60,7 +60,9 @@ export const modelEditTool = defineTool({
60
60
  presentationMeta(_args, value) {
61
61
  return reportCard(value, MODEL_EDIT_TOOL_NAME);
62
62
  },
63
- }, presentResult: reportResultView(MODEL_EDIT_TOOL_NAME),
63
+ },
64
+ presentCall: reportCallView(MODEL_EDIT_TOOL_NAME),
65
+ presentResult: editResultView(MODEL_EDIT_TOOL_NAME),
64
66
  timeoutMs: 20_000,
65
67
  isConcurrencySafe: () => false,
66
68
  async execute(args) {
@@ -1,6 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, validateModel, verdictOf } from './engine.js';
3
- import { reportCard, reportResultView } from './presentation.js';
3
+ import { cardWithSource, reportCallView, reportCard, reportResultView } from './presentation.js';
4
4
  import { ExportError, exportModel } from './exporters.js';
5
5
  import { renderReportText } from './report-view.js';
6
6
  /** The model hash of an export's input, when the input validates (the success path always does). */
@@ -47,9 +47,12 @@ export const modelExportTool = defineTool({
47
47
  },
48
48
  // The card a UI shows, separated from the text the model reads.
49
49
  presentationMeta(_args, value) {
50
- return reportCard(value, EXPORT_TOOL_NAME);
50
+ const report = value;
51
+ return cardWithSource(value, EXPORT_TOOL_NAME, typeof report.primary === 'string' ? report.primary : undefined, typeof report.format === 'string' ? 'artifact: ' + report.format : 'artifact');
51
52
  },
52
- }, presentResult: reportResultView(EXPORT_TOOL_NAME),
53
+ },
54
+ presentCall: reportCallView(EXPORT_TOOL_NAME),
55
+ presentResult: reportResultView(EXPORT_TOOL_NAME),
53
56
  timeoutMs: 10_000,
54
57
  isConcurrencySafe: () => true,
55
58
  async execute(args) {
@@ -21,10 +21,37 @@ export function reportCard(value, family) {
21
21
  const findings = array(report.findings)
22
22
  .map((entry) => record(entry))
23
23
  .filter((entry) => entry !== undefined);
24
+ // Two shapes live under one payload: the families with a top-level `findings` array, and the verify
25
+ // family, whose findings sit inside `checks` and whose counts live in `summary`. Reading only the first
26
+ // shape made a failing verification card say "0 error(s)" while its own verdict said fail.
27
+ const violations = array(report.violations);
28
+ const signals = array(report.signals);
29
+ const facts = array(report.facts);
30
+ if (findings.length === 0 && violations.length > 0) {
31
+ // The design family keeps three levels apart on purpose: a violation decides the verdict, a signal is a
32
+ // structural fact, and a fact is a measurement. Only the violations are findings here, or the card would
33
+ // show a signal with an error mark.
34
+ for (const entry of violations) {
35
+ const finding = record(entry);
36
+ if (finding !== undefined)
37
+ findings.push({ ...finding, severity: 'error' });
38
+ }
39
+ }
40
+ else if (findings.length === 0 && violations.length === 0 && array(report.violations).length === 0 && !Array.isArray(report.violations)) {
41
+ for (const check of array(report.checks)) {
42
+ for (const entry of array(record(check)?.findings)) {
43
+ const finding = record(entry);
44
+ if (finding !== undefined)
45
+ findings.push(finding);
46
+ }
47
+ }
48
+ }
24
49
  const severity = (entry) => String(entry.severity ?? 'info');
25
50
  const errors = findings.filter((entry) => severity(entry) === 'error');
26
51
  const warnings = findings.filter((entry) => severity(entry) === 'warning');
27
52
  const summary = record(report.summary);
53
+ const summaryErrors = typeof summary?.errors === 'number' ? summary.errors : undefined;
54
+ const summaryWarnings = typeof summary?.warnings === 'number' ? summary.warnings : undefined;
28
55
  const checksRun = typeof summary?.checksRun === 'number'
29
56
  ? summary.checksRun
30
57
  : array(report.checks).length > 0
@@ -32,20 +59,47 @@ export function reportCard(value, family) {
32
59
  : undefined;
33
60
  const first = errors[0] ?? warnings[0];
34
61
  const next = array(report.nextSteps).map((step) => String(step))[0];
62
+ const route = routeFor(family);
63
+ const deltaSummary = record(record(report.reportDelta)?.summary) ?? record(report.summary);
64
+ const delta = deltaSummary === undefined
65
+ ? undefined
66
+ : {
67
+ added: Number(deltaSummary.added ?? 0),
68
+ removed: Number(deltaSummary.removed ?? 0),
69
+ changed: Number(deltaSummary.changed ?? 0),
70
+ addedErrors: Number(deltaSummary.addedErrors ?? 0),
71
+ };
72
+ const changes = array(report.changes)
73
+ .map((entry) => record(entry))
74
+ .filter((entry) => entry !== undefined)
75
+ .map((entry) => ({ kind: String(entry.kind ?? '?'), what: String(entry.what ?? ''), ...(typeof entry.detail === 'string' && entry.detail !== '' ? { detail: entry.detail } : {}) }));
76
+ const firstAddedRaw = (array(report.added).map((entry) => record(entry)).find((entry) => entry !== undefined && String(entry.severity ?? '') === 'error')
77
+ ?? array(report.added).map((entry) => record(entry)).find((entry) => entry !== undefined));
35
78
  return {
36
79
  ...(typeof report.verdict === 'string' ? { verdict: report.verdict } : {}),
37
80
  ...(typeof report.verdictReason === 'string' ? { verdictReason: shorten(report.verdictReason) } : {}),
38
- errors: errors.length,
39
- warnings: warnings.length,
81
+ errors: summaryErrors ?? errors.length,
82
+ warnings: summaryWarnings ?? warnings.length,
40
83
  ...(checksRun === undefined ? {} : { checksRun }),
41
84
  first: first === undefined
42
85
  ? null
43
86
  : { code: String(first.code ?? 'UNKNOWN'), severity: severity(first), message: shorten(String(first.message ?? '')) },
44
87
  next: next === undefined ? null : shorten(next),
45
88
  family,
46
- ...(routeFor(family) === undefined
89
+ ...(route === undefined ? {} : { route: { owns: route.owns, instead: route.notFor.tool } }),
90
+ ...(changes.length === 0 ? {} : { changes }),
91
+ ...(delta === undefined ? {} : { delta }),
92
+ ...(firstAddedRaw === undefined ? {} : { firstAdded: { code: String(firstAddedRaw.code ?? '?'), severity: String(firstAddedRaw.severity ?? 'info'), message: shorten(String(firstAddedRaw.message ?? '')) } }),
93
+ ...(typeof report.currentVerdict === 'string' ? { currentVerdict: report.currentVerdict } : {}),
94
+ ...(typeof summary?.nodes === 'number' && typeof summary?.edges === 'number'
95
+ ? { graph: { nodes: summary.nodes, edges: summary.edges, cycles: Number(summary.cycles ?? 0), isolated: Number(summary.isolated ?? 0), dangling: Number(summary.dangling ?? 0) } }
96
+ : {}),
97
+ ...(violations.length === 0 && signals.length === 0 && facts.length === 0
47
98
  ? {}
48
- : { route: { owns: routeFor(family)?.owns ?? '', instead: routeFor(family)?.notFor.tool ?? '' } }),
99
+ : { levels: { violations: violations.length, signals: signals.length, facts: facts.length, skipped: array(report.checksSkipped).length } }),
100
+ ...(typeof summary?.absoluteClaims === 'number'
101
+ ? { claims: { absolute: summary.absoluteClaims, keywords: Number(summary.keywords ?? 0), families: claimFamilies(findings) } }
102
+ : {}),
49
103
  };
50
104
  }
51
105
  /** The card body: four lines, in the order a reader asks the questions. */
@@ -58,24 +112,198 @@ export function cardBlocks(card) {
58
112
  lines.push(head + ' — ' + counts);
59
113
  if (card.verdictReason !== undefined)
60
114
  lines.push(card.verdictReason);
61
- if (card.first !== null) {
62
- lines.push((card.first.severity === 'error' ? '✗ ' : '⚠ ') + card.first.code + ': ' + card.first.message);
115
+ if (card.first != null) {
116
+ // A finding with no message prints its code and stops: a colon with nothing after it reads as a
117
+ // truncated message rather than as an absent one.
118
+ const mark = card.first.severity === 'error' ? '✗ ' : '⚠ ';
119
+ lines.push(mark + card.first.code + (card.first.message.trim() === '' ? '' : ': ' + card.first.message));
63
120
  }
64
- if (card.next !== null)
121
+ if (card.next != null)
65
122
  lines.push('next: ' + card.next);
123
+ if (card.source !== undefined) {
124
+ lines.push('');
125
+ const shown = card.source.lines.length;
126
+ lines.push(card.source.label + (card.source.total > shown ? ' (first ' + String(shown) + ' of ' + String(card.source.total) + ' lines)' : ''));
127
+ lines.push(...card.source.lines);
128
+ }
66
129
  return [{ type: 'text', text: lines.join('\n') }];
67
130
  }
68
131
  /**
69
132
  * `presentResult` for a report tool: render the card from the persisted payload, and fall back to the
70
133
  * raw content by returning undefined when there is none.
71
134
  */
135
+ /** The last path segment, for a title. No `node:path` here: this runs on replay too. */
136
+ function lastSegment(path) {
137
+ const parts = path.split(/[\\/]/).filter((part) => part !== '' && part !== '.');
138
+ return parts[parts.length - 1] ?? path;
139
+ }
140
+ /**
141
+ * The pending card for a report tool: a title naming the artifact, and the file to follow.
142
+ *
143
+ * From the arguments only, because a UI calls this during streaming and again on replay; the file has not
144
+ * been opened at that point and must not be.
145
+ */
146
+ /**
147
+ * The card payload with the artifact previewed.
148
+ *
149
+ * A translation and a rendering both produce text a reader wants to see the shape of, and neither should
150
+ * paste the whole file into a card: the preview is bounded, and it says how much it left out. The report
151
+ * keeps the artifact in full for whoever needs it.
152
+ */
153
+ export function cardWithSource(value, family, text, label, limit = 14) {
154
+ const card = reportCard(value, family);
155
+ if (text === undefined || text.trim() === '')
156
+ return card;
157
+ const all = text.split(String.fromCharCode(10));
158
+ const lines = all.slice(0, limit);
159
+ return { ...card, source: { label, lines, total: all.length } };
160
+ }
161
+ export function reportCallView(family) {
162
+ return (args) => {
163
+ const a = record(args) ?? {};
164
+ const path = ['modelPath', 'repoPath', 'path']
165
+ .map((key) => a[key])
166
+ .find((value) => typeof value === 'string' && value !== '');
167
+ const kind = family === 'model_edit' ? 'edit' : family.startsWith('uml_') ? 'search' : 'other';
168
+ const title = path === undefined ? family : family + ' ' + lastSegment(path);
169
+ return { card: 'generic', title, kind, ...(path === undefined ? {} : { locations: [{ path }] }) };
170
+ };
171
+ }
172
+ /**
173
+ * The card for `model_edit`: what changed, and what the change did to the findings.
174
+ *
175
+ * Both lists are already in the report, so this is a rendering of the same value rather than a second
176
+ * computation - the rule every card here follows.
177
+ */
178
+ export function editCardBlocks(card) {
179
+ const changes = card.changes ?? [];
180
+ const lines = [];
181
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase();
182
+ lines.push(head + (changes.length === 0 ? ' — no change was applied' : ' — ' + String(changes.length) + ' change(s)'));
183
+ for (const change of changes.slice(0, 6)) {
184
+ lines.push('· ' + change.kind + ' ' + change.what + (change.detail === undefined ? '' : ' (' + change.detail + ')'));
185
+ }
186
+ if (changes.length > 6)
187
+ lines.push('· … ' + String(changes.length - 6) + ' more in the report');
188
+ if (card.delta !== undefined) {
189
+ lines.push('findings: added ' + String(card.delta.added) + ' (' + String(card.delta.addedErrors) + ' error) · removed ' + String(card.delta.removed) + ' · changed ' + String(card.delta.changed));
190
+ }
191
+ if (card.next != null)
192
+ lines.push('next: ' + card.next);
193
+ return [{ type: 'text', text: lines.join('\n') }];
194
+ }
195
+ /**
196
+ * The card for `report_diff`: does this change add anything, read as counts and the first added error.
197
+ *
198
+ * A diff's `verdict` is about the delta, so the card leads with the delta and never with the current
199
+ * report's own verdict - the two differ exactly when a clean change lands on an already-failing report.
200
+ */
201
+ export function diffCardBlocks(card) {
202
+ const delta = card.delta;
203
+ const lines = [];
204
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase();
205
+ lines.push(head + ' — added ' + String(delta?.added ?? 0) + ' (' + String(delta?.addedErrors ?? 0) + ' error) · removed ' + String(delta?.removed ?? 0) + ' · changed ' + String(delta?.changed ?? 0));
206
+ if (card.currentVerdict !== undefined)
207
+ lines.push('the current report on its own: ' + card.currentVerdict);
208
+ if (card.firstAdded !== undefined)
209
+ lines.push('✗ ' + card.firstAdded.code + ': ' + card.firstAdded.message);
210
+ if ((delta?.added ?? 0) === 0)
211
+ lines.push('nothing was added: this change is accepted by the "no new findings" rule');
212
+ if (card.next != null)
213
+ lines.push('next: ' + card.next);
214
+ return [{ type: 'text', text: lines.join('\n') }];
215
+ }
216
+ /** `presentResult` for `model_edit`, falling back to the generic card when the payload has no changes. */
217
+ export function editResultView(family) {
218
+ return (_args, result) => {
219
+ const card = record(result.meta);
220
+ if (card === undefined || card === null)
221
+ return undefined;
222
+ const changeCount = card.changes === undefined ? '' : ' · ' + String(card.changes.length) + ' change(s)';
223
+ return { card: 'generic', title: family + ' ' + (card.verdict ?? '') + changeCount, content: editCardBlocks(card) };
224
+ };
225
+ }
72
226
  export function reportResultView(family) {
73
227
  return (_args, result) => {
74
228
  const card = record(result.meta);
75
229
  if (card === undefined || card === null)
76
230
  return undefined;
77
- if (card.verdict === undefined && card.next === null && card.first === null)
231
+ if (card.verdict === undefined && card.next == null && card.first == null)
78
232
  return undefined;
79
233
  return { card: 'generic', title: family + ' ' + (card.verdict ?? ''), content: cardBlocks(card) };
80
234
  };
81
235
  }
236
+ /** How many findings per family, for the scan card: the reader wants the shape, not the list. */
237
+ function claimFamilies(findings) {
238
+ const counts = new Map();
239
+ for (const finding of findings) {
240
+ const family = typeof finding.family === 'string' ? finding.family : 'other';
241
+ counts.set(family, (counts.get(family) ?? 0) + 1);
242
+ }
243
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([family, count]) => ({ family, count }));
244
+ }
245
+ /** The card for `uml_structure_verify`: the graph's shape first, then the first finding. */
246
+ export function structureCardBlocks(card) {
247
+ const lines = [];
248
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase();
249
+ lines.push(head + ' — ' + String(card.errors) + ' error(s) · ' + String(card.warnings) + ' warning(s)');
250
+ if (card.graph !== undefined) {
251
+ lines.push('graph: ' + String(card.graph.nodes) + ' nodes · ' + String(card.graph.edges) + ' edges · ' + String(card.graph.cycles) + ' cycle(s) · ' + String(card.graph.isolated) + ' isolated');
252
+ }
253
+ if (card.first != null)
254
+ lines.push((card.first.severity === 'error' ? '✗ ' : '⚠ ') + card.first.code + ': ' + card.first.message);
255
+ if (card.next != null)
256
+ lines.push('next: ' + card.next);
257
+ return [{ type: 'text', text: lines.join(String.fromCharCode(10)) }];
258
+ }
259
+ /** The card for `model_design_review`: violations, then signals, then facts - never one number. */
260
+ export function designCardBlocks(card) {
261
+ const lines = [];
262
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase();
263
+ lines.push(head + ' — ' + String(card.errors) + ' violation(s) · ' + String(card.warnings) + ' signal(s)');
264
+ if (card.levels !== undefined) {
265
+ 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');
266
+ }
267
+ if (card.first != null)
268
+ lines.push('✗ ' + card.first.code + ': ' + card.first.message);
269
+ if (card.levels !== undefined && card.levels.skipped > 0)
270
+ lines.push('a skipped check examined nothing: silence there is not a pass');
271
+ if (card.next != null)
272
+ lines.push('next: ' + card.next);
273
+ return [{ type: 'text', text: lines.join(String.fromCharCode(10)) }];
274
+ }
275
+ /** The card for `model_interleaving_scan`: claims by family, and the reminder that nothing was proven. */
276
+ export function scanCardBlocks(card) {
277
+ const lines = [];
278
+ const head = card.verdict === undefined ? card.family : card.verdict.toUpperCase();
279
+ lines.push(head + ' — ' + String(card.errors) + ' absolute claim(s) · ' + String(card.warnings) + ' risk(s)');
280
+ if (card.claims !== undefined) {
281
+ const families = card.claims.families.map((entry) => entry.family + ' ' + String(entry.count)).join(' · ');
282
+ lines.push('claims: ' + String(card.claims.absolute) + ' absolute, ' + String(card.claims.keywords) + ' keyword(s) hit' + (families === '' ? '' : ' · ' + families));
283
+ }
284
+ if (card.first != null)
285
+ lines.push('✗ ' + card.first.code + ': ' + card.first.message);
286
+ lines.push('this plugin routed the claims; it proved none of them');
287
+ return [{ type: 'text', text: lines.join(String.fromCharCode(10)) }];
288
+ }
289
+ /** One `presentResult` per card body, since the shapes differ but the fallback does not. */
290
+ export function familyResultView(family, body) {
291
+ return (_args, result) => {
292
+ const card = record(result.meta);
293
+ if (card === undefined || card === null)
294
+ return undefined;
295
+ return { card: 'generic', title: family + ' ' + (card.verdict ?? ''), content: body(card) };
296
+ };
297
+ }
298
+ export const structureResultView = familyResultView('uml_structure_verify', structureCardBlocks);
299
+ export const designResultView = familyResultView('model_design_review', designCardBlocks);
300
+ export const scanResultView = familyResultView('model_interleaving_scan', scanCardBlocks);
301
+ /** `presentResult` for `report_diff`: the delta leads, never the current report's own verdict. */
302
+ export function diffResultView(family) {
303
+ return (_args, result) => {
304
+ const card = record(result.meta);
305
+ if (card === undefined || card === null)
306
+ return undefined;
307
+ return { card: 'generic', title: family + ' ' + (card.verdict ?? ''), content: diffCardBlocks(card) };
308
+ };
309
+ }
@@ -1,7 +1,7 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { diffReports } from './baseline.js';
3
3
  import { refusalVerdict } from './engine.js';
4
- import { reportCard, reportResultView } from './presentation.js';
4
+ import { diffResultView, reportCallView, reportCard } from './presentation.js';
5
5
  import { renderReportText } from './report-view.js';
6
6
  export const REPORT_DIFF_TOOL_NAME = 'report_diff';
7
7
  /**
@@ -54,7 +54,9 @@ export const reportDiffTool = defineTool({
54
54
  presentationMeta(_args, value) {
55
55
  return reportCard(value, REPORT_DIFF_TOOL_NAME);
56
56
  },
57
- }, presentResult: reportResultView(REPORT_DIFF_TOOL_NAME),
57
+ },
58
+ presentCall: reportCallView(REPORT_DIFF_TOOL_NAME),
59
+ presentResult: diffResultView(REPORT_DIFF_TOOL_NAME),
58
60
  timeoutMs: 10_000,
59
61
  isConcurrencySafe: () => true,
60
62
  async execute(args) {
@@ -5,7 +5,7 @@ import { scanDependencyGraph } from './scan.js';
5
5
  import { reviewGranularity } from './granularity.js';
6
6
  import { OptionShapeError, optionRefusal, requireArrayOption } from './option-shape.js';
7
7
  import { renderReportText } from './report-view.js';
8
- import { reportCard, reportResultView } from './presentation.js';
8
+ import { reportCallView, reportCard, structureResultView } from './presentation.js';
9
9
  export const STRUCTURE_TOOL_NAME = 'uml_structure_verify';
10
10
  /**
11
11
  * Model-visible DSH tool that audits a *structure* diagram — components, packages,
@@ -59,7 +59,8 @@ export const umlStructureTool = defineTool({
59
59
  return reportCard(value, STRUCTURE_TOOL_NAME);
60
60
  },
61
61
  },
62
- presentResult: reportResultView(STRUCTURE_TOOL_NAME),
62
+ presentCall: reportCallView(STRUCTURE_TOOL_NAME),
63
+ presentResult: structureResultView,
63
64
  timeoutMs: 10_000,
64
65
  isConcurrencySafe: () => true,
65
66
  async execute(args) {
package/lib/tool.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { runVerification } from './engine.js';
3
3
  import { renderReportText } from './report-view.js';
4
- import { reportCard, reportResultView } from './presentation.js';
4
+ import { reportCallView, reportCard, reportResultView } from './presentation.js';
5
5
  import { OptionShapeError, optionRefusal, requireObjectOption } from './option-shape.js';
6
6
  import { oneOf } from './model-file.js';
7
7
  export const MODEL_VERIFY_TOOL_NAME = 'model_verify';
@@ -61,10 +61,11 @@ export const modelVerifyTool = defineTool({
61
61
  return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }];
62
62
  },
63
63
  // The card a UI shows, separated from the text the model reads.
64
- presentationMeta(_args, value) {
64
+ presentationMeta(args, value) {
65
65
  return reportCard(value, MODEL_VERIFY_TOOL_NAME);
66
66
  },
67
67
  },
68
+ presentCall: reportCallView(MODEL_VERIFY_TOOL_NAME),
68
69
  presentResult: reportResultView(MODEL_VERIFY_TOOL_NAME),
69
70
  timeoutMs: 10_000,
70
71
  isConcurrencySafe: () => true,