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/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
|
-
|
|
448
|
-
|
|
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
|
|
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,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,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 {};
|
package/lib/types/engine.d.ts
CHANGED
|
@@ -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;
|
package/lib/types/structure.d.ts
CHANGED
|
@@ -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.
|
|
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:
|
|
68
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
|