dsh-model-everything 0.1.0 → 0.2.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
@@ -2,6 +2,13 @@
2
2
 
3
3
  <p align="center"><strong>English</strong> · <a href="README.md">中文</a></p>
4
4
 
5
+ <p align="center">
6
+ <a href="https://github.com/AmethystLuna/dsh-model-everything/actions/workflows/dsh-bundle.yml"><img src="https://github.com/AmethystLuna/dsh-model-everything/actions/workflows/dsh-bundle.yml/badge.svg" alt="CI"></a>
7
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
8
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/node-%3E%3D20-blue.svg" alt="Node 20+"></a>
9
+ <a href="https://afdian.com/a/amethystluna"><img src="https://img.shields.io/badge/%E7%88%B1%E5%8F%91%E7%94%B5-Afdian-946CE6" alt="Sponsor on Afdian"></a>
10
+ </p>
11
+
5
12
  Abstract what a design or a piece of code claims into a model and let an engine run it: findings carry citations and hashes, so they can be recomputed and compared. The conclusion comes from a run rather than from reasoning it out: thinking it through becomes running it.
6
13
 
7
14
  A native DeepSeek Harness plugin. It turns the assertions a design makes **about behaviour** into models that can be run, reviewed and exported:
@@ -141,6 +148,8 @@ The limit: it stores and compares **model and diagram source**; a rendered pictu
141
148
  | `model_design_review` | Design review: the dependency graph's shape, plus the contracts the design declares about itself - layering, responsibility boundaries, substitutability, interface width |
142
149
  | `report_diff` | Compares two reports: what a change added, removed or changed |
143
150
 
151
+ `model_verify`, `model_datamodel_verify` and `model_design_review` also take `modelPath`: a JSON file in the workspace, in any formatting (indentation, a BOM and a trailing newline all parse). It is the alternative to an inline `model`; passing both is refused.
152
+
144
153
  ## Reading a report
145
154
 
146
155
  What a tool returns is a report, and the outcome is in `verdict`: read `verdict`, never `ok`. `ok` carries a different historical meaning in each family — most set it to `true` for a failing review, and a composition sets it to `false` when any error-severity finding exists — so it cannot be read as a verdict, and reading it as one is how a reader concludes the opposite of what the report says.
@@ -154,7 +163,7 @@ What a tool returns is a report, and the outcome is in `verdict`: read `verdict`
154
163
  | `schema` | The versioned report contract, for example `model-everything/verify/v1`. Branch on this field rather than on the shape of the payload |
155
164
  | `truncated` | The search hit its ceiling and stopped. **A truncated search is not a pass**, it is no conclusion; a property that could not be decided appears as `_UNDECIDED` |
156
165
 
157
- The body is what comes back by default. For the whole structured result, pass `reportView: "full"`.
166
+ The body is what comes back by default. A truncated check can be expanded on its own: pass `check: "<id>"` and every finding of that check prints. For the whole structured result, pass `reportView: "full"` — compact JSON, no field lost.
158
167
 
159
168
  When `ok` is `false` the verdict is `fail` as well, but it means something else: the tool did not run. The reason is in `errorCode` - invalid input, an unsupported diagram family, or a declared `intent` this view cannot serve. Such a refusal starts its `verdictReason` with `the tool did not run:`.
160
169
 
@@ -234,8 +243,6 @@ How it works, in four sentences:
234
243
 
235
244
  **Default and switch**: on by default. That default was measured: it reaches the one request class the plugin otherwise never sees, and a control task was not over-triggered. The price is about 3.2k characters per session; turn it off in `cordis.patch.yml` (below) or with the Plugins-page switch, which is also how to run a comparison - the same request with the gate off and on.
236
245
 
237
- `gateContent` can be replaced wholesale to try different wording; `tests/gate` still requires it to name every shipped skill.
238
-
239
246
  ## Configuration
240
247
 
241
248
  The routing component (on by default) has three keys:
@@ -251,11 +258,11 @@ The main row has one key, `interaction` (what happens when a tool call is offere
251
258
  Write it into the profile's `cordis.patch.yml` by row id:
252
259
 
253
260
  ```yaml
254
- - insert:
255
- - id: dsh-model-everything-gate
256
- name: 'dsh-model-everything/gate'
257
- config:
258
- enabled: false # turn the injection off
261
+ # Override that row's config by id: the whole config is replaced, not merged.
262
+ # Do not add `name` here - a row with a name inserts a new row instead of changing this one.
263
+ - id: dsh-model-everything-gate
264
+ config:
265
+ enabled: false # turn the injection off
259
266
  ```
260
267
 
261
268
  ## Boundaries
@@ -317,3 +324,9 @@ The published work behind the checks — and which checks have none — is in [d
317
324
  ## License and security
318
325
 
319
326
  MIT, see [LICENSE](LICENSE). Security policy in [SECURITY.md](SECURITY.md).
327
+
328
+ ## Sponsor
329
+
330
+ This plugin is open source under the MIT license. If it saves you time, you can support development on [Afdian](https://afdian.com/a/amethystluna).
331
+
332
+ Sponsorship is voluntary and changes neither the license nor the features.
package/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  <p align="center"><a href="README.en-US.md">English</a> · <strong>中文</strong></p>
4
4
 
5
+ <p align="center">
6
+ <a href="https://github.com/AmethystLuna/dsh-model-everything/actions/workflows/dsh-bundle.yml"><img src="https://github.com/AmethystLuna/dsh-model-everything/actions/workflows/dsh-bundle.yml/badge.svg" alt="CI"></a>
7
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
8
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/node-%3E%3D20-blue.svg" alt="Node 20+"></a>
9
+ <a href="https://afdian.com/a/amethystluna"><img src="https://img.shields.io/badge/%E7%88%B1%E5%8F%91%E7%94%B5-Afdian-946CE6" alt="在爱发电上赞助"></a>
10
+ </p>
11
+
5
12
  把设计与代码里的断言抽象成模型,交给引擎跑一遍:结论带出处与哈希,能复算、能比对。让结论来自一次运行,而不是一次通盘推理:把「想清楚」变成「跑一遍」。
6
13
 
7
14
  DeepSeek Harness 原生插件。把设计与代码里**关于行为的断言**变成可运行、可审查、可导出的模型:
@@ -141,6 +148,8 @@ DeepSeek Harness 原生插件。把设计与代码里**关于行为的断言**
141
148
  | `model_design_review` | 设计审阅:依赖图形状 + 设计自身声明的契约(分层、职责边界、可替换性、接口宽度) |
142
149
  | `report_diff` | 两份报告比对:新增/消失/变化的发现 |
143
150
 
151
+ `model_verify`、`model_datamodel_verify`、`model_design_review` 另收 `modelPath`:从工作区里的 JSON 文件读模型,缩进、BOM、尾随换行都不影响。它与内联的 `model` 二选一,两个都给会被拒绝。
152
+
144
153
  ## 怎么读报告
145
154
 
146
155
  工具的产出是一份报告,结论只在 `verdict` 里:**看 `verdict`,不看 `ok`**。`ok` 在各族的含义不同——多数族在评审失败时仍是 `true`,组合族在出现 error 级发现时是 `false`——所以它不能当判定读;把它当判定读,会得出与报告相反的结论。
@@ -154,7 +163,7 @@ DeepSeek Harness 原生插件。把设计与代码里**关于行为的断言**
154
163
  | `schema` | 报告的版本化契约,例如 `model-everything/verify/v1`。要按字段分支,就分支在它上面 |
155
164
  | `truncated` | 搜索撞到上限就停了。**截断不等于通过**,是没结论;某个性质没判定出来时,报告里会出现 `_UNDECIDED` |
156
165
 
157
- 默认返回正文。要完整 JSON,传 `reportView: "full"`。
166
+ 默认返回正文。被截断的检查可以单独展开:传 `check: "<id>"`,该检查的全部条目都会打印。要完整结果,传 `reportView: "full"`——紧凑 JSON,对象不丢字段。
158
167
 
159
168
  `ok` 为 `false` 时判定也是 `fail`,但含义不同:工具根本没跑。原因写在 `errorCode` 里——输入不合法、图族不支持,或声明的 `intent` 服务不了。这类拒绝的 `verdictReason` 以 `the tool did not run:` 开头。
160
169
 
@@ -234,8 +243,6 @@ npx -p @deepseek-ai/dsh dsh plugin --profile <profile> add dsh-model-everything
234
243
 
235
244
  **开关与默认**:默认开。这个默认值是量出来的——它补上的是插件唯一接不住的那类请求,而对照任务没有被过度触发。代价是每个会话约 3.2k 字符;要关掉就改 `cordis.patch.yml`(见下)或翻插件页的开关。想做对照(同一个请求,关一次开一次)也用它。
236
245
 
237
- `gateContent` 可以整段替换,用来试别的措辞;`tests/gate` 仍要求它点名每个已发布的技能。
238
-
239
246
  ## 配置
240
247
 
241
248
  注入组件(默认开)有三个键:
@@ -251,11 +258,11 @@ npx -p @deepseek-ai/dsh dsh plugin --profile <profile> add dsh-model-everything
251
258
  按 row id 写进 profile 的 `cordis.patch.yml`:
252
259
 
253
260
  ```yaml
254
- - insert:
255
- - id: dsh-model-everything-gate
256
- name: 'dsh-model-everything/gate'
257
- config:
258
- enabled: false # 关掉注入
261
+ # 按 id 覆盖那一行的 config:整段替换,不做深合并。
262
+ # 这里不要写 name——写了 name 是「插入一行」,不是「改这一行」。
263
+ - id: dsh-model-everything-gate
264
+ config:
265
+ enabled: false # 关掉注入
259
266
  ```
260
267
 
261
268
  ## 边界
@@ -317,3 +324,9 @@ MODEL_EVERYTHING_SPIN=/path/to/spin MODEL_EVERYTHING_GCC=/path/to/gcc npm run te
317
324
  ## 许可证与安全
318
325
 
319
326
  MIT,见 [LICENSE](LICENSE)。安全策略见 [SECURITY.md](SECURITY.md)。
327
+
328
+ ## 赞助
329
+
330
+ 本插件以 MIT 许可证开源。如果它节省了你的时间,可以在[爱发电](https://afdian.com/a/amethystluna)赞助开发。
331
+
332
+ 赞助完全自愿,不影响许可证,也不改变任何功能。
@@ -33,6 +33,10 @@ export const modelComposeTool = defineTool({
33
33
  type: 'integer',
34
34
  description: 'Maximum composite states to explore. Default 10000.',
35
35
  },
36
+ check: {
37
+ type: 'string',
38
+ description: 'Print one check in full when the digest truncated it. Pass the check id the CHECKS line lists (for example A9); the per-check budget no longer applies, and an id this report does not carry is answered with the ids that it does.',
39
+ },
36
40
  reportView: {
37
41
  type: 'string',
38
42
  enum: ['digest', 'full'],
@@ -45,7 +49,7 @@ export const modelComposeTool = defineTool({
45
49
  description: 'Composition verification report with C1/C2 findings and, when shared locations are declared, C3/C4/C5.',
46
50
  },
47
51
  render(args, value) {
48
- return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
52
+ return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }];
49
53
  },
50
54
  },
51
55
  timeoutMs: 10_000,
@@ -1132,7 +1132,7 @@ function dataNextSteps(checks) {
1132
1132
  // check looked at the model. `pass` does not say that, and neither does a list of the failures.
1133
1133
  const skipped = checks.filter((check) => check.status === 'skip').map((check) => check.name + ' (' + check.id + ')');
1134
1134
  if (skipped.length > 0) {
1135
- steps.push('These examined nothing, so their silence is not a result: ' + skipped.join(', ') + '. Declare what each one would need, or accept that the model is unverified on those points.');
1135
+ steps.push('Not checked: ' + skipped.join(', ') + ' examined nothing — silence there is not a pass. Declare what each needs, or treat the model as unverified on those points.');
1136
1136
  }
1137
1137
  if (warningCodes.length > 0)
1138
1138
  steps.push('Review the warning findings (' + warningCodes.join(', ') + '): a nullable field or a missing index is a decision, not silence.');
package/lib/data-tool.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { runDataVerification, DATA_ENGINE_SCHEMA_VERSION } from './data-engine.js';
3
+ import { oneOf } from './model-file.js';
3
4
  import { optionRefusal, requireArrayOption, requireObjectOption } from './option-shape.js';
4
5
  import { renderReportText } from './report-view.js';
5
6
  export const DATAMODEL_VERIFY_TOOL_NAME = 'model_datamodel_verify';
@@ -15,8 +16,11 @@ export const modelDataModelVerifyTool = defineTool({
15
16
  parameters: {
16
17
  model: {
17
18
  type: 'json',
18
- required: true,
19
- description: 'DataModelV1 data model to verify.',
19
+ description: 'DataModelV1 data model to verify. Pass this, or modelPath, not both.',
20
+ },
21
+ modelPath: {
22
+ type: 'string',
23
+ description: 'Path to a JSON file holding the model, instead of inlining it. Formatting does not matter: whitespace, a BOM and pretty printing all parse. Relative paths resolve against the process working directory, which is the session workspace.',
20
24
  },
21
25
  beforeModel: {
22
26
  type: 'json',
@@ -38,6 +42,10 @@ export const modelDataModelVerifyTool = defineTool({
38
42
  type: 'json',
39
43
  description: 'Optional backup/restore pairs for DA7 rollback symmetry.',
40
44
  },
45
+ check: {
46
+ type: 'string',
47
+ description: 'Print one check in full when the digest truncated it. Pass the check id the CHECKS line lists (for example A9); the per-check budget no longer applies, and an id this report does not carry is answered with the ids that it does.',
48
+ },
41
49
  reportView: {
42
50
  type: 'string',
43
51
  enum: ['digest', 'full'],
@@ -50,7 +58,7 @@ export const modelDataModelVerifyTool = defineTool({
50
58
  description: 'model-everything-datamodel verification report with summary and per-check findings.',
51
59
  },
52
60
  render(args, value) {
53
- return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
61
+ return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }];
54
62
  },
55
63
  },
56
64
  timeoutMs: 10_000,
@@ -63,7 +71,7 @@ export const modelDataModelVerifyTool = defineTool({
63
71
  requireArrayOption(args.copyPairs, 'copyPairs', 'DATA_OPTION_INVALID');
64
72
  requireArrayOption(args.migrationMappings, 'migrationMappings', 'DATA_OPTION_INVALID');
65
73
  requireArrayOption(args.backupPairs, 'backupPairs', 'DATA_OPTION_INVALID');
66
- return runDataVerification(args.model, {
74
+ return runDataVerification(oneOf(args.model, args.modelPath, 'model', 'modelPath', 'DATA_OPTION_INVALID'), {
67
75
  beforeModel: args.beforeModel,
68
76
  fieldMapping: args.fieldMapping,
69
77
  copyPairs: args.copyPairs,
@@ -1,5 +1,6 @@
1
1
  import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { REPORT_SCHEMAS } from './engine.js';
3
+ import { oneOf } from './model-file.js';
3
4
  import { runDesignReview } from './design-engine.js';
4
5
  import { renderReportText } from './report-view.js';
5
6
  export const DESIGN_TOOL_NAME = 'model_design_review';
@@ -18,9 +19,16 @@ export const designTool = defineTool({
18
19
  parameters: {
19
20
  model: {
20
21
  type: 'json',
21
- required: true,
22
22
  description: 'DesignModelV1: {schemaVersion: 1, architecture: {nodes: [ids], edges: [[from, to]]}, types?: [{name, stereotype, extends?, implements?, operations?, transitions?, forbids?, responsibilities?, dependsOn?, uses?, layer?}], declare?: {layers?, boundaries?, allowedEdges?, requiredEdges?}}. The schema is closed: an unknown key is refused rather than ignored. `types` is what makes the principle checks possible - with no `extends` there is no substitutability to decide, with no `uses` no interface can be called fat, and no `responsibilities` means nothing to compare the dependencies against.',
23
23
  },
24
+ modelPath: {
25
+ type: 'string',
26
+ description: 'Path to a JSON file holding the model, instead of inlining it. Formatting does not matter: whitespace, a BOM and pretty printing all parse. Relative paths resolve against the process working directory, which is the session workspace.',
27
+ },
28
+ check: {
29
+ type: 'string',
30
+ description: 'Print one check in full when the digest truncated it. Pass the check id the CHECKS line lists (for example A9); the per-check budget no longer applies, and an id this report does not carry is answered with the ids that it does.',
31
+ },
24
32
  reportView: {
25
33
  type: 'string',
26
34
  enum: ['digest', 'full'],
@@ -33,23 +41,23 @@ export const designTool = defineTool({
33
41
  description: 'Design review report: every check with its status, `violations` (they decide the verdict), `signals`, `facts` (graph features), summary and next steps.',
34
42
  },
35
43
  render(args, value) {
36
- return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
44
+ return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }];
37
45
  },
38
46
  },
39
47
  timeoutMs: 10_000,
40
48
  isConcurrencySafe: () => true,
41
49
  async execute(args) {
42
- if (args.model === undefined) {
50
+ if (args.model === undefined && args.modelPath === undefined) {
43
51
  return {
44
52
  ok: false,
45
53
  ran: false,
46
54
  verdict: 'fail',
47
- verdictReason: 'the tool did not run: pass `model`, a DesignModelV1 with schemaVersion, architecture and the declarations to review it against',
55
+ verdictReason: 'the tool did not run: pass `model` (a DesignModelV1 with schemaVersion, architecture and the declarations) or `modelPath` (a JSON file holding one)',
48
56
  schema: REPORT_SCHEMAS.design,
49
57
  errorCode: 'DESIGN_INPUT',
50
- error: 'design review needs `model`',
58
+ error: 'design review needs `model` or `modelPath`',
51
59
  };
52
60
  }
53
- return runDesignReview(args.model);
61
+ return runDesignReview(oneOf(args.model, args.modelPath, 'model', 'modelPath', 'DESIGN_OPTION_INVALID'));
54
62
  },
55
63
  });
package/lib/engine.js CHANGED
@@ -52,7 +52,7 @@ function verificationNextSteps(checks, coverage) {
52
52
  // named check looked at the model. A list of failures does not say that, and neither does `pass`.
53
53
  const skipped = checks.filter((check) => check.status === 'skip').map((check) => check.name + ' (' + check.id + ')');
54
54
  if (skipped.length > 0) {
55
- steps.push('These examined nothing, so their silence is not a result: ' + skipped.join(', ') + '. Declare what each one would need, or accept that the model is unverified on those points.');
55
+ steps.push('Not checked: ' + skipped.join(', ') + ' examined nothing — silence there is not a pass. Declare what each needs, or treat the model as unverified on those points.');
56
56
  }
57
57
  // Named checks above, bare codes here. That is deliberate: each warning is a different kind of thing,
58
58
  // so there is no single name to give the set, and the sentence points at where each one IS explained
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Read a JSON model from a file instead of requiring it inline.
3
+ *
4
+ * A model is the largest argument any of these tools takes, and it is the one that arrives as
5
+ * streamed tool-call JSON: the provider rejected one session with `tool input is invalid JSON`
6
+ * after the model spent sixteen steps hand-editing a nested model argument. Reading it from the
7
+ * workspace removes that failure mode and the tokens.
8
+ */
9
+ import { readFileSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ import { OptionShapeError } from './option-shape.js';
12
+ /** Read and parse one JSON file. Formatting is irrelevant: whitespace, BOM and pretty printing all parse. */
13
+ export function readJsonFile(path, option, code) {
14
+ if (typeof path !== 'string' || path.trim() === '') {
15
+ throw new OptionShapeError(code, `${option} must be a non-empty path to a JSON file.`);
16
+ }
17
+ const full = resolve(path);
18
+ let text;
19
+ try {
20
+ text = readFileSync(full, 'utf8');
21
+ }
22
+ catch (error) {
23
+ const reason = error instanceof Error ? error.message : String(error);
24
+ throw new OptionShapeError(code, `${option} could not be read: ${reason}. The path resolves against the process working directory; relative paths in a session resolve against the session's workspace.`);
25
+ }
26
+ try {
27
+ return JSON.parse(text.replace(/^\uFEFF/, ''));
28
+ }
29
+ catch (error) {
30
+ const reason = error instanceof Error ? error.message : String(error);
31
+ throw new OptionShapeError(code, `${option} is not valid JSON: ${reason}.`);
32
+ }
33
+ }
34
+ /**
35
+ * Exactly one of the inline value and the file path. Both is ambiguous, neither is a missing
36
+ * argument, and each gets its own sentence so the caller knows which one to drop.
37
+ */
38
+ export function oneOf(value, path, inlineName, pathName, code) {
39
+ const hasValue = value !== undefined;
40
+ const hasPath = path !== undefined;
41
+ if (hasValue && hasPath) {
42
+ throw new OptionShapeError(code, `Pass either ${inlineName} or ${pathName}, not both: the file would be read and then ignored, or the other way round.`);
43
+ }
44
+ if (!hasValue && !hasPath) {
45
+ throw new OptionShapeError(code, `Pass ${inlineName} or ${pathName}: the model has to come from somewhere.`);
46
+ }
47
+ return hasPath ? readJsonFile(path, pathName, code) : value;
48
+ }
@@ -1,4 +1,4 @@
1
- const DEFAULTS = { maxFindingsPerCheck: 24, maxEvidenceChars: 160, includePassing: false, full: false };
1
+ const DEFAULTS = { maxFindingsPerCheck: 24, maxEvidenceChars: 160, includePassing: false, full: false, check: undefined };
2
2
  const record = (value) => typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
3
3
  const array = (value) => (Array.isArray(value) ? value : []);
4
4
  const text = (value) => (typeof value === 'string' ? value : undefined);
@@ -93,8 +93,20 @@ function findingLines(findings, options, seenFacts) {
93
93
  group.events.push(event);
94
94
  stateGroups.set(state, group);
95
95
  }
96
+ // The same event set on every state is one fact, not one per state: a 26-state machine whose
97
+ // states all ignore the same four events printed 24 near-identical lines, a quarter of the body.
98
+ // States that share a set are counted; a handful still name themselves, because there the names
99
+ // are what the reader acts on.
100
+ const byEventSet = new Map();
96
101
  for (const [state, group] of stateGroups) {
97
- lines.push(` ${state} (${String(group.events.length)}) [${group.severity}/${group.code}]: ${group.events.join(', ')}`);
102
+ const key = `${group.severity}|${group.code}|${group.events.join(',')}`;
103
+ const bucket = byEventSet.get(key) ?? { severity: group.severity, code: group.code, events: group.events, states: [] };
104
+ bucket.states.push(state);
105
+ byEventSet.set(key, bucket);
106
+ }
107
+ for (const group of byEventSet.values()) {
108
+ const who = group.states.length > 3 ? `${String(group.states.length)} states` : group.states.join(', ');
109
+ lines.push(` ${who} (${String(group.events.length)}) [${group.severity}/${group.code}]: ${group.events.join(', ')}`);
98
110
  }
99
111
  const rendered = collapse(singles.map((finding) => findingLine(finding, options)));
100
112
  const budget = options.maxFindingsPerCheck;
@@ -219,13 +231,24 @@ function renderChecksReport(report, options) {
219
231
  .map(([status, ids]) => `${String(status)} ${ids.join(' ')}`)
220
232
  .join(' | ');
221
233
  const lines = [...headerLines(report, options), `CHECKS ${inventory}`];
234
+ const requested = options.check;
235
+ const ids = checks.map((check) => text(check.id)).filter((id) => id !== undefined);
236
+ const selected = requested === undefined ? checks : checks.filter((check) => text(check.id) === requested);
237
+ // A name that matches nothing is answered with the names that exist: silently printing the whole
238
+ // report, or an empty body, both read as "that check was clean".
239
+ if (requested !== undefined && selected.length === 0) {
240
+ lines.push(`NOTE no check \`${requested}\` in this report; it carries: ${ids.join(' ')}`);
241
+ }
242
+ // Asking for one check is a request for its lines: the per-check budget that keeps the digest
243
+ // small does not apply, since the caller has already paid for the narrowing.
244
+ const view = requested === undefined ? options : { ...options, includePassing: true, maxFindingsPerCheck: Number.MAX_SAFE_INTEGER };
222
245
  const seenFacts = new Set();
223
- for (const check of checks) {
246
+ for (const check of selected) {
224
247
  const status = text(check.status) ?? 'skip';
225
248
  const findings = array(check.findings).map((finding) => toViewFinding(finding, text(check.id)));
226
249
  if (status === 'pass' && !options.includePassing)
227
250
  continue;
228
- const body = findingLines(findings, options, seenFacts);
251
+ const body = findingLines(findings, view, seenFacts);
229
252
  if (status !== 'fail' && body.length === 0)
230
253
  continue;
231
254
  lines.push('');
@@ -380,8 +403,10 @@ function renderFallbackReport(report, options) {
380
403
  */
381
404
  export function renderReportText(value, options = {}) {
382
405
  const resolved = { ...DEFAULTS, ...options };
406
+ // Compact rather than pretty: `full` exists so a caller can hand the object on (report_diff matches
407
+ // on check + code + evidence), and indentation is a third of a large report's bytes for no reader.
383
408
  if (resolved.full)
384
- return JSON.stringify(value, null, 2);
409
+ return JSON.stringify(value);
385
410
  const report = record(value);
386
411
  if (report === undefined)
387
412
  return JSON.stringify(value) ?? String(value);
package/lib/tool.js CHANGED
@@ -2,6 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
2
2
  import { runVerification } from './engine.js';
3
3
  import { renderReportText } from './report-view.js';
4
4
  import { OptionShapeError, optionRefusal, requireObjectOption } from './option-shape.js';
5
+ import { oneOf } from './model-file.js';
5
6
  export const MODEL_VERIFY_TOOL_NAME = 'model_verify';
6
7
  /**
7
8
  * Model-visible DSH tool wrapping the bundled TypeScript verification engine.
@@ -18,8 +19,11 @@ export const modelVerifyTool = defineTool({
18
19
  parameters: {
19
20
  model: {
20
21
  type: 'json',
21
- required: true,
22
- description: 'LogicModelV1 state machine model to verify.',
22
+ description: 'LogicModelV1 state machine model to verify. Pass this, or modelPath, not both.',
23
+ },
24
+ modelPath: {
25
+ type: 'string',
26
+ description: 'Path to a JSON file holding the model, instead of inlining it. Formatting does not matter: whitespace, a BOM and pretty printing all parse. Relative paths resolve against the process working directory, which is the session workspace.',
23
27
  },
24
28
  maxStates: {
25
29
  type: 'integer',
@@ -37,6 +41,10 @@ export const modelVerifyTool = defineTool({
37
41
  type: 'json',
38
42
  description: 'Optional object mapping BEFORE state ids to AFTER state ids. Omit for identity mapping (same state names).',
39
43
  },
44
+ check: {
45
+ type: 'string',
46
+ description: 'Print one check in full when the digest truncated it. Pass the check id the CHECKS line lists (for example A9); the per-check budget no longer applies, and an id this report does not carry is answered with the ids that it does.',
47
+ },
40
48
  reportView: {
41
49
  type: 'string',
42
50
  enum: ['digest', 'full'],
@@ -49,7 +57,7 @@ export const modelVerifyTool = defineTool({
49
57
  description: 'this plugin verification report with summary and per-check findings.',
50
58
  },
51
59
  render(args, value) {
52
- return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
60
+ return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }];
53
61
  },
54
62
  },
55
63
  timeoutMs: 10_000,
@@ -71,7 +79,8 @@ export const modelVerifyTool = defineTool({
71
79
  throw new OptionShapeError('VERIFY_OPTION_INVALID', 'stateMapping.' + from + ' is not a state id (it is ' + (Array.isArray(to) ? 'an array' : to === null ? 'null' : 'a ' + typeof to) + '). Every value is the AFTER state id the BEFORE state maps to, so it has to be a non-empty string.');
72
80
  }
73
81
  }
74
- return runVerification(args.model, {
82
+ const model = oneOf(args.model, args.modelPath, 'model', 'modelPath', 'VERIFY_OPTION_INVALID');
83
+ return runVerification(model, {
75
84
  maxStates: args.maxStates,
76
85
  maxPermutationEvents: args.maxPermutationEvents,
77
86
  beforeModel: args.beforeModel,
@@ -0,0 +1,7 @@
1
+ /** Read and parse one JSON file. Formatting is irrelevant: whitespace, BOM and pretty printing all parse. */
2
+ export declare function readJsonFile(path: unknown, option: string, code: string): unknown;
3
+ /**
4
+ * Exactly one of the inline value and the file path. Both is ambiguous, neither is a missing
5
+ * argument, and each gets its own sentence so the caller knows which one to drop.
6
+ */
7
+ export declare function oneOf(value: unknown, path: unknown, inlineName: string, pathName: string, code: string): unknown;
@@ -12,6 +12,13 @@ export interface ReportViewOptions {
12
12
  * matches on check + code + evidence, so a body cannot stand in for the object.
13
13
  */
14
14
  full?: boolean;
15
+ /**
16
+ * Print one check in full: every finding, evidence included, without the per-check budget the
17
+ * digest applies. The digest truncates a check's warnings and tells the reader to ask for it by
18
+ * id; before this option existed that sentence named something the tools could not do, and the
19
+ * only way to see the lines was `full`, which returns the whole structured report.
20
+ */
21
+ check?: string | undefined;
15
22
  }
16
23
  type Record_ = Record<string, unknown>;
17
24
  /** One normalized finding. `evidence` is kept because a finding's identity depends on it. */
@@ -22,7 +29,7 @@ export interface ViewFinding {
22
29
  evidence?: Record_;
23
30
  file?: string;
24
31
  line?: number;
25
- check?: string;
32
+ check?: string | undefined;
26
33
  }
27
34
  /** Normalize one raw finding. Unknown fields are ignored; a missing message renders as the code. */
28
35
  export declare function toViewFinding(value: unknown, check?: string): ViewFinding;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-everything",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Modeling abstraction and automated review for designs and code, including a refactoring harness: UML diagrams, executable verification of machines and protocols with before/after regression, data-model and migration checks, concurrency claim routing, export to external checkers, dependency-graph review, and report diffing. Native plugin for DeepSeek Harness (dsh).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -104,6 +104,10 @@
104
104
  },
105
105
  "license": "MIT",
106
106
  "repository": "https://github.com/AmethystLuna/dsh-model-everything",
107
+ "funding": {
108
+ "type": "custom",
109
+ "url": "https://afdian.com/a/amethystluna"
110
+ },
107
111
  "keywords": [
108
112
  "verification",
109
113
  "model-checking",