dsh-model-everything 0.2.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 +104 -76
- package/README.md +84 -71
- package/lib/automaton-tool.js +70 -0
- package/lib/automaton.js +328 -0
- package/lib/compose-tool.js +7 -0
- package/lib/concurrency-tool.js +7 -0
- package/lib/data-tool.js +7 -0
- package/lib/design-tool.js +8 -1
- package/lib/edit-tool.js +113 -0
- package/lib/editor.js +293 -0
- package/lib/engine.js +7 -0
- package/lib/export-tool.js +8 -0
- package/lib/index.js +4 -0
- package/lib/presentation.js +309 -0
- package/lib/report-diff-tool.js +7 -0
- package/lib/report-view.js +62 -10
- package/lib/routing.js +85 -0
- package/lib/scan.js +547 -0
- package/lib/structure-tool.js +35 -1
- package/lib/structure.js +28 -3
- package/lib/tool.js +7 -0
- package/lib/types/automaton-tool.d.ts +2 -0
- package/lib/types/automaton.d.ts +57 -0
- package/lib/types/edit-tool.d.ts +2 -0
- package/lib/types/editor.d.ts +46 -0
- package/lib/types/engine.d.ts +1 -0
- package/lib/types/presentation.d.ts +164 -0
- package/lib/types/routing.d.ts +31 -0
- package/lib/types/scan.d.ts +37 -0
- package/lib/types/structure.d.ts +16 -0
- package/lib/uml-tool.js +21 -6
- package/package.json +6 -3
- package/skills/model-everything/SKILL.md +6 -2
- package/skills/model-everything/references/automata.md +102 -0
- package/src/automaton-tool.ts +74 -0
- package/src/automaton.ts +376 -0
- package/src/compose-tool.ts +13 -5
- package/src/concurrency-tool.ts +8 -0
- package/src/data-tool.ts +8 -0
- package/src/design-tool.ts +76 -68
- package/src/edit-tool.ts +121 -0
- package/src/editor.ts +297 -0
- package/src/engine.ts +7 -0
- package/src/export-tool.ts +90 -81
- package/src/index.ts +4 -0
- package/src/presentation.ts +365 -0
- package/src/report-diff-tool.ts +8 -0
- package/src/report-view.ts +53 -2
- package/src/routing.ts +96 -0
- package/src/scan.ts +564 -0
- package/src/structure-tool.ts +34 -1
- package/src/structure.ts +37 -3
- package/src/tool.ts +7 -0
- package/src/uml-tool.ts +245 -230
package/lib/editor.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applying edits to a model file: the delta side of the verification loop.
|
|
3
|
+
*
|
|
4
|
+
* A model is the largest thing any of these tools takes. Handing it back whole on every
|
|
5
|
+
* iteration is what makes a four-round fix cost four models. Here an edit names one change,
|
|
6
|
+
* the file carries the rest, and the caller gets the structural diff plus - when asked - the
|
|
7
|
+
* difference in what the engine decided, in the same call.
|
|
8
|
+
*
|
|
9
|
+
* Every operation works on a copy and the result is validated before anything is written. A
|
|
10
|
+
* refused edit leaves the file exactly as it was: an invalid model on disk would make the next
|
|
11
|
+
* verification spend a round reporting that, which is the cost this tool exists to remove.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
import { validateModel } from './engine.js';
|
|
16
|
+
import { OptionShapeError } from './option-shape.js';
|
|
17
|
+
const CLONE = (value) => JSON.parse(JSON.stringify(value));
|
|
18
|
+
/** Read the model at `path`; a missing file is a refusal, not an empty model. */
|
|
19
|
+
export function readModelFile(path) {
|
|
20
|
+
if (typeof path !== 'string' || path.trim() === '') {
|
|
21
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'modelPath must be a non-empty path to a JSON file.');
|
|
22
|
+
}
|
|
23
|
+
const full = resolve(path);
|
|
24
|
+
let text;
|
|
25
|
+
try {
|
|
26
|
+
text = readFileSync(full, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
30
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `modelPath could not be read: ${reason}. Edits apply to a file that already exists; model_verify can create the model inline first, or write it yourself.`);
|
|
31
|
+
}
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(text.replace(/^\uFEFF/, ''));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
38
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `modelPath is not valid JSON: ${reason}.`);
|
|
39
|
+
}
|
|
40
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
41
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'modelPath must hold a model object, not a list or a scalar.');
|
|
42
|
+
}
|
|
43
|
+
return { model: parsed, full };
|
|
44
|
+
}
|
|
45
|
+
function statesOf(model) {
|
|
46
|
+
return Array.isArray(model.states) ? model.states : [];
|
|
47
|
+
}
|
|
48
|
+
function transitionsOf(model) {
|
|
49
|
+
return Array.isArray(model.transitions) ? model.transitions : [];
|
|
50
|
+
}
|
|
51
|
+
function requireString(value, name) {
|
|
52
|
+
if (typeof value !== 'string' || value === '')
|
|
53
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `${name} must be a non-empty string.`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/** JSON pointer, so anything without a named operation still has a way to be edited. */
|
|
57
|
+
function pointerParts(path) {
|
|
58
|
+
const text = requireString(path, 'path');
|
|
59
|
+
if (!text.startsWith('/'))
|
|
60
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `path must be a JSON pointer starting with "/", got ${JSON.stringify(text)}.`);
|
|
61
|
+
return text.split('/').slice(1).map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
62
|
+
}
|
|
63
|
+
function atPointer(model, parts, create) {
|
|
64
|
+
let node = model;
|
|
65
|
+
for (const part of parts.slice(0, -1)) {
|
|
66
|
+
const next = node[part];
|
|
67
|
+
if (next === undefined) {
|
|
68
|
+
if (!create)
|
|
69
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `path does not exist at "${part}".`);
|
|
70
|
+
const created = {};
|
|
71
|
+
node[part] = created;
|
|
72
|
+
node = created;
|
|
73
|
+
}
|
|
74
|
+
else if (next !== null && typeof next === 'object') {
|
|
75
|
+
node = next;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `path runs through "${part}", which is not an object.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { parent: node, key: parts[parts.length - 1] ?? '' };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Accept the compact spelling of an operation as well as the long one.
|
|
85
|
+
*
|
|
86
|
+
* An operation is a small thing - add a state, wire a transition - and the long form spends more
|
|
87
|
+
* tokens on key names than on the change. Five operations cost 76 tokens spelled out and 39 in the
|
|
88
|
+
* compact form, and a batch of twenty is where that difference starts to matter.
|
|
89
|
+
*
|
|
90
|
+
* {addState: "c"} {addState: {id: "c", terminal: true}}
|
|
91
|
+
* {removeState: "c"}
|
|
92
|
+
* {addTransition: ["a", "reset", "c"]} {addTransition: {from, event, to, guard?, ...}}
|
|
93
|
+
* {removeTransition: ["a", "go"]} {updateTransition: ["a", "go", {guard: ...}]}
|
|
94
|
+
* {set: ["/init", "a"]} {remove: "/init"}
|
|
95
|
+
*
|
|
96
|
+
* The long form is unchanged and is what the errors talk about.
|
|
97
|
+
*/
|
|
98
|
+
export function normalizeOperation(raw) {
|
|
99
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
100
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'every op must be an object.');
|
|
101
|
+
}
|
|
102
|
+
const value = raw;
|
|
103
|
+
if (typeof value.op === 'string')
|
|
104
|
+
return value;
|
|
105
|
+
const keys = Object.keys(value);
|
|
106
|
+
if (keys.length !== 1) {
|
|
107
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `an operation names exactly one op, got ${JSON.stringify(keys)}.`);
|
|
108
|
+
}
|
|
109
|
+
const name = keys[0];
|
|
110
|
+
const argument = value[name];
|
|
111
|
+
if (name === 'addState' || name === 'removeState') {
|
|
112
|
+
if (typeof argument === 'string') {
|
|
113
|
+
return name === 'addState' ? { op: name, state: { id: argument } } : { op: name, id: argument };
|
|
114
|
+
}
|
|
115
|
+
if (name === 'addState' && argument !== null && typeof argument === 'object')
|
|
116
|
+
return { op: name, state: argument };
|
|
117
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `${name} takes a state id, or a state object for addState.`);
|
|
118
|
+
}
|
|
119
|
+
if (name === 'addTransition' || name === 'removeTransition' || name === 'updateTransition') {
|
|
120
|
+
if (Array.isArray(argument)) {
|
|
121
|
+
if (name === 'addTransition' && argument.length === 3) {
|
|
122
|
+
const [from, event, to] = argument;
|
|
123
|
+
return { op: name, transition: { from, event, to } };
|
|
124
|
+
}
|
|
125
|
+
if (name === 'removeTransition' && argument.length === 2) {
|
|
126
|
+
const [from, event] = argument;
|
|
127
|
+
return { op: name, from, event };
|
|
128
|
+
}
|
|
129
|
+
if (name === 'updateTransition' && argument.length === 3) {
|
|
130
|
+
const [from, event, patch] = argument;
|
|
131
|
+
return { op: name, from, event, patch };
|
|
132
|
+
}
|
|
133
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `${name} takes ${name === 'updateTransition' ? '["from", "event", {patch}]' : name === 'addTransition' ? '["from", "event", "to"]' : '["from", "event"]'}.`);
|
|
134
|
+
}
|
|
135
|
+
if (name === 'addTransition' && argument !== null && typeof argument === 'object')
|
|
136
|
+
return { op: name, transition: argument };
|
|
137
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `${name} takes a list, or a transition object for addTransition.`);
|
|
138
|
+
}
|
|
139
|
+
if (name === 'set') {
|
|
140
|
+
if (Array.isArray(argument) && argument.length === 2)
|
|
141
|
+
return { op: 'set', path: argument[0], value: argument[1] };
|
|
142
|
+
if (argument !== null && typeof argument === 'object' && 'path' in argument) {
|
|
143
|
+
const { path, value } = argument;
|
|
144
|
+
return { op: 'set', path, value };
|
|
145
|
+
}
|
|
146
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'set takes ["/path", value] or {path, value}.');
|
|
147
|
+
}
|
|
148
|
+
if (name === 'remove') {
|
|
149
|
+
if (typeof argument === 'string')
|
|
150
|
+
return { op: 'remove', path: argument };
|
|
151
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'remove takes a JSON pointer string.');
|
|
152
|
+
}
|
|
153
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `unknown op ${JSON.stringify(name)}. Known ops: addState, removeState, addTransition, removeTransition, updateTransition, set, remove.`);
|
|
154
|
+
}
|
|
155
|
+
/** Apply one operation to a model copy, returning the changes it made. */
|
|
156
|
+
export function applyOperation(model, operation) {
|
|
157
|
+
const op = requireString(operation.op, 'op');
|
|
158
|
+
const changes = [];
|
|
159
|
+
if (op === 'addState') {
|
|
160
|
+
const state = operation.state;
|
|
161
|
+
const id = requireString(state?.id, 'state.id');
|
|
162
|
+
if (statesOf(model).some((entry) => entry.id === id)) {
|
|
163
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `state "${id}" already exists; use update on it instead of adding it twice.`);
|
|
164
|
+
}
|
|
165
|
+
model.states = [...statesOf(model), state];
|
|
166
|
+
changes.push({ kind: 'added', what: 'state ' + id, detail: state?.terminal === true ? 'terminal' : '' });
|
|
167
|
+
return changes;
|
|
168
|
+
}
|
|
169
|
+
if (op === 'removeState') {
|
|
170
|
+
const id = requireString(operation.id, 'id');
|
|
171
|
+
const before = statesOf(model);
|
|
172
|
+
if (!before.some((entry) => entry.id === id))
|
|
173
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `no state "${id}" to remove.`);
|
|
174
|
+
model.states = before.filter((entry) => entry.id !== id);
|
|
175
|
+
const dropped = transitionsOf(model).filter((entry) => entry.from === id || entry.to === id);
|
|
176
|
+
model.transitions = transitionsOf(model).filter((entry) => entry.from !== id && entry.to !== id);
|
|
177
|
+
changes.push({ kind: 'removed', what: 'state ' + id, detail: dropped.length > 0 ? `and ${String(dropped.length)} transition(s) that used it` : '' });
|
|
178
|
+
return changes;
|
|
179
|
+
}
|
|
180
|
+
if (op === 'addTransition') {
|
|
181
|
+
const transition = operation.transition;
|
|
182
|
+
const from = requireString(transition?.from, 'transition.from');
|
|
183
|
+
const event = requireString(transition?.event, 'transition.event');
|
|
184
|
+
const to = requireString(transition?.to, 'transition.to');
|
|
185
|
+
if (transitionsOf(model).some((entry) => entry.from === from && entry.event === event)) {
|
|
186
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `a transition for ${from} + ${event} already exists; update it instead of adding another.`);
|
|
187
|
+
}
|
|
188
|
+
model.transitions = [...transitionsOf(model), transition];
|
|
189
|
+
changes.push({ kind: 'added', what: `transition ${from} + ${event}`, detail: 'to ' + to });
|
|
190
|
+
return changes;
|
|
191
|
+
}
|
|
192
|
+
if (op === 'removeTransition') {
|
|
193
|
+
const from = requireString(operation.from, 'from');
|
|
194
|
+
const event = requireString(operation.event, 'event');
|
|
195
|
+
const before = transitionsOf(model);
|
|
196
|
+
const kept = before.filter((entry) => !(entry.from === from && entry.event === event));
|
|
197
|
+
if (kept.length === before.length)
|
|
198
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `no transition for ${from} + ${event} to remove.`);
|
|
199
|
+
model.transitions = kept;
|
|
200
|
+
changes.push({ kind: 'removed', what: `transition ${from} + ${event}`, detail: '' });
|
|
201
|
+
return changes;
|
|
202
|
+
}
|
|
203
|
+
if (op === 'updateTransition') {
|
|
204
|
+
const from = requireString(operation.from, 'from');
|
|
205
|
+
const event = requireString(operation.event, 'event');
|
|
206
|
+
const patch = operation.patch;
|
|
207
|
+
if (patch === undefined || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
208
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'patch must be an object of the fields to replace.');
|
|
209
|
+
}
|
|
210
|
+
const list = transitionsOf(model);
|
|
211
|
+
const index = list.findIndex((entry) => entry.from === from && entry.event === event);
|
|
212
|
+
if (index < 0)
|
|
213
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `no transition for ${from} + ${event} to update.`);
|
|
214
|
+
list[index] = { ...list[index], ...patch };
|
|
215
|
+
model.transitions = list;
|
|
216
|
+
changes.push({ kind: 'updated', what: `transition ${from} + ${event}`, detail: Object.keys(patch).join(', ') });
|
|
217
|
+
return changes;
|
|
218
|
+
}
|
|
219
|
+
if (op === 'set') {
|
|
220
|
+
const parts = pointerParts(operation.path);
|
|
221
|
+
if (parts.length === 0)
|
|
222
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'path must name something inside the model.');
|
|
223
|
+
const { parent, key } = atPointer(model, parts, true);
|
|
224
|
+
parent[key] = CLONE(operation.value);
|
|
225
|
+
changes.push({ kind: 'updated', what: 'set ' + String(operation.path), detail: '' });
|
|
226
|
+
return changes;
|
|
227
|
+
}
|
|
228
|
+
if (op === 'remove') {
|
|
229
|
+
const parts = pointerParts(operation.path);
|
|
230
|
+
if (parts.length === 0)
|
|
231
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'path must name something inside the model.');
|
|
232
|
+
const { parent, key } = atPointer(model, parts, false);
|
|
233
|
+
if (!(key in parent))
|
|
234
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `nothing at ${String(operation.path)} to remove.`);
|
|
235
|
+
delete parent[key];
|
|
236
|
+
changes.push({ kind: 'removed', what: 'remove ' + String(operation.path), detail: '' });
|
|
237
|
+
return changes;
|
|
238
|
+
}
|
|
239
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `unknown op ${JSON.stringify(op)}. Known ops: addState, removeState, addTransition, removeTransition, updateTransition, set, remove.`);
|
|
240
|
+
}
|
|
241
|
+
export function applyEdits(path, operations, options = {}) {
|
|
242
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
243
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', 'ops must be a non-empty array of operations.');
|
|
244
|
+
}
|
|
245
|
+
const { model: original, full } = readModelFile(path);
|
|
246
|
+
const next = CLONE(original);
|
|
247
|
+
const changes = [];
|
|
248
|
+
const normalized = operations.map((raw, index) => {
|
|
249
|
+
try {
|
|
250
|
+
return normalizeOperation(raw);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
254
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `operation ${String(index + 1)} was refused: ${reason}`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
for (const [index, raw] of normalized.entries()) {
|
|
258
|
+
try {
|
|
259
|
+
changes.push(...applyOperation(next, raw));
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
263
|
+
throw new OptionShapeError('EDIT_OPTION_INVALID', `operation ${String(index + 1)} (${String(raw.op)}) was refused: ${reason}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const validation = validateModel(next);
|
|
267
|
+
if (!validation.ok && options.allowInvalid !== true) {
|
|
268
|
+
// Name the operation the batch first stops validating at, not just the symptom: a caller that
|
|
269
|
+
// reads "a transition to an unknown state" still has to find which entry introduced it.
|
|
270
|
+
let firstBad = -1;
|
|
271
|
+
const partial = CLONE(original);
|
|
272
|
+
for (const [index, raw] of operations.entries()) {
|
|
273
|
+
try {
|
|
274
|
+
applyOperation(partial, raw);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
firstBad = index;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
if (!validateModel(partial).ok) {
|
|
281
|
+
firstBad = index;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const where = firstBad < 0 ? '' : ` Operation ${String(firstBad + 1)} (${String(operations[firstBad]?.op)}) is where it first stops validating.`;
|
|
286
|
+
throw new OptionShapeError('EDIT_RESULT_INVALID', 'the batch would leave an invalid model, so nothing was written: ' + validation.errors.join('; ') + '.' + where + ' Fix the list, or pass allowInvalid to write it anyway and continue in a second call.');
|
|
287
|
+
}
|
|
288
|
+
const text = JSON.stringify(validation.ok ? validation.model : next, null, 2) + '\n';
|
|
289
|
+
const temporary = full + '.tmp';
|
|
290
|
+
writeFileSync(temporary, text, 'utf8');
|
|
291
|
+
renameSync(temporary, full);
|
|
292
|
+
return { model: next, changes, full, valid: validation.ok, errors: validation.ok ? [] : validation.errors };
|
|
293
|
+
}
|
package/lib/engine.js
CHANGED
|
@@ -62,6 +62,12 @@ function verificationNextSteps(checks, coverage) {
|
|
|
62
62
|
if (coverage !== undefined && !narrativeComplete(coverage)) {
|
|
63
63
|
steps.push('Complete the narrative (states ' + coverage.states + ', events ' + coverage.events + ', scenarios ' + coverage.scenarios + ') — a partial narrative is valid, but a reader still has to re-derive the missing symbols.');
|
|
64
64
|
}
|
|
65
|
+
// A truncated search is the one outcome where "look harder" is the wrong advice: the wall is
|
|
66
|
+
// combinatorial, and raising maxStates buys a constant factor. Name the ladder instead, so the reader
|
|
67
|
+
// knows this is not a bug to fix but a model to reduce, or a different tool to use.
|
|
68
|
+
if (hasUndecidedFinding(checks)) {
|
|
69
|
+
steps.push('The search stopped before deciding, so the undecided checks are neither proven nor refuted. Do not just raise maxStates: first reduce the model (drop variables the property does not read, collapse symmetric states, order independent events), and if it still explodes, move to a tool that does not enumerate every state - SPIN for partial-order reduction, NuSMV/nuXMV for symbolic search, CBMC for bounded SAT. This plugin establishes no abstraction and reads no program source, so a property that needs one is outside it.');
|
|
70
|
+
}
|
|
65
71
|
// The range is dropped: the code that would be reported back is already named in `findings`, and a
|
|
66
72
|
// reader who has not seen a report yet learns nothing from four digits.
|
|
67
73
|
steps.push('Re-run with beforeModel/stateMapping after the change to prove the behaviour did not regress: behaviour preserved, invariants continuous, the state mapping consistent. Then compare the two reports with `report_diff` rather than reading them side by side.');
|
|
@@ -181,6 +187,7 @@ export const REPORT_SCHEMAS = {
|
|
|
181
187
|
granularity: 'model-everything/granularity/v1',
|
|
182
188
|
design: 'model-everything/design/v1',
|
|
183
189
|
baseline: 'model-everything/baseline/v1',
|
|
190
|
+
edit: 'model-everything/edit/v1',
|
|
184
191
|
};
|
|
185
192
|
export function narrativeCoverageOf(model) {
|
|
186
193
|
const narrative = model.narrative;
|
package/lib/export-tool.js
CHANGED
|
@@ -1,5 +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 { cardWithSource, reportCallView, reportCard, reportResultView } from './presentation.js';
|
|
3
4
|
import { ExportError, exportModel } from './exporters.js';
|
|
4
5
|
import { renderReportText } from './report-view.js';
|
|
5
6
|
/** The model hash of an export's input, when the input validates (the success path always does). */
|
|
@@ -44,7 +45,14 @@ export const modelExportTool = defineTool({
|
|
|
44
45
|
render(args, value) {
|
|
45
46
|
return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
|
|
46
47
|
},
|
|
48
|
+
// The card a UI shows, separated from the text the model reads.
|
|
49
|
+
presentationMeta(_args, value) {
|
|
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');
|
|
52
|
+
},
|
|
47
53
|
},
|
|
54
|
+
presentCall: reportCallView(EXPORT_TOOL_NAME),
|
|
55
|
+
presentResult: reportResultView(EXPORT_TOOL_NAME),
|
|
48
56
|
timeoutMs: 10_000,
|
|
49
57
|
isConcurrencySafe: () => true,
|
|
50
58
|
async execute(args) {
|
package/lib/index.js
CHANGED
|
@@ -33,6 +33,8 @@ import { umlRenderTool, umlParseTool, umlReviewTool, umlExplainLabelsTool } from
|
|
|
33
33
|
import { umlStructureTool } from './structure-tool.js';
|
|
34
34
|
import { designTool } from './design-tool.js';
|
|
35
35
|
import { reportDiffTool } from './report-diff-tool.js';
|
|
36
|
+
import { modelEditTool } from './edit-tool.js';
|
|
37
|
+
import { modelAutomatonTool } from './automaton-tool.js';
|
|
36
38
|
import { ENGINE_SCHEMA_VERSION } from './engine.js';
|
|
37
39
|
// DSH 0.1.7-alpha.1 (session format v4) retires the shared
|
|
38
40
|
// `{ kind: 'plugin', plugin }` wrapper: native admission rejects it in every
|
|
@@ -192,6 +194,8 @@ export function apply(ctx, config) {
|
|
|
192
194
|
ctx.effect(() => tools.register(umlStructureTool), 'dsh-model-everything: structure tool');
|
|
193
195
|
ctx.effect(() => tools.register(designTool), 'dsh-model-everything: design review tool');
|
|
194
196
|
ctx.effect(() => tools.register(reportDiffTool), 'dsh-model-everything: report diff tool');
|
|
197
|
+
ctx.effect(() => tools.register(modelEditTool), 'dsh-model-everything: model edit tool');
|
|
198
|
+
ctx.effect(() => tools.register(modelAutomatonTool), 'dsh-model-everything: automaton tool');
|
|
195
199
|
toolRegistered = true;
|
|
196
200
|
dataToolRegistered = true;
|
|
197
201
|
concurrencyToolRegistered = true;
|