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.
Files changed (54) hide show
  1. package/README.en-US.md +81 -73
  2. package/README.md +67 -68
  3. package/lib/automaton-tool.js +69 -0
  4. package/lib/automaton.js +327 -0
  5. package/lib/compose-tool.js +6 -1
  6. package/lib/concurrency-tool.js +6 -1
  7. package/lib/data-tool.js +6 -1
  8. package/lib/design-tool.js +7 -2
  9. package/lib/edit-tool.js +111 -0
  10. package/lib/editor.js +293 -0
  11. package/lib/engine.js +7 -0
  12. package/lib/export-tool.js +6 -1
  13. package/lib/index.js +4 -0
  14. package/lib/presentation.js +81 -0
  15. package/lib/report-diff-tool.js +6 -1
  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 +34 -1
  20. package/lib/structure.js +28 -3
  21. package/lib/tool.js +6 -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 +59 -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 +16 -6
  32. package/package.json +4 -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 +73 -0
  36. package/src/automaton.ts +375 -0
  37. package/src/compose-tool.ts +12 -6
  38. package/src/concurrency-tool.ts +7 -1
  39. package/src/data-tool.ts +7 -1
  40. package/src/design-tool.ts +74 -68
  41. package/src/edit-tool.ts +119 -0
  42. package/src/editor.ts +297 -0
  43. package/src/engine.ts +7 -0
  44. package/src/export-tool.ts +87 -81
  45. package/src/index.ts +4 -0
  46. package/src/presentation.ts +125 -0
  47. package/src/report-diff-tool.ts +7 -1
  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 +33 -1
  52. package/src/structure.ts +37 -3
  53. package/src/tool.ts +6 -0
  54. 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
- 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 { 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,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,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;
@@ -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.2.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 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
 
@@ -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.