dsh-model-everything 0.2.0 → 0.3.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 +81 -73
- package/README.md +67 -68
- package/lib/automaton-tool.js +69 -0
- package/lib/automaton.js +327 -0
- package/lib/compose-tool.js +6 -1
- package/lib/concurrency-tool.js +6 -1
- package/lib/data-tool.js +6 -1
- package/lib/design-tool.js +7 -2
- package/lib/edit-tool.js +111 -0
- package/lib/editor.js +293 -0
- package/lib/engine.js +7 -0
- package/lib/export-tool.js +6 -1
- package/lib/index.js +4 -0
- package/lib/presentation.js +81 -0
- package/lib/report-diff-tool.js +6 -1
- package/lib/report-view.js +62 -10
- package/lib/routing.js +85 -0
- package/lib/scan.js +547 -0
- package/lib/structure-tool.js +34 -1
- package/lib/structure.js +28 -3
- package/lib/tool.js +6 -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 +59 -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 +16 -6
- package/package.json +4 -3
- package/skills/model-everything/SKILL.md +6 -2
- package/skills/model-everything/references/automata.md +102 -0
- package/src/automaton-tool.ts +73 -0
- package/src/automaton.ts +375 -0
- package/src/compose-tool.ts +12 -6
- package/src/concurrency-tool.ts +7 -1
- package/src/data-tool.ts +7 -1
- package/src/design-tool.ts +74 -68
- package/src/edit-tool.ts +119 -0
- package/src/editor.ts +297 -0
- package/src/engine.ts +7 -0
- package/src/export-tool.ts +87 -81
- package/src/index.ts +4 -0
- package/src/presentation.ts +125 -0
- package/src/report-diff-tool.ts +7 -1
- package/src/report-view.ts +53 -2
- package/src/routing.ts +96 -0
- package/src/scan.ts +564 -0
- package/src/structure-tool.ts +33 -1
- package/src/structure.ts +37 -3
- package/src/tool.ts +6 -0
- package/src/uml-tool.ts +240 -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 { 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,12 @@ 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
|
+
presentResult: reportResultView(MODEL_VERIFY_TOOL_NAME),
|
|
63
69
|
timeoutMs: 10_000,
|
|
64
70
|
isConcurrencySafe: () => true,
|
|
65
71
|
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,59 @@
|
|
|
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 { 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
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Project a report into the card payload.
|
|
45
|
+
*
|
|
46
|
+
* Read defensively: this runs on replay too, and a payload that once had a shape may come back with a
|
|
47
|
+
* field missing. A card that throws would take the whole session view with it.
|
|
48
|
+
*/
|
|
49
|
+
export declare function reportCard(value: unknown, family: string): ReportCard;
|
|
50
|
+
/** The card body: four lines, in the order a reader asks the questions. */
|
|
51
|
+
export declare function cardBlocks(card: ReportCard): ContentBlock[];
|
|
52
|
+
/**
|
|
53
|
+
* `presentResult` for a report tool: render the card from the persisted payload, and fall back to the
|
|
54
|
+
* raw content by returning undefined when there is none.
|
|
55
|
+
*/
|
|
56
|
+
export declare function reportResultView(family: string): (_args: unknown, result: {
|
|
57
|
+
content: ContentBlock[];
|
|
58
|
+
meta?: unknown;
|
|
59
|
+
}) => 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 { reportCard, reportResultView } from './presentation.js';
|
|
6
7
|
/**
|
|
7
8
|
* The UML front end as four tools, one per job.
|
|
8
9
|
*
|
|
@@ -41,13 +42,18 @@ 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
|
+
return reportCard(value, family);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
51
57
|
/** Draw a model as diagram source. */
|
|
52
58
|
export const umlRenderTool = defineTool({
|
|
53
59
|
name: UML_RENDER_TOOL_NAME,
|
|
@@ -60,7 +66,8 @@ export const umlRenderTool = defineTool({
|
|
|
60
66
|
intent: intentParameter,
|
|
61
67
|
reportView: reportViewParameter,
|
|
62
68
|
},
|
|
63
|
-
output: reportOutput,
|
|
69
|
+
output: reportOutput(UML_RENDER_TOOL_NAME),
|
|
70
|
+
presentResult: reportResultView(UML_RENDER_TOOL_NAME),
|
|
64
71
|
timeoutMs: 10_000,
|
|
65
72
|
isConcurrencySafe: () => true,
|
|
66
73
|
async execute(args) {
|
|
@@ -97,7 +104,8 @@ export const umlParseTool = defineTool({
|
|
|
97
104
|
intent: intentParameter,
|
|
98
105
|
reportView: reportViewParameter,
|
|
99
106
|
},
|
|
100
|
-
output: reportOutput,
|
|
107
|
+
output: reportOutput(UML_PARSE_TOOL_NAME),
|
|
108
|
+
presentResult: reportResultView(UML_PARSE_TOOL_NAME),
|
|
101
109
|
timeoutMs: 10_000,
|
|
102
110
|
isConcurrencySafe: () => true,
|
|
103
111
|
async execute(args) {
|
|
@@ -148,7 +156,8 @@ export const umlReviewTool = defineTool({
|
|
|
148
156
|
intent: intentParameter,
|
|
149
157
|
reportView: reportViewParameter,
|
|
150
158
|
},
|
|
151
|
-
output: reportOutput,
|
|
159
|
+
output: reportOutput(UML_REVIEW_TOOL_NAME),
|
|
160
|
+
presentResult: reportResultView(UML_REVIEW_TOOL_NAME),
|
|
152
161
|
timeoutMs: 10_000,
|
|
153
162
|
isConcurrencySafe: () => true,
|
|
154
163
|
async execute(args) {
|
|
@@ -179,7 +188,8 @@ export const umlExplainLabelsTool = defineTool({
|
|
|
179
188
|
notation: notationParameter,
|
|
180
189
|
reportView: reportViewParameter,
|
|
181
190
|
},
|
|
182
|
-
output: reportOutput,
|
|
191
|
+
output: reportOutput(UML_LABELS_TOOL_NAME),
|
|
192
|
+
presentResult: reportResultView(UML_LABELS_TOOL_NAME),
|
|
183
193
|
timeoutMs: 10_000,
|
|
184
194
|
isConcurrencySafe: () => true,
|
|
185
195
|
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.3.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,9 @@
|
|
|
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: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",
|
|
68
|
+
"test:full": "npm run build && node tests/full-suite.mjs",
|
|
69
|
+
"check": "npm run lint:md && npm run test:engine"
|
|
69
70
|
},
|
|
70
71
|
"peerDependencies": {
|
|
71
72
|
"@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
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Automata as a verification target
|
|
2
|
+
|
|
3
|
+
What `model_automaton_verify` decides, why each decision is a theorem rather than a heuristic, and where
|
|
4
|
+
the boundary sits. Read it when a report has to be defended, or when a question lands on the edge:
|
|
5
|
+
liveness, infinite words, or a product that will not finish.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
Finite words over an explicit alphabet. A run starts at `init`, reads one symbol per transition, and is
|
|
10
|
+
accepted if it ends in `accepting`.
|
|
11
|
+
|
|
12
|
+
Not in scope, and the report says so:
|
|
13
|
+
|
|
14
|
+
- liveness, fairness, "eventually" properties on an infinite run;
|
|
15
|
+
- timing, clocks, deadlines;
|
|
16
|
+
- conformance by observation rather than by language;
|
|
17
|
+
- any abstraction. The tool reads a model, never program source.
|
|
18
|
+
|
|
19
|
+
## What each check decides, and on what authority
|
|
20
|
+
|
|
21
|
+
| Check | Decision | Authority |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `AUT003` nondeterminism | whether some state has two transitions on one symbol | the definition of a deterministic automaton |
|
|
24
|
+
| `AUT001` unreachable | whether a state is reachable from `init` | reachability is decidable in linear space |
|
|
25
|
+
| `AUT002` trap | whether a reachable state can still reach acceptance | the same reachability, run backwards |
|
|
26
|
+
| `AUT004` empty language | whether any accepting state is reachable | emptiness of a finite automaton is decidable |
|
|
27
|
+
| `AUT005` alphabet mismatch | whether the two machines can be compared at all | a language relation needs one alphabet |
|
|
28
|
+
| `AUT006` equivalence | whether two machines accept the same words | decide `L(A) \ L(B)` and `L(B) \ L(A)` |
|
|
29
|
+
| `AUT007` subset | whether every word of the first is a word of the second | decide `L(A) \ L(B)` |
|
|
30
|
+
| `AUT008` superset | the same, with the sides exchanged | decide `L(B) \ L(A)` |
|
|
31
|
+
| `AUT009` undecided | the product search stopped at its cap | a stopped search proves nothing |
|
|
32
|
+
|
|
33
|
+
Equivalence and inclusion are decided by exploring the product of the two automata and asking whether an
|
|
34
|
+
accepting side pairs with a rejecting one. A breadth-first walk returns the **shortest** word that does,
|
|
35
|
+
so the witness is the smallest counterexample there is, not merely one that exists.
|
|
36
|
+
|
|
37
|
+
## The wall, stated plainly
|
|
38
|
+
|
|
39
|
+
These are decidable problems, not approximations: automaton equivalence is NL-complete, inclusion for
|
|
40
|
+
nondeterministic machines and LTL model checking are PSPACE-complete. The cost is what grows, not the
|
|
41
|
+
certainty. Two machines of `n` states each can produce `n × n` pairs, and a model with counters or
|
|
42
|
+
queues in it can be unbounded before anyone writes a single transition.
|
|
43
|
+
|
|
44
|
+
So a cap is honest and a larger cap is not a strategy. When the product stops, the report carries
|
|
45
|
+
`AUT009_UNDECIDED` and sets `truncated: true`: the verdict is "not known", never "equal". If the same
|
|
46
|
+
happens to a state machine in `model_verify`, the report names the escalation instead of suggesting a
|
|
47
|
+
bigger budget.
|
|
48
|
+
|
|
49
|
+
## Reading, by what each one settles
|
|
50
|
+
|
|
51
|
+
Foundations, for the constructions above:
|
|
52
|
+
|
|
53
|
+
- Hopcroft, Motwani, Ullman, *Introduction to Automata Theory, Languages, and Computation* — subset
|
|
54
|
+
construction, product automata, the equivalence test.
|
|
55
|
+
- Sipser, *Introduction to the Theory of Computation* — closure properties and decidability, in the
|
|
56
|
+
gentler telling.
|
|
57
|
+
|
|
58
|
+
Model checking, which is where liveness lives and this family stops:
|
|
59
|
+
|
|
60
|
+
- Baier and Katoen, *Principles of Model Checking* — LTL, CTL, Büchi automata, fairness. The boundary in
|
|
61
|
+
the scope section above is this book's first half.
|
|
62
|
+
- Clarke, Grumberg, Peled, *Model Checking* — the classical account, including why explicit enumeration
|
|
63
|
+
runs out.
|
|
64
|
+
|
|
65
|
+
The automata-theoretic framing this tool is a finite-word shadow of:
|
|
66
|
+
|
|
67
|
+
- Kurshan, *Computer-Aided Verification of Coordinating Processes* — systems as automata, properties as
|
|
68
|
+
automata, language inclusion as the decision procedure.
|
|
69
|
+
- Vardi and Wolper, *An automata-theoretic approach to automatic program verification* — the same idea
|
|
70
|
+
for LTL, with Büchi acceptance.
|
|
71
|
+
|
|
72
|
+
Conformance, for what "implementation matches specification" means when it is made precise:
|
|
73
|
+
|
|
74
|
+
- Tretmans, *Model Based Testing with Labelled Transition Systems* — `ioco` and the family of
|
|
75
|
+
implementation relations; `subset` and `superset` are the language-level versions of that choice.
|
|
76
|
+
- Lynch and Vaandrager, *Forward and backward simulations* — refinement in two directions, which is
|
|
77
|
+
exactly the difference between asking `subset` and asking `superset`.
|
|
78
|
+
|
|
79
|
+
## What a pass does not mean
|
|
80
|
+
|
|
81
|
+
A passing report says the stated words are accepted or rejected as claimed. It does not say the model is
|
|
82
|
+
the system: the alphabet is whatever the modeller wrote, and a property that needs a symbol nobody
|
|
83
|
+
declared cannot be seen. State the alphabet next to the claim, or the verdict is about a machine that
|
|
84
|
+
exists only in the report.
|
|
85
|
+
|
|
86
|
+
## Provenance
|
|
87
|
+
|
|
88
|
+
Which parts are borrowed, and which are this repository's own decisions. A check that came from a
|
|
89
|
+
textbook is cited so a reader can check the argument; a choice that is not in any book is marked as one.
|
|
90
|
+
|
|
91
|
+
| Part | Where it comes from |
|
|
92
|
+
|---|---|
|
|
93
|
+
| subset construction for determinisation | Hopcroft, Motwani and Ullman |
|
|
94
|
+
| deciding equivalence and inclusion on the product | Hopcroft, Motwani and Ullman; Kurshan for the framing |
|
|
95
|
+
| the shortest-witness search | breadth-first search on the product; the choice to return the shortest word is this repository's |
|
|
96
|
+
| the check codes and their grouping | this repository's convention, shared with the other families |
|
|
97
|
+
| `truncated` and the `_UNDECIDED` suffix | this repository's convention: a stopped search is not a pass |
|
|
98
|
+
| the cap on the product | this repository's decision, and a practical one, not a theoretical boundary |
|
|
99
|
+
| `subset` and `superset` as the two refinements | Tretmans; Lynch and Vaandrager |
|
|
100
|
+
|
|
101
|
+
The theory above decides the checks. Nothing in this family is a heuristic dressed as a decision: every
|
|
102
|
+
code either proves a property or says it could not.
|