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.
Files changed (54) hide show
  1. package/README.en-US.md +104 -76
  2. package/README.md +84 -71
  3. package/lib/automaton-tool.js +70 -0
  4. package/lib/automaton.js +328 -0
  5. package/lib/compose-tool.js +7 -0
  6. package/lib/concurrency-tool.js +7 -0
  7. package/lib/data-tool.js +7 -0
  8. package/lib/design-tool.js +8 -1
  9. package/lib/edit-tool.js +113 -0
  10. package/lib/editor.js +293 -0
  11. package/lib/engine.js +7 -0
  12. package/lib/export-tool.js +8 -0
  13. package/lib/index.js +4 -0
  14. package/lib/presentation.js +309 -0
  15. package/lib/report-diff-tool.js +7 -0
  16. package/lib/report-view.js +62 -10
  17. package/lib/routing.js +85 -0
  18. package/lib/scan.js +547 -0
  19. package/lib/structure-tool.js +35 -1
  20. package/lib/structure.js +28 -3
  21. package/lib/tool.js +7 -0
  22. package/lib/types/automaton-tool.d.ts +2 -0
  23. package/lib/types/automaton.d.ts +57 -0
  24. package/lib/types/edit-tool.d.ts +2 -0
  25. package/lib/types/editor.d.ts +46 -0
  26. package/lib/types/engine.d.ts +1 -0
  27. package/lib/types/presentation.d.ts +164 -0
  28. package/lib/types/routing.d.ts +31 -0
  29. package/lib/types/scan.d.ts +37 -0
  30. package/lib/types/structure.d.ts +16 -0
  31. package/lib/uml-tool.js +21 -6
  32. package/package.json +6 -3
  33. package/skills/model-everything/SKILL.md +6 -2
  34. package/skills/model-everything/references/automata.md +102 -0
  35. package/src/automaton-tool.ts +74 -0
  36. package/src/automaton.ts +376 -0
  37. package/src/compose-tool.ts +13 -5
  38. package/src/concurrency-tool.ts +8 -0
  39. package/src/data-tool.ts +8 -0
  40. package/src/design-tool.ts +76 -68
  41. package/src/edit-tool.ts +121 -0
  42. package/src/editor.ts +297 -0
  43. package/src/engine.ts +7 -0
  44. package/src/export-tool.ts +90 -81
  45. package/src/index.ts +4 -0
  46. package/src/presentation.ts +365 -0
  47. package/src/report-diff-tool.ts +8 -0
  48. package/src/report-view.ts +53 -2
  49. package/src/routing.ts +96 -0
  50. package/src/scan.ts +564 -0
  51. package/src/structure-tool.ts +34 -1
  52. package/src/structure.ts +37 -3
  53. package/src/tool.ts +7 -0
  54. package/src/uml-tool.ts +245 -230
package/lib/structure.js CHANGED
@@ -444,8 +444,9 @@ function requireDiagramText(value) {
444
444
  export function reviewStructure(options) {
445
445
  // Not `if (options.diagram !== undefined)`: a missing `diagram` reached the parser as `undefined`
446
446
  // and threw a read of `split` on it - the same defect, one guard narrower.
447
- requireDiagramText(options.diagram);
448
- const graph = parseStructure(options.diagram, options.notation ?? 'auto');
447
+ // A graph built from source arrives already parsed; a caller that passes one has no diagram text to
448
+ // require, so the text guard belongs to the other branch.
449
+ const graph = options.graph ?? parseStructure(requireDiagramText(options.diagram), options.notation ?? 'auto');
449
450
  const warnings = [...graph.warnings];
450
451
  const findings = [];
451
452
  const nodes = graph.nodes;
@@ -657,6 +658,25 @@ export function reviewStructure(options) {
657
658
  evidence: { missing },
658
659
  });
659
660
  }
661
+ // Degree is what a reader asks first and counts worst: which module everything depends on, and which
662
+ // depends on nothing. Counting it here means the answer is one line in the report rather than a scan of
663
+ // the graph by hand, which is where a correct graph still produced a wrong answer.
664
+ const inDegree = new Map();
665
+ const outDegree = new Map();
666
+ for (const edge of graph.edges) {
667
+ inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
668
+ outDegree.set(edge.from, (outDegree.get(edge.from) ?? 0) + 1);
669
+ }
670
+ const hubs = [...inDegree.entries()]
671
+ .filter(([, count]) => count > 0)
672
+ .sort((a, b) => (b[1] === a[1] ? a[0].localeCompare(b[0]) : b[1] - a[1]))
673
+ .slice(0, 5)
674
+ .map(([id, dependents]) => ({ id, dependents }));
675
+ const leaves = nodes
676
+ .filter((node) => !node.container && (outDegree.get(node.id) ?? 0) === 0)
677
+ .map((node) => node.id)
678
+ .sort()
679
+ .slice(0, 10);
660
680
  const errors = findings.filter((finding) => finding.severity === 'error').length;
661
681
  const warningCount = findings.filter((finding) => finding.severity === 'warning').length;
662
682
  const summary = {
@@ -672,6 +692,8 @@ export function reviewStructure(options) {
672
692
  errors,
673
693
  warnings: warningCount,
674
694
  rules: rules.length,
695
+ hubs,
696
+ leaves,
675
697
  };
676
698
  const nextSteps = [];
677
699
  if (errors > 0)
@@ -689,10 +711,13 @@ export function reviewStructure(options) {
689
711
  notation: graph.notation,
690
712
  graph,
691
713
  ...(options.matrix === undefined ? {} : { edgeVerdicts }),
714
+ ...(options.scan === undefined ? {} : { scan: options.scan }),
692
715
  findings,
693
716
  summary,
694
717
  hashes: {
695
- diagram: createHash('sha256').update(options.diagram).digest('hex'),
718
+ // A graph built from source has no diagram text to hash; hashing the text would hash the empty
719
+ // string, and every scan would then carry the same input hash.
720
+ diagram: createHash('sha256').update(options.graph === undefined ? options.diagram : canonicalJson({ nodes: options.graph.nodes.map((node) => node.id), edges: options.graph.edges.map((edge) => ({ from: edge.from, to: edge.to })) })).digest('hex'),
696
721
  ...(options.matrix === undefined ? {} : { matrix: createHash('sha256').update(canonicalJson(options.matrix)).digest('hex') }),
697
722
  },
698
723
  warnings,
package/lib/tool.js CHANGED
@@ -1,6 +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 { reportCallView, reportCard, reportResultView } from './presentation.js';
4
5
  import { OptionShapeError, optionRefusal, requireObjectOption } from './option-shape.js';
5
6
  import { oneOf } from './model-file.js';
6
7
  export const MODEL_VERIFY_TOOL_NAME = 'model_verify';
@@ -59,7 +60,13 @@ export const modelVerifyTool = defineTool({
59
60
  render(args, value) {
60
61
  return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }];
61
62
  },
63
+ // The card a UI shows, separated from the text the model reads.
64
+ presentationMeta(args, value) {
65
+ return reportCard(value, MODEL_VERIFY_TOOL_NAME);
66
+ },
62
67
  },
68
+ presentCall: reportCallView(MODEL_VERIFY_TOOL_NAME),
69
+ presentResult: reportResultView(MODEL_VERIFY_TOOL_NAME),
63
70
  timeoutMs: 10_000,
64
71
  isConcurrencySafe: () => true,
65
72
  async execute(args) {
@@ -0,0 +1,2 @@
1
+ export declare const MODEL_AUTOMATON_TOOL_NAME = "model_automaton_verify";
2
+ export declare const modelAutomatonTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
@@ -0,0 +1,57 @@
1
+ export interface AutomatonTransition {
2
+ from: string;
3
+ symbol: string;
4
+ to: string;
5
+ }
6
+ export interface Automaton {
7
+ states: string[];
8
+ alphabet: string[];
9
+ transitions: AutomatonTransition[];
10
+ init: string;
11
+ accepting: string[];
12
+ }
13
+ export interface AutomatonFinding {
14
+ code: string;
15
+ severity: 'error' | 'warning' | 'info';
16
+ message: string;
17
+ detail?: string;
18
+ }
19
+ /** Validate the shape, so a refusal says which field is wrong rather than throwing later. */
20
+ export declare function validateAutomaton(input: unknown): {
21
+ ok: true;
22
+ automaton: Automaton;
23
+ } | {
24
+ ok: false;
25
+ errors: string[];
26
+ };
27
+ export type AutomatonRelation = 'equivalent' | 'subset' | 'superset';
28
+ export interface AutomatonReport {
29
+ ok: boolean;
30
+ ran: boolean;
31
+ verdict: 'pass' | 'pass_with_findings' | 'fail';
32
+ verdictReason: string;
33
+ schema: string;
34
+ automaton: {
35
+ states: number;
36
+ transitions: number;
37
+ alphabet: number;
38
+ deterministic: boolean;
39
+ };
40
+ findings: AutomatonFinding[];
41
+ /** Set when the relation fails: the shortest word that breaks it. */
42
+ witness?: {
43
+ word: string[];
44
+ reason: string;
45
+ };
46
+ /** True when the product search stopped before deciding, so `pass` is not a proof of equality. */
47
+ truncated?: boolean;
48
+ hashes: {
49
+ automaton: string;
50
+ other?: string;
51
+ };
52
+ nextSteps: string[];
53
+ }
54
+ export declare function verifyAutomaton(input: unknown, options?: {
55
+ other?: unknown;
56
+ relation?: AutomatonRelation;
57
+ }): AutomatonReport;
@@ -0,0 +1,2 @@
1
+ export declare const MODEL_EDIT_TOOL_NAME = "model_edit";
2
+ export declare const modelEditTool: import("@deepseek-ai/dsh-tools").ToolDefinition;
@@ -0,0 +1,46 @@
1
+ type Json = Record<string, unknown>;
2
+ export interface EditOperation {
3
+ op: string;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface ModelChange {
7
+ kind: 'added' | 'removed' | 'updated';
8
+ what: string;
9
+ detail: string;
10
+ }
11
+ /** Read the model at `path`; a missing file is a refusal, not an empty model. */
12
+ export declare function readModelFile(path: unknown): {
13
+ model: Json;
14
+ full: string;
15
+ };
16
+ /**
17
+ * Accept the compact spelling of an operation as well as the long one.
18
+ *
19
+ * An operation is a small thing - add a state, wire a transition - and the long form spends more
20
+ * tokens on key names than on the change. Five operations cost 76 tokens spelled out and 39 in the
21
+ * compact form, and a batch of twenty is where that difference starts to matter.
22
+ *
23
+ * {addState: "c"} {addState: {id: "c", terminal: true}}
24
+ * {removeState: "c"}
25
+ * {addTransition: ["a", "reset", "c"]} {addTransition: {from, event, to, guard?, ...}}
26
+ * {removeTransition: ["a", "go"]} {updateTransition: ["a", "go", {guard: ...}]}
27
+ * {set: ["/init", "a"]} {remove: "/init"}
28
+ *
29
+ * The long form is unchanged and is what the errors talk about.
30
+ */
31
+ export declare function normalizeOperation(raw: unknown): EditOperation;
32
+ /** Apply one operation to a model copy, returning the changes it made. */
33
+ export declare function applyOperation(model: Json, operation: EditOperation): ModelChange[];
34
+ /** Validate the edited model and write it atomically. A refused edit leaves the file untouched. */
35
+ export interface EditOutcome {
36
+ model: Json;
37
+ changes: ModelChange[];
38
+ full: string;
39
+ /** Whether the model as written validates. False only when the caller allowed it. */
40
+ valid: boolean;
41
+ errors: string[];
42
+ }
43
+ export declare function applyEdits(path: unknown, operations: unknown, options?: {
44
+ allowInvalid?: boolean;
45
+ }): EditOutcome;
46
+ export {};
@@ -314,6 +314,7 @@ export declare const REPORT_SCHEMAS: {
314
314
  readonly granularity: 'model-everything/granularity/v1';
315
315
  readonly design: 'model-everything/design/v1';
316
316
  readonly baseline: 'model-everything/baseline/v1';
317
+ readonly edit: 'model-everything/edit/v1';
317
318
  };
318
319
  export type ReportSchema = typeof REPORT_SCHEMAS[keyof typeof REPORT_SCHEMAS];
319
320
  /**
@@ -0,0 +1,164 @@
1
+ /**
2
+ * What a UI shows for a report, separated from what the model reads.
3
+ *
4
+ * A report is written for a model: it names every check, every finding and the next step, because the
5
+ * model has to act on all of it. A reader wants four things instead - did it run, what does it decide,
6
+ * what is the first thing wrong, what to do next - and wants the rest folded away.
7
+ *
8
+ * Two host hooks carry that split:
9
+ *
10
+ * - `output.presentationMeta` projects a small JSON payload the host persists on the result; a client
11
+ * card narrows it without parsing prose.
12
+ * - `presentResult` returns the card body from that payload, so a UI that supports it shows the summary
13
+ * and a UI that does not falls back to the raw text. The model-facing content never changes.
14
+ *
15
+ * Both are pure and replayable: they read the persisted value, never the filesystem.
16
+ */
17
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm';
18
+ import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools';
19
+ /** One finding, as a card needs it: a code to name it and a sentence to read. */
20
+ export interface CardFinding {
21
+ code: string;
22
+ severity: string;
23
+ message: string;
24
+ }
25
+ /** The payload a client card narrows. Small on purpose: a card is a summary, not a second report. */
26
+ export interface ReportCard {
27
+ verdict?: string;
28
+ verdictReason?: string;
29
+ errors: number;
30
+ warnings: number;
31
+ checksRun?: number;
32
+ first: CardFinding | null;
33
+ /** The one step a reader should take, already a sentence. */
34
+ next: string | null;
35
+ /** Which family produced this, so a card can pick an icon and a colour without parsing codes. */
36
+ family: string;
37
+ /** What this family owns, and where the case just outside it goes. One table, not a second prose copy. */
38
+ route?: {
39
+ owns: string;
40
+ instead: string;
41
+ };
42
+ /** `model_edit`: the changes this call applied, as the report lists them. */
43
+ changes?: Array<{
44
+ kind: string;
45
+ what: string;
46
+ detail?: string;
47
+ }>;
48
+ /** `model_edit` and `report_diff`: how the findings moved, when either report could compare. */
49
+ delta?: {
50
+ added: number;
51
+ removed: number;
52
+ changed: number;
53
+ addedErrors: number;
54
+ };
55
+ /** `report_diff`: the first finding the change adds, so a card can lead with it. */
56
+ firstAdded?: CardFinding;
57
+ /** `report_diff`: the current report's own verdict, which differs from the delta's when a clean change lands on a failing report. */
58
+ currentVerdict?: string;
59
+ /** The artifact a translation or a rendering produced, as the first lines of it. */
60
+ source?: {
61
+ label: string;
62
+ lines: string[];
63
+ total: number;
64
+ };
65
+ /** `uml_structure_verify`: the graph's own counts, which are measurements rather than findings. */
66
+ graph?: {
67
+ nodes: number;
68
+ edges: number;
69
+ cycles: number;
70
+ isolated: number;
71
+ dangling: number;
72
+ };
73
+ /** `model_design_review`: three levels, because a signal is not a violation and a fact is neither. */
74
+ levels?: {
75
+ violations: number;
76
+ signals: number;
77
+ facts: number;
78
+ skipped: number;
79
+ };
80
+ /** `model_interleaving_scan`: how many claims were found, and in which families. */
81
+ claims?: {
82
+ absolute: number;
83
+ keywords: number;
84
+ families: Array<{
85
+ family: string;
86
+ count: number;
87
+ }>;
88
+ };
89
+ }
90
+ /**
91
+ * Project a report into the card payload.
92
+ *
93
+ * Read defensively: this runs on replay too, and a payload that once had a shape may come back with a
94
+ * field missing. A card that throws would take the whole session view with it.
95
+ */
96
+ export declare function reportCard(value: unknown, family: string): ReportCard;
97
+ /** The card body: four lines, in the order a reader asks the questions. */
98
+ export declare function cardBlocks(card: ReportCard): ContentBlock[];
99
+ /**
100
+ * The pending card for a report tool: a title naming the artifact, and the file to follow.
101
+ *
102
+ * From the arguments only, because a UI calls this during streaming and again on replay; the file has not
103
+ * been opened at that point and must not be.
104
+ */
105
+ /**
106
+ * The card payload with the artifact previewed.
107
+ *
108
+ * A translation and a rendering both produce text a reader wants to see the shape of, and neither should
109
+ * paste the whole file into a card: the preview is bounded, and it says how much it left out. The report
110
+ * keeps the artifact in full for whoever needs it.
111
+ */
112
+ export declare function cardWithSource(value: unknown, family: string, text: string | undefined, label: string, limit?: number): ReportCard;
113
+ export declare function reportCallView(family: string): (args: unknown) => ToolCallView | undefined;
114
+ /**
115
+ * The card for `model_edit`: what changed, and what the change did to the findings.
116
+ *
117
+ * Both lists are already in the report, so this is a rendering of the same value rather than a second
118
+ * computation - the rule every card here follows.
119
+ */
120
+ export declare function editCardBlocks(card: ReportCard): ContentBlock[];
121
+ /**
122
+ * The card for `report_diff`: does this change add anything, read as counts and the first added error.
123
+ *
124
+ * A diff's `verdict` is about the delta, so the card leads with the delta and never with the current
125
+ * report's own verdict - the two differ exactly when a clean change lands on an already-failing report.
126
+ */
127
+ export declare function diffCardBlocks(card: ReportCard): ContentBlock[];
128
+ /** `presentResult` for `model_edit`, falling back to the generic card when the payload has no changes. */
129
+ export declare function editResultView(family: string): (_args: unknown, result: {
130
+ content: ContentBlock[];
131
+ meta?: unknown;
132
+ }) => ToolResultView | undefined;
133
+ export declare function reportResultView(family: string): (_args: unknown, result: {
134
+ content: ContentBlock[];
135
+ meta?: unknown;
136
+ }) => ToolResultView | undefined;
137
+ /** The card for `uml_structure_verify`: the graph's shape first, then the first finding. */
138
+ export declare function structureCardBlocks(card: ReportCard): ContentBlock[];
139
+ /** The card for `model_design_review`: violations, then signals, then facts - never one number. */
140
+ export declare function designCardBlocks(card: ReportCard): ContentBlock[];
141
+ /** The card for `model_interleaving_scan`: claims by family, and the reminder that nothing was proven. */
142
+ export declare function scanCardBlocks(card: ReportCard): ContentBlock[];
143
+ /** One `presentResult` per card body, since the shapes differ but the fallback does not. */
144
+ export declare function familyResultView(family: string, body: (card: ReportCard) => ContentBlock[]): (_args: unknown, result: {
145
+ content: ContentBlock[];
146
+ meta?: unknown;
147
+ }) => ToolResultView | undefined;
148
+ export declare const structureResultView: (_args: unknown, result: {
149
+ content: ContentBlock[];
150
+ meta?: unknown;
151
+ }) => ToolResultView | undefined;
152
+ export declare const designResultView: (_args: unknown, result: {
153
+ content: ContentBlock[];
154
+ meta?: unknown;
155
+ }) => ToolResultView | undefined;
156
+ export declare const scanResultView: (_args: unknown, result: {
157
+ content: ContentBlock[];
158
+ meta?: unknown;
159
+ }) => ToolResultView | undefined;
160
+ /** `presentResult` for `report_diff`: the delta leads, never the current report's own verdict. */
161
+ export declare function diffResultView(family: string): (_args: unknown, result: {
162
+ content: ContentBlock[];
163
+ meta?: unknown;
164
+ }) => ToolResultView | undefined;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Which tool owns which question, in one table.
3
+ *
4
+ * Two consumers read this, and they used to read two different things: the result a model gets
5
+ * (`nextSteps` naming a sibling) and the toolset map a human reads (hand-written prose). That is how the
6
+ * map came to say "about ten thousand characters" while the descriptions measured twenty-nine: a fact
7
+ * stated twice drifts, and nothing fails when it does.
8
+ *
9
+ * So the table states the routing once. `routeFor` hands it to the card a UI draws; the toolset map
10
+ * generator reads the same table and refuses to write a map for a tool that is missing from it.
11
+ *
12
+ * What it is not: a decision. The tool a request should use is chosen by the model from the descriptions.
13
+ * This table says what each tool owns and which neighbour takes the case just outside that line, so a
14
+ * report can redirect without a reader having to remember the roster.
15
+ */
16
+ export interface Route {
17
+ /** What this tool decides, in the words its report already uses. */
18
+ owns: string;
19
+ /** The case just outside the line, and the tool that owns it. */
20
+ notFor: {
21
+ when: string;
22
+ tool: string;
23
+ };
24
+ }
25
+ export declare const ROUTES: Record<string, Route>;
26
+ /** The route for a tool, or undefined when the tool has none. */
27
+ export declare function routeFor(tool: string): Route | undefined;
28
+ /** Every tool a route names, for a caller that has to check the table against the registry. */
29
+ export declare function routedTools(): string[];
30
+ /** The tools named as the owner of an adjacent case. */
31
+ export declare function siblingTools(): string[];
@@ -0,0 +1,37 @@
1
+ export interface ScannedEdge {
2
+ from: string;
3
+ to: string;
4
+ }
5
+ export interface ScannedGraph {
6
+ /** Module ids, posix-style and relative to the scanned root. File ids for file languages, directory ids for package languages. */
7
+ nodes: string[];
8
+ edges: ScannedEdge[];
9
+ /** Files read, and files skipped because they are not source. */
10
+ filesRead: number;
11
+ /** Relative specifiers that pointed outside the root or at a file that does not exist. */
12
+ unresolved: string[];
13
+ /** Bare specifiers seen (a package name), counted once each. */
14
+ packages: string[];
15
+ /** Which languages were scanned, and at what granularity. */
16
+ languages: {
17
+ language: string;
18
+ files: number;
19
+ granularity: 'file' | 'package';
20
+ }[];
21
+ }
22
+ export type LangGranularity = 'file' | 'package';
23
+ export interface Language {
24
+ name: string;
25
+ extensions: string[];
26
+ granularity: LangGranularity;
27
+ /** Specifiers this file names, already separated from external ones. */
28
+ extract(text: string): {
29
+ local: string[];
30
+ external: string[];
31
+ };
32
+ }
33
+ /** Every language this scan understands, by extension. */
34
+ export declare const LANGUAGES: Language[];
35
+ /** Which language a file belongs to, if any. */
36
+ export declare function languageOf(path: string): Language | undefined;
37
+ export declare function scanDependencyGraph(root: unknown): ScannedGraph;
@@ -95,6 +95,13 @@ export interface StructureSummary {
95
95
  errors: number;
96
96
  warnings: number;
97
97
  rules: number;
98
+ /** The most depended-on nodes, by incoming edges. What a reader asks first and counts worst. */
99
+ hubs: {
100
+ id: string;
101
+ dependents: number;
102
+ }[];
103
+ /** Modules nothing depends on (no outgoing edges), capped for the report. */
104
+ leaves: string[];
98
105
  }
99
106
  export interface StructureReport {
100
107
  /** The tool ran and produced this report; read `verdict` for the outcome. */
@@ -142,6 +149,15 @@ export declare function shortestCycle(nodes: string[], edges: StructureEdge[]):
142
149
  export interface StructureReviewOptions {
143
150
  /** Diagram text to review. */
144
151
  diagram: string;
152
+ /** A graph built from source, instead of parsed from a diagram. `diagram` is then ignored. */
153
+ graph?: StructureGraph;
154
+ /** How the graph was produced, when it came from a scan rather than a drawing. */
155
+ scan?: {
156
+ files: number;
157
+ unresolved: number;
158
+ packages: number;
159
+ languages: string[];
160
+ };
145
161
  notation?: 'auto' | StructureNotation;
146
162
  /** Dependency matrix: allowed/denied edges, layers, required edges. */
147
163
  matrix?: unknown;
package/lib/uml-tool.js CHANGED
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, valueHash, refusalVerdict, verdictOf, verdictOfFindings } from './engine.js';
4
4
  import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, checkUmlIntent, UmlError, UML_DIAGRAMS, } from './uml.js';
5
5
  import { renderReportText } from './report-view.js';
6
+ import { cardWithSource, reportCallView, reportCard, reportResultView } from './presentation.js';
6
7
  /**
7
8
  * The UML front end as four tools, one per job.
8
9
  *
@@ -41,13 +42,19 @@ const intentParameter = {
41
42
  enum: ['present', 'verify', 'fidelity'],
42
43
  description: 'What the artifact is for, declared up front: present = a human will read it; verify = it must be readable as a model by the verification engine; fidelity = the render-and-reparse proof is the point. A view that cannot serve the declared intent is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes.',
43
44
  };
44
- const reportOutput = {
45
+ const reportOutput = (family) => ({
45
46
  schema: { type: 'json', description: 'The result: diagram source, a family model, or a modelling review report with findings, round-trip fidelity and next steps.' },
46
47
  render(args, value) {
47
48
  // A rendered diagram is source text: the body prints it verbatim, once.
48
49
  return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
49
50
  },
50
- };
51
+ // The card a UI shows, separated from the text the model reads. The family is the tool's own name, so
52
+ // one shared output does not have to guess which tool is calling it.
53
+ presentationMeta(_args, value) {
54
+ const report = value;
55
+ return cardWithSource(value, family, typeof report.diagram === 'string' ? report.diagram : undefined, 'diagram source');
56
+ },
57
+ });
51
58
  /** Draw a model as diagram source. */
52
59
  export const umlRenderTool = defineTool({
53
60
  name: UML_RENDER_TOOL_NAME,
@@ -60,7 +67,9 @@ export const umlRenderTool = defineTool({
60
67
  intent: intentParameter,
61
68
  reportView: reportViewParameter,
62
69
  },
63
- output: reportOutput,
70
+ output: reportOutput(UML_RENDER_TOOL_NAME),
71
+ presentCall: reportCallView(UML_RENDER_TOOL_NAME),
72
+ presentResult: reportResultView(UML_RENDER_TOOL_NAME),
64
73
  timeoutMs: 10_000,
65
74
  isConcurrencySafe: () => true,
66
75
  async execute(args) {
@@ -97,7 +106,9 @@ export const umlParseTool = defineTool({
97
106
  intent: intentParameter,
98
107
  reportView: reportViewParameter,
99
108
  },
100
- output: reportOutput,
109
+ output: reportOutput(UML_PARSE_TOOL_NAME),
110
+ presentCall: reportCallView(UML_PARSE_TOOL_NAME),
111
+ presentResult: reportResultView(UML_PARSE_TOOL_NAME),
101
112
  timeoutMs: 10_000,
102
113
  isConcurrencySafe: () => true,
103
114
  async execute(args) {
@@ -148,7 +159,9 @@ export const umlReviewTool = defineTool({
148
159
  intent: intentParameter,
149
160
  reportView: reportViewParameter,
150
161
  },
151
- output: reportOutput,
162
+ output: reportOutput(UML_REVIEW_TOOL_NAME),
163
+ presentCall: reportCallView(UML_REVIEW_TOOL_NAME),
164
+ presentResult: reportResultView(UML_REVIEW_TOOL_NAME),
152
165
  timeoutMs: 10_000,
153
166
  isConcurrencySafe: () => true,
154
167
  async execute(args) {
@@ -179,7 +192,9 @@ export const umlExplainLabelsTool = defineTool({
179
192
  notation: notationParameter,
180
193
  reportView: reportViewParameter,
181
194
  },
182
- output: reportOutput,
195
+ output: reportOutput(UML_LABELS_TOOL_NAME),
196
+ presentCall: reportCallView(UML_LABELS_TOOL_NAME),
197
+ presentResult: reportResultView(UML_LABELS_TOOL_NAME),
183
198
  timeoutMs: 10_000,
184
199
  isConcurrencySafe: () => true,
185
200
  async execute(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-everything",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",
@@ -64,8 +64,11 @@
64
64
  "typecheck": "tsc -p tsconfig.json --noEmit",
65
65
  "lint:md": "markdownlint --dot --ignore \"**/node_modules/**\" .",
66
66
  "bump": "node scripts/bump-version.mjs",
67
- "test:engine": "npm run build && node tests/engine/run.mjs && node tests/design-engine/run.mjs && node tests/catalog/run.mjs && node tests/data-engine/run.mjs && node tests/concurrency/run.mjs && node tests/uml/run.mjs && node tests/apply-smoke.mjs && node tests/memory/run.mjs && node tests/skills/run.mjs && node tests/gate/run.mjs && node tests/skill-routing/run.mjs && node tests/toolset/run.mjs && node tests/principles/run.mjs && node tests/feature-checklist/run.mjs && node tests/usage/run.mjs && node tests/translation/run.mjs && node tests/documentation/run.mjs && node tests/contributing/run.mjs && node tests/routing-map/run.mjs && node tests/report-reading/run.mjs && node tests/structure/run.mjs && node tests/baseline/run.mjs && node tests/granularity/run.mjs && node tests/exporters/run.mjs && node tests/report-view/run.mjs && node tests/contracts/run.mjs && node tests/tool-boundary/run.mjs && node tests/negatives/run.mjs && node tests/external/run.mjs",
68
- "test:full": "npm run build && node tests/full-suite.mjs"
67
+ "test:cards": "node tests/presentation/run.mjs && node tests/automaton/run.mjs && node tests/routing/run.mjs",
68
+ "coverage": "c8 --reporter=text --exclude-after-remap=false npm run test:engine",
69
+ "test:engine": "npm run build && node tests/engine/run.mjs && node tests/design-engine/run.mjs && node tests/catalog/run.mjs && node tests/data-engine/run.mjs && node tests/concurrency/run.mjs && node tests/uml/run.mjs && node tests/apply-smoke.mjs && node tests/memory/run.mjs && node tests/skills/run.mjs && node tests/gate/run.mjs && node tests/skill-routing/run.mjs && node tests/toolset/run.mjs && node tests/principles/run.mjs && node tests/feature-checklist/run.mjs && node tests/usage/run.mjs && node tests/translation/run.mjs && node tests/documentation/run.mjs && node tests/contributing/run.mjs && node tests/routing-map/run.mjs && node tests/report-reading/run.mjs && node tests/structure/run.mjs && node tests/automaton/run.mjs && node tests/presentation/run.mjs && node tests/routing/run.mjs && node tests/tool-routing/run.mjs && node tests/tools/run.mjs && node tests/baseline/run.mjs && node tests/granularity/run.mjs && node tests/exporters/run.mjs && node tests/report-view/run.mjs && node tests/contracts/run.mjs && node tests/tool-boundary/run.mjs && node tests/negatives/run.mjs && node tests/external/run.mjs",
70
+ "test:full": "npm run build && node tests/full-suite.mjs",
71
+ "check": "npm run lint:md && npm run test:engine"
69
72
  },
70
73
  "peerDependencies": {
71
74
  "@deepseek-ai/cordis": "^4.0.4",
@@ -9,7 +9,11 @@ For an unverified design, failure is only a matter of time. Complex code becomes
9
9
 
10
10
  ## When This Skill Applies
11
11
 
12
- **This is the entry point.** Load this skill whenever you need to check whether something about the code is true, however small the claim looks. It owns the doctrine: enumerate the claims, verify each against the code, cite `file:line`, read `verdict`. It also owns the executable verification for behaviour. A neighbouring domain goes to its own skill through the routing table below.
12
+ **This is the entry point.** Load this skill when a claim about the code has to be settled and a reading will not settle it. It owns the doctrine: enumerate the claims, verify each against the code, cite `file:line`, read `verdict`. It also owns the executable verification for behaviour. A neighbouring domain goes to its own skill through the routing table below.
13
+
14
+ **It decides small models, not programs.** The engine enumerates states, so a model that explodes is reported `truncated` with the undecided check named. That is not a bug to retry with a larger `maxStates`: reduce the model first (drop variables the property does not read, collapse symmetric states), and if it still explodes the property needs a tool that does not enumerate - SPIN for partial-order reduction, NuSMV for symbolic search, CBMC for bounded SAT. This plugin builds no abstraction and reads no program source.
15
+
16
+ **This costs more than reading, and less the larger the task.** On a fixture of a few hundred lines it costs several times a plain reading, because the fixed cost has nothing to amortise against. On a tree of tens of thousands of lines the ratio falls to roughly parity. Spend it where the object is large enough for one decision to replace many reads. Reach for it when the claim is about all paths rather than one, when nothing in the repository will go red if it is false, or when the verdict has to be reproducible later. A claim one reading settles belongs in a sentence, not in a model.
13
17
 
14
18
  | The task involves… | What it does |
15
19
  |---|---|
@@ -180,7 +184,7 @@ not a bound on the search.
180
184
 
181
185
  ### Native dsh Tools
182
186
 
183
- Twelve tools: `model_verify`, `model_compose_verify`, `model_datamodel_verify`, `model_design_review`, `model_interleaving_scan`, `model_export`, `report_diff`, `uml_render`, `uml_parse`, `uml_review`, `uml_explain_labels`, `uml_structure_verify`. Each tool's description states what it decides and what it does not. Read the description before calling it, and read the report's `verdict` afterwards.
187
+ The tools are `model_verify`, `model_edit`, `model_compose_verify`, `model_datamodel_verify`, `model_design_review`, `model_interleaving_scan`, `model_export`, `report_diff`, `uml_render`, `uml_parse`, `uml_review`, `uml_explain_labels`, `uml_structure_verify` and `model_automaton_verify`. `model_automaton_verify` decides a finite-word language and its relation to another automaton, returning the shortest word that separates them; `references/automata.md` states what each of its checks decides, on what authority, and where it stops. `model_edit` changes a model file by naming the change and returns the structural diff plus the difference in findings, so a fix round costs one call instead of three. `uml_structure_verify` builds its graph from a source tree when given `repoPath`, reading each language's own import construct, so a dependency question about code does not start with drawing. Each tool's description states what it decides and what it does not. Read the description before calling it, and read the report's `verdict` afterwards.
184
188
 
185
189
  A model is the largest argument these tools take. When it is long, or you have edited it more than once, write it to a file and pass `modelPath` instead of inlining it: a streamed argument that long is where invalid JSON comes from, and the file accepts any formatting.
186
190