dsh-model-everything 0.0.0-stage → 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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.en-US.md +332 -0
  3. package/README.md +332 -3
  4. package/cordis.patch.yml +20 -0
  5. package/examples/README.md +48 -0
  6. package/examples/control-dispatcher.json +113 -0
  7. package/examples/control-dispatcher.overbudget.json +113 -0
  8. package/examples/ecommerce-data-model.json +185 -0
  9. package/examples/order-state-machine.after.json +116 -0
  10. package/examples/order-state-machine.before.json +101 -0
  11. package/examples/user-migration.after.json +29 -0
  12. package/examples/user-migration.before.json +29 -0
  13. package/gate/locale/en.json +6 -0
  14. package/gate/locale/zh.json +6 -0
  15. package/gate/package.json +5 -0
  16. package/lib/baseline.js +249 -0
  17. package/lib/compose-tool.js +80 -0
  18. package/lib/concurrency-tool.js +40 -0
  19. package/lib/concurrency.js +206 -0
  20. package/lib/data-engine.js +1141 -0
  21. package/lib/data-tool.js +87 -0
  22. package/lib/design-engine.js +1149 -0
  23. package/lib/design-tool.js +63 -0
  24. package/lib/engine.js +4343 -0
  25. package/lib/export-tool.js +78 -0
  26. package/lib/exporters.js +434 -0
  27. package/lib/gate.js +274 -0
  28. package/lib/granularity.js +237 -0
  29. package/lib/index.js +237 -0
  30. package/lib/json-value.js +18 -0
  31. package/lib/model-file.js +48 -0
  32. package/lib/option-shape.js +62 -0
  33. package/lib/report-diff-tool.js +63 -0
  34. package/lib/report-view.js +433 -0
  35. package/lib/structure-tool.js +91 -0
  36. package/lib/structure.js +711 -0
  37. package/lib/tool.js +94 -0
  38. package/lib/types/baseline.d.ts +65 -0
  39. package/lib/types/compose-tool.d.ts +7 -0
  40. package/lib/types/concurrency-tool.d.ts +8 -0
  41. package/lib/types/concurrency.d.ts +37 -0
  42. package/lib/types/data-engine.d.ts +211 -0
  43. package/lib/types/data-tool.d.ts +10 -0
  44. package/lib/types/design-engine.d.ts +215 -0
  45. package/lib/types/design-tool.d.ts +11 -0
  46. package/lib/types/engine.d.ts +494 -0
  47. package/lib/types/export-tool.d.ts +7 -0
  48. package/lib/types/exporters.d.ts +37 -0
  49. package/lib/types/gate.d.ts +91 -0
  50. package/lib/types/granularity.d.ts +106 -0
  51. package/lib/types/index.d.ts +45 -0
  52. package/lib/types/json-value.d.ts +21 -0
  53. package/lib/types/model-file.d.ts +7 -0
  54. package/lib/types/option-shape.d.ts +22 -0
  55. package/lib/types/report-diff-tool.d.ts +8 -0
  56. package/lib/types/report-view.d.ts +50 -0
  57. package/lib/types/structure-tool.d.ts +10 -0
  58. package/lib/types/structure.d.ts +152 -0
  59. package/lib/types/tool.d.ts +11 -0
  60. package/lib/types/uml-communication.d.ts +109 -0
  61. package/lib/types/uml-composite.d.ts +107 -0
  62. package/lib/types/uml-interaction-overview.d.ts +91 -0
  63. package/lib/types/uml-profile.d.ts +112 -0
  64. package/lib/types/uml-structure.d.ts +53 -0
  65. package/lib/types/uml-syntax.d.ts +296 -0
  66. package/lib/types/uml-timing.d.ts +152 -0
  67. package/lib/types/uml-tool.d.ts +12 -0
  68. package/lib/types/uml-usecase.d.ts +58 -0
  69. package/lib/types/uml.d.ts +303 -0
  70. package/lib/uml-communication.js +353 -0
  71. package/lib/uml-composite.js +442 -0
  72. package/lib/uml-interaction-overview.js +277 -0
  73. package/lib/uml-profile.js +373 -0
  74. package/lib/uml-structure.js +306 -0
  75. package/lib/uml-syntax.js +545 -0
  76. package/lib/uml-timing.js +974 -0
  77. package/lib/uml-tool.js +213 -0
  78. package/lib/uml-usecase.js +532 -0
  79. package/lib/uml.js +2110 -0
  80. package/locale/en.json +6 -0
  81. package/locale/zh.json +6 -0
  82. package/package.json +128 -6
  83. package/skills/model-everything/SKILL.md +450 -0
  84. package/skills/model-everything/references/dsh-model-schema.md +437 -0
  85. package/skills/model-everything/references/gap-routing-guide.md +42 -0
  86. package/skills/model-everything/references/hash-spec.md +152 -0
  87. package/skills/model-everything/references/logic-verification-guide.md +422 -0
  88. package/skills/model-everything-datamodel/SKILL.md +130 -0
  89. package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
  90. package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
  91. package/skills/model-everything-design/SKILL.md +115 -0
  92. package/skills/model-everything-interleaving/SKILL.md +63 -0
  93. package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
  94. package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
  95. package/skills/model-everything-report-reading/SKILL.md +137 -0
  96. package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
  97. package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
  98. package/skills/model-everything-structure/SKILL.md +96 -0
  99. package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
  100. package/skills/model-everything-uml/SKILL.md +60 -0
  101. package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
  102. package/src/baseline.ts +288 -0
  103. package/src/compose-tool.ts +82 -0
  104. package/src/concurrency-tool.ts +44 -0
  105. package/src/concurrency.ts +254 -0
  106. package/src/data-engine.ts +1208 -0
  107. package/src/data-tool.ts +91 -0
  108. package/src/design-engine.ts +1263 -0
  109. package/src/design-tool.ts +68 -0
  110. package/src/engine.ts +4564 -0
  111. package/src/export-tool.ts +81 -0
  112. package/src/exporters.ts +399 -0
  113. package/src/gate.ts +330 -0
  114. package/src/granularity.ts +312 -0
  115. package/src/index.ts +289 -0
  116. package/src/json-value.ts +20 -0
  117. package/src/model-file.ts +49 -0
  118. package/src/option-shape.ts +64 -0
  119. package/src/report-diff-tool.ts +67 -0
  120. package/src/report-view.ts +483 -0
  121. package/src/structure-tool.ts +93 -0
  122. package/src/structure.ts +786 -0
  123. package/src/tool.ts +103 -0
  124. package/src/uml-communication.ts +428 -0
  125. package/src/uml-composite.ts +520 -0
  126. package/src/uml-interaction-overview.ts +321 -0
  127. package/src/uml-profile.ts +439 -0
  128. package/src/uml-structure.ts +317 -0
  129. package/src/uml-syntax.ts +764 -0
  130. package/src/uml-timing.ts +1106 -0
  131. package/src/uml-tool.ts +230 -0
  132. package/src/uml-usecase.ts +566 -0
  133. package/src/uml.ts +2139 -0
package/src/index.ts ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * dsh-model-everything — DeepSeek Harness native plugin for models and diagrams.
3
+ *
4
+ * It ships a skill set (one entry skill plus a skill per domain) and the tools that do
5
+ * the work: UML diagrams render, parse and review; state machines, protocols and composed
6
+ * machines are checked by running them; data models and migrations are checked by the same
7
+ * doctrine; concurrency claims are mined and routed; a verified model is exported to an
8
+ * external checker; a structure diagram is audited as a dependency graph; two reports are
9
+ * diffed against each other.
10
+ *
11
+ * Every tool carries its own description and every report says what it proves, so a model
12
+ * reaches for one the way it reaches for any other tool: by reading what it says it does.
13
+ * The plugin writes nothing into a session.
14
+ *
15
+ * The skills ship in this package's `skills/` directory and are registered at apply time
16
+ * into dsh's `ctx.skills` registry through the standard filesystem provider, so they appear
17
+ * in every session catalog without a manual copy step.
18
+ *
19
+ * Method, doctrine and per-domain detail live in the skills and their references, not here.
20
+ *
21
+ * @module dsh-model-everything
22
+ */
23
+
24
+ import { fileURLToPath } from 'node:url'
25
+ import type { Context } from '@deepseek-ai/cordis'
26
+ import z from '@deepseek-ai/schemastery'
27
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
28
+ import type { ContextFormed } from '@deepseek-ai/dsh-llm'
29
+ import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
30
+ import type { HostCordisInspectProviderRegistration } from '@deepseek-ai/dsh-cordis-host-runner'
31
+ import type { AssembleContext, PromptContext } from '@deepseek-ai/dsh-system-prompt'
32
+ import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem'
33
+ import { modelVerifyTool } from './tool.js'
34
+ import { modelDataModelVerifyTool, DATA_ENGINE_SCHEMA_VERSION } from './data-tool.js'
35
+ import { modelConcurrencyScanTool } from './concurrency-tool.js'
36
+ import { modelComposeTool } from './compose-tool.js'
37
+ import { modelExportTool } from './export-tool.js'
38
+ import { umlRenderTool, umlParseTool, umlReviewTool, umlExplainLabelsTool } from './uml-tool.js'
39
+ import { umlStructureTool } from './structure-tool.js'
40
+ import { designTool } from './design-tool.js'
41
+ import { reportDiffTool } from './report-diff-tool.js'
42
+ import { ENGINE_SCHEMA_VERSION } from './engine.js'
43
+
44
+ // DSH 0.1.7-alpha.1 (session format v4) retires the shared
45
+ // `{ kind: 'plugin', plugin }` wrapper: native admission rejects it in every
46
+ // declared durable message slot, and the official v3-to-v4 migration rewrites
47
+ // those historical rows to `plugin:<name>`. Declaring the producer-owned kind
48
+ // here keeps the write path and the history guard on one identity, and still
49
+ // compiles against the earlier releases that only declare `plugin`.
50
+
51
+ export const name = 'dsh-model-everything'
52
+
53
+ // Skills are contributed through the registry service, which dsh-base always
54
+ // mounts before bundle rows such as this one apply.
55
+ export const inject = ['skills']
56
+
57
+ // Absolute path of the package's shipped skills directory. `lib/index.js`
58
+ // lives one level below the package root, so `../skills` from the module URL
59
+ // lands on `<package>/skills` regardless of where the package was installed.
60
+ const SKILLS_DIR = fileURLToPath(new URL('../skills', import.meta.url))
61
+
62
+
63
+
64
+ export type InteractionMode = 'ask' | 'auto' | 'follow-approval'
65
+
66
+
67
+ export interface Config {
68
+ /**
69
+ * How an offer to run a tool resolves: `ask` and `auto` are fixed, while `follow-approval`
70
+ * reads the session's approval policy per call. `resolveInteraction` is its only reader.
71
+ *
72
+ * There is deliberately no on/off key here. A row is switched off by the loader row that
73
+ * mounts it (`disabled`, or the plugin manager), which removes the tools and the skills
74
+ * together; a key that turned the tools off while the skills stayed would leave every skill
75
+ * naming a tool that is not there, which is worse than having no skill at all.
76
+ */
77
+ interaction: InteractionMode
78
+ }
79
+
80
+ export const Config = z.object({
81
+ interaction: z.union(['ask', 'auto', 'follow-approval']).default('follow-approval'),
82
+ })
83
+
84
+
85
+ interface SessionEventLike {
86
+ type: string
87
+ data?: Record<string, unknown>
88
+ }
89
+
90
+ /**
91
+ * Minimum session-store surface this plugin reads. DSH 0.1.2-alpha.4 replaced
92
+ * the `Session.events` getter with on-demand reads (`seq`, `eventAt()`,
93
+ * `snapshotEvents()`); releases up to 0.1.2-alpha.3 expose `events` as the
94
+ * full log snapshot. Read through a structural union so one build serves every
95
+ * declared DSH release.
96
+ */
97
+ interface SessionEventSource {
98
+ events?: readonly SessionEventLike[]
99
+ snapshotEvents?: () => readonly SessionEventLike[]
100
+ }
101
+
102
+ function readSessionEvents(session: Session): readonly SessionEventLike[] {
103
+ const source = session as unknown as SessionEventSource
104
+ if (typeof source.snapshotEvents === 'function') {
105
+ const snapshot = source.snapshotEvents()
106
+ if (Array.isArray(snapshot)) return snapshot
107
+ }
108
+ if (Array.isArray(source.events)) return source.events
109
+ return []
110
+ }
111
+
112
+ function lastApprovalPolicy(session: Session): 'ask' | 'never' | undefined {
113
+ const events = readSessionEvents(session)
114
+ for (let index = events.length - 1; index >= 0; index -= 1) {
115
+ const event = events[index]
116
+ if (event.type === 'approval/policy') {
117
+ return event.data?.policy === 'never' ? 'never' : 'ask'
118
+ }
119
+ }
120
+ return undefined
121
+ }
122
+
123
+ function planModeActive(session: Session): boolean {
124
+ const events = readSessionEvents(session)
125
+ for (let index = events.length - 1; index >= 0; index -= 1) {
126
+ const event = events[index]
127
+ if (event.type === 'plan/mode') return event.data?.active === true
128
+ }
129
+ return false
130
+ }
131
+
132
+ function resolveInteraction(config: Config, session: Session): 'ask' | 'auto' {
133
+ if (config.interaction === 'ask' || config.interaction === 'auto') return config.interaction
134
+ return lastApprovalPolicy(session) === 'never' ? 'auto' : 'ask'
135
+ }
136
+
137
+
138
+ interface ToolRegistryLike {
139
+ register(definition: unknown): () => void
140
+ }
141
+
142
+ interface SystemPromptLike {
143
+ context(contribution: PromptContext): () => void
144
+ }
145
+
146
+ /**
147
+ * Model-visible catalog entry (cordis_inspect_list / cordis_inspect_query):
148
+ * lets the model read this plugin's runtime status without guessing. Mirrors
149
+ * the registration pattern of the official dsh-tool-cordis host providers.
150
+ */
151
+ function inspectProvider(config: Config, isToolRegistered: () => boolean, isDataToolRegistered: () => boolean, isConcurrencyToolRegistered: () => boolean, isComposeToolRegistered: () => boolean, isExportToolRegistered: () => boolean, isUmlToolRegistered: () => boolean, isStructureToolRegistered: () => boolean, isReportDiffToolRegistered: () => boolean): HostCordisInspectProviderRegistration {
152
+ return {
153
+ manifest: {
154
+ id: 'dsh-model-everything',
155
+ description: 'Model and diagram tooling for DeepSeek Harness: UML diagrams (render, read, review), executable verification of state machines, protocols and composed machines, data-model and migration checks, concurrency claim routing, external-checker export, dependency-graph review, and report diffing. The tools carry their own descriptions; the skills carry the doctrine and the per-domain detail.',
156
+ methods: [
157
+ {
158
+ name: 'status',
159
+ description: 'Read this plugin status: whether it registers its tools, which tools are on, and the model schema versions the bundled engines accept.',
160
+ inputSchema: {
161
+ type: 'object',
162
+ properties: {},
163
+ additionalProperties: false,
164
+ },
165
+ outputSchema: {
166
+ type: 'object',
167
+ description: 'Plugin status: registration state and the schema versions the bundled engines accept.',
168
+ properties: {
169
+ interaction: { type: 'string', enum: ['ask', 'auto', 'follow-approval'], description: 'Configured interaction mode. follow-approval resolves per session from approval/policy.' },
170
+ toolRegistered: { type: 'boolean', description: 'Whether the model_verify tool is registered on ctx.tools.' },
171
+ dataToolRegistered: { type: 'boolean', description: 'Whether the model_datamodel_verify tool is registered on ctx.tools.' },
172
+ concurrencyToolRegistered: { type: 'boolean', description: 'Whether the model_interleaving_scan tool is registered on ctx.tools.' },
173
+ composeToolRegistered: { type: 'boolean', description: 'Whether the model_compose_verify tool is registered on ctx.tools.' },
174
+ exportToolRegistered: { type: 'boolean', description: 'Whether the model_export tool is registered on ctx.tools.' },
175
+ umlToolsRegistered: { type: 'boolean', description: 'Whether the four UML tools (uml_render, uml_parse, uml_review, uml_explain_labels) are registered on ctx.tools.' },
176
+ structureToolRegistered: { type: 'boolean', description: 'Whether the uml_structure_verify tool (structure-diagram / dependency review) is registered on ctx.tools.' },
177
+ reportDiffToolRegistered: { type: 'boolean', description: 'Whether the report_diff tool (baseline report comparison) is registered on ctx.tools.' },
178
+ engineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled state-machine verification engine accepts.' },
179
+ dataEngineSchemaVersion: { type: 'integer', description: 'Model schema version the bundled data-model verification engine accepts.' },
180
+ },
181
+ additionalProperties: false,
182
+ },
183
+ },
184
+ ],
185
+ },
186
+ query: async (method) => {
187
+ if (method === 'status') {
188
+ return {
189
+ interaction: config.interaction,
190
+ toolRegistered: isToolRegistered(),
191
+ dataToolRegistered: isDataToolRegistered(),
192
+ concurrencyToolRegistered: isConcurrencyToolRegistered(),
193
+ composeToolRegistered: isComposeToolRegistered(),
194
+ exportToolRegistered: isExportToolRegistered(),
195
+ umlToolsRegistered: isUmlToolRegistered(),
196
+ structureToolRegistered: isStructureToolRegistered(),
197
+ reportDiffToolRegistered: isReportDiffToolRegistered(),
198
+ engineSchemaVersion: ENGINE_SCHEMA_VERSION,
199
+ dataEngineSchemaVersion: DATA_ENGINE_SCHEMA_VERSION,
200
+ }
201
+ }
202
+ return null
203
+ },
204
+ }
205
+ }
206
+
207
+ export function apply(ctx: Context, config: Config): void {
208
+ // Optional services are registered opportunistically; base-bundle rows can
209
+ // mount after this row applies, so registration is retried on the first
210
+ // agent/pre-step — by then the app is fully booted.
211
+ let providerRegistered = false
212
+ let toolRegistered = false
213
+ let dataToolRegistered = false
214
+ let concurrencyToolRegistered = false
215
+ let composeToolRegistered = false
216
+ let exportToolRegistered = false
217
+ let umlToolsRegistered = false
218
+ let structureToolRegistered = false
219
+ let reportDiffToolRegistered = false
220
+ const registerProvider = (): void => {
221
+ if (providerRegistered) return
222
+ const inspect = ctx.get('cordisInspect')
223
+ if (inspect === undefined) return
224
+ try {
225
+ ctx.effect(() => inspect.register(inspectProvider(config, () => toolRegistered, () => dataToolRegistered, () => concurrencyToolRegistered, () => composeToolRegistered, () => exportToolRegistered, () => umlToolsRegistered, () => structureToolRegistered, () => reportDiffToolRegistered)), 'dsh-model-everything: inspect provider')
226
+ providerRegistered = true
227
+ } catch (err) {
228
+ console.warn('[dsh-model-everything] inspect provider registration failed', err)
229
+ }
230
+ }
231
+ const registerTool = (): void => {
232
+ if (toolRegistered) return
233
+ const tools = ctx.get('tools') as ToolRegistryLike | undefined
234
+ if (tools === undefined) return
235
+ try {
236
+ ctx.effect(() => tools.register(modelVerifyTool), 'dsh-model-everything: verify tool')
237
+ ctx.effect(() => tools.register(modelDataModelVerifyTool), 'dsh-model-everything: data verify tool')
238
+ ctx.effect(() => tools.register(modelConcurrencyScanTool), 'dsh-model-everything: concurrency scan tool')
239
+ ctx.effect(() => tools.register(modelComposeTool), 'dsh-model-everything: compose tool')
240
+ ctx.effect(() => tools.register(modelExportTool), 'dsh-model-everything: export tool')
241
+ ctx.effect(() => tools.register(umlRenderTool), 'dsh-model-everything: uml render tool')
242
+ ctx.effect(() => tools.register(umlParseTool), 'dsh-model-everything: uml parse tool')
243
+ ctx.effect(() => tools.register(umlReviewTool), 'dsh-model-everything: uml review tool')
244
+ ctx.effect(() => tools.register(umlExplainLabelsTool), 'dsh-model-everything: uml explain-labels tool')
245
+ ctx.effect(() => tools.register(umlStructureTool), 'dsh-model-everything: structure tool')
246
+ ctx.effect(() => tools.register(designTool), 'dsh-model-everything: design review tool')
247
+ ctx.effect(() => tools.register(reportDiffTool), 'dsh-model-everything: report diff tool')
248
+ toolRegistered = true
249
+ dataToolRegistered = true
250
+ concurrencyToolRegistered = true
251
+ composeToolRegistered = true
252
+ exportToolRegistered = true
253
+ umlToolsRegistered = true
254
+ structureToolRegistered = true
255
+ reportDiffToolRegistered = true
256
+ } catch (err) {
257
+ console.warn('[dsh-model-everything] model_verify/model_datamodel_verify tool registration failed', err)
258
+ }
259
+ }
260
+ const registerIntegrations = (): void => {
261
+ registerProvider()
262
+ registerTool()
263
+ }
264
+ registerIntegrations()
265
+ // Ship the bundled skill through the registry: reuse the standard
266
+ // filesystem provider over this package's own `skills/` directory, so
267
+ // catalog discovery, frontmatter parsing, and SKILL.md loading behave
268
+ // exactly like project/user skills while the plugin stays self-contained.
269
+ // Registration lands in the global registry layer (this row mounts at the
270
+ // profile root), so every agent preset sees the skill. `registerProvider`
271
+ // returns the effect disposer; its teardown unregisters and invalidates.
272
+ ctx.skills.registerProvider((control) => {
273
+ return new FileSystemSkillProvider(ctx, control, {
274
+ providerName: 'dsh-model-everything',
275
+ includeDefaultRoots: false,
276
+ customSkillDirs: [SKILLS_DIR],
277
+ })
278
+ })
279
+ // Optional services register opportunistically and a base-bundle row can mount after
280
+ // this one applies, so registration is retried on the first model step - the point by
281
+ // which the app is fully booted. Nothing is appended to the step: the tools and the
282
+ // skill text are the guide, not an injected reminder. Session-start routing, if a
283
+ // deployment wants it, is the separate gate component in `gate.ts`, not this row.
284
+ ctx.on('agent/pre-step', async (_payload, next) => {
285
+ const decision = await next()
286
+ registerIntegrations()
287
+ return decision
288
+ })
289
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Structural mirror of the JSON value union the dsh tool registry accepts.
3
+ *
4
+ * `@deepseek-ai/dsh-tools` exported this type through the 0.1.x line and stopped
5
+ * exporting it in 0.2.x, where the same type now lives in
6
+ * `@deepseek-ai/dsh-util-values`. This package declares peer support for both
7
+ * lines, so it cannot import the type from either one without breaking the other
8
+ * build. Declaring the one-line union here keeps `npm run typecheck` green against
9
+ * both, and it costs nothing at runtime: every use is a type-only cast on a value
10
+ * this package already produced.
11
+ *
12
+ * Keep it structurally identical to the official type. A cast through `unknown` to
13
+ * this alias is only a way to satisfy `defineTool`'s return-type constraint; if the
14
+ * official union ever widens, this alias must widen with it.
15
+ *
16
+ * @module this plugin-json-value
17
+ */
18
+
19
+ /** JSON-serializable value: null, boolean, number, string, or an array/object of those. */
20
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
@@ -0,0 +1,49 @@
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
+
12
+ import { OptionShapeError } from './option-shape.js'
13
+
14
+ /** Read and parse one JSON file. Formatting is irrelevant: whitespace, BOM and pretty printing all parse. */
15
+ export function readJsonFile(path: unknown, option: string, code: string): unknown {
16
+ if (typeof path !== 'string' || path.trim() === '') {
17
+ throw new OptionShapeError(code, `${option} must be a non-empty path to a JSON file.`)
18
+ }
19
+ const full = resolve(path)
20
+ let text: string
21
+ try {
22
+ text = readFileSync(full, 'utf8')
23
+ } catch (error) {
24
+ const reason = error instanceof Error ? error.message : String(error)
25
+ 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.`)
26
+ }
27
+ try {
28
+ return JSON.parse(text.replace(/^\uFEFF/, ''))
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
+ /**
36
+ * Exactly one of the inline value and the file path. Both is ambiguous, neither is a missing
37
+ * argument, and each gets its own sentence so the caller knows which one to drop.
38
+ */
39
+ export function oneOf(value: unknown, path: unknown, inlineName: string, pathName: string, code: string): unknown {
40
+ const hasValue = value !== undefined
41
+ const hasPath = path !== undefined
42
+ if (hasValue && hasPath) {
43
+ throw new OptionShapeError(code, `Pass either ${inlineName} or ${pathName}, not both: the file would be read and then ignored, or the other way round.`)
44
+ }
45
+ if (!hasValue && !hasPath) {
46
+ throw new OptionShapeError(code, `Pass ${inlineName} or ${pathName}: the model has to come from somewhere.`)
47
+ }
48
+ return hasPath ? readJsonFile(path, pathName, code) : value
49
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Option shapes at the tool boundary.
3
+ *
4
+ * A tool declares `type: 'json'` for an option, which accepts any JSON value, and the engine then reads
5
+ * it as the type it needs. A string where an array was expected threw `machinesInput.forEach is not a
6
+ * function` and `(options.migrationMappings ?? []).map is not a function` - raw TypeErrors with no code,
7
+ * which a caller cannot route on and which say nothing about which option was wrong.
8
+ *
9
+ * The suite pinned those as known gaps. This closes them the way the rest of the plugin reports a
10
+ * refusal: a code naming the family, and a message naming the option.
11
+ */
12
+
13
+ /** A refusal the caller can route on: the domain's code, and what arrived. */
14
+ export class OptionShapeError extends Error {
15
+ constructor(
16
+ readonly errorCode: string,
17
+ message: string,
18
+ ) {
19
+ super(message)
20
+ this.name = 'OptionShapeError'
21
+ }
22
+ }
23
+
24
+ function describe(value: unknown): string {
25
+ if (value === null) return 'null'
26
+ if (Array.isArray(value)) return 'an array'
27
+ return 'a ' + typeof value
28
+ }
29
+
30
+ /** An option the engine reads with `.map`: a string or a number is not one. */
31
+ export function requireArrayOption(value: unknown, option: string, errorCode: string): void {
32
+ if (value === undefined) return
33
+ if (!Array.isArray(value)) {
34
+ throw new OptionShapeError(errorCode, option + ' is ' + describe(value) + ', not an array. This option is read as a list; omit it, or pass the list the engine documents.')
35
+ }
36
+ // A null ENTRY fails later, deeper, with a message about a property of null. Naming the index here is the
37
+ // difference between "something is wrong" and "entry 2 is wrong".
38
+ const bad = value.findIndex((entry) => entry === null || typeof entry !== 'object' || Array.isArray(entry))
39
+ if (bad >= 0) {
40
+ throw new OptionShapeError(errorCode, option + '[' + String(bad) + '] is ' + describe(value[bad]) + ', not an object. Each entry is read as a mapping or a pair; a position cannot be omitted.')
41
+ }
42
+ }
43
+
44
+ /** An option the engine reads with `Object.entries`: a string has entries, and they are not the ones meant. */
45
+ export function requireObjectOption(value: unknown, option: string, errorCode: string): void {
46
+ if (value === undefined) return
47
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
48
+ throw new OptionShapeError(errorCode, option + ' is ' + describe(value) + ', not an object. This option is read as a map from one name to another.')
49
+ }
50
+ }
51
+
52
+ /** The refusal a tool returns for a shape it could not read. */
53
+ export function optionRefusal(error: unknown, errorCode: string): Record<string, unknown> {
54
+ const message = error instanceof Error ? error.message : String(error)
55
+ const code = error instanceof OptionShapeError ? error.errorCode : errorCode
56
+ return {
57
+ ok: false,
58
+ ran: false,
59
+ verdict: 'fail',
60
+ verdictReason: 'the tool did not run: ' + message,
61
+ errorCode: code,
62
+ error: message,
63
+ }
64
+ }
@@ -0,0 +1,67 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from './json-value.js'
3
+ import { diffReports } from './baseline.js'
4
+ import { refusalVerdict } from './engine.js'
5
+ import { renderReportText } from './report-view.js'
6
+
7
+ export const REPORT_DIFF_TOOL_NAME = 'report_diff'
8
+
9
+ /**
10
+ * Turn a refusal into a report the model can route on.
11
+ *
12
+ * `diffReports` throws when a side is not a report, which is the right thing at the engine boundary -
13
+ * there is nothing to compare and no verdict to give. The tool boundary has a different obligation:
14
+ * malformed input has to come back as something with a code, never as a raw throw. This is the same
15
+ * shape the UML tools use, with a code of this tool's own.
16
+ */
17
+ function refusal(error: unknown): JsonValue {
18
+ const message = error instanceof Error ? error.message : String(error)
19
+ return { ok: false, ran: false, ...refusalVerdict(message), errorCode: 'REPORT_INVALID', error: message } as unknown as JsonValue
20
+ }
21
+
22
+ /**
23
+ * Model-visible DSH tool that compares two reports of the same family — the accepted
24
+ * "violations must not increase" acceptance criterion, computed instead of eyeballed.
25
+ * A finding's identity is its check + code + machine-readable locator, so a reworded
26
+ * message reads as `changed`, and two runs over the same input add nothing.
27
+ */
28
+ export const reportDiffTool = defineTool({
29
+ name: REPORT_DIFF_TOOL_NAME,
30
+ description:
31
+ 'Compare two reports of the same family, to accept a change by what it added rather than by reading two reports: `baseline` (earlier) against `current` (newer). Use it to accept a slice by "no new findings". Findings match on a stable identity: check id, code, and a locator built from `evidence` and `path`. Reworded prose counts as `changed`, not as new. Returns added, removed and changed findings, plus `summary`. It does not look at a single artefact and it does not produce a report of its own: for the first look at one, call the domain tool directly (`model_verify`, `model_datamodel_verify`, `model_design_review`, `uml_review`, `uml_structure_verify`, `model_interleaving_scan`), then compare what those two calls returned. `verdict` is about the delta: a newly added error finding fails it. `currentVerdict` keeps the run\'s own verdict, so a clean delta over a failing report still shows as failing. Two runs over the same input add nothing. Both sides have to be reports this can compare: a refusal, a run that examined nothing, and a report from another family are refused with `REPORT_INVALID` instead of being compared, because a finding absent from an incomparable roster would otherwise read as resolved.',
32
+ parameters: {
33
+ baseline: {
34
+ type: 'json',
35
+ required: true,
36
+ description: 'The earlier report (any family: verify, compose, datamodel, uml review, structure, concurrency).',
37
+ },
38
+ current: {
39
+ type: 'json',
40
+ required: true,
41
+ description: 'The current report to compare against the baseline.',
42
+ },
43
+ reportView: {
44
+ type: 'string',
45
+ enum: ['digest', 'full'],
46
+ description: 'digest (default) prints the readable body; full prints the structured diff report.',
47
+ },
48
+ },
49
+ output: {
50
+ schema: {
51
+ type: 'json',
52
+ description: 'Baseline diff report: added/removed/changed findings, delta verdict, current verdict, summary and next steps.',
53
+ },
54
+ render(args, value) {
55
+ return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }]
56
+ },
57
+ },
58
+ timeoutMs: 10_000,
59
+ isConcurrencySafe: () => true,
60
+ async execute(args) {
61
+ try {
62
+ return diffReports(args.baseline, args.current) as unknown as JsonValue
63
+ } catch (error) {
64
+ return refusal(error)
65
+ }
66
+ },
67
+ })