dsh-logicprobe 0.9.0 → 1.0.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 +79 -14
- package/README.md +79 -14
- package/lib/baseline.js +142 -0
- package/lib/client.js +56 -1
- package/lib/compose-tool.js +1 -1
- package/lib/concurrency.js +14 -0
- package/lib/data-engine.js +42 -8
- package/lib/engine.js +379 -79
- package/lib/export-tool.js +27 -2
- package/lib/granularity.js +237 -0
- package/lib/index.js +63 -41
- package/lib/report-diff-tool.js +39 -0
- package/lib/structure-tool.js +64 -0
- package/lib/structure.js +648 -0
- package/lib/tool.js +3 -3
- package/lib/types/baseline.d.ts +69 -0
- package/lib/types/concurrency.d.ts +10 -0
- package/lib/types/data-engine.d.ts +13 -1
- package/lib/types/engine.d.ts +138 -1
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +9 -8
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +156 -0
- package/lib/types/tool.d.ts +2 -2
- package/lib/types/uml.d.ts +89 -0
- package/lib/uml-tool.js +36 -7
- package/lib/uml.js +217 -23
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +16 -14
- package/skills/logicprobe/SKILL.md +84 -58
- package/skills/logicprobe/references/dsh-model-schema.md +81 -10
- package/skills/logicprobe/references/gap-routing-guide.md +37 -36
- package/skills/logicprobe/references/hash-spec.md +128 -0
- package/skills/logicprobe/references/logic-verification-guide.md +27 -250
- package/skills/logicprobe-concurrency/SKILL.md +43 -0
- package/skills/{logicprobe → logicprobe-concurrency}/references/concurrency-risk-guide.md +1 -1
- package/skills/logicprobe-datamodel/SKILL.md +5 -4
- package/skills/logicprobe-datamodel/references/data-model-guide.md +1 -1
- package/skills/logicprobe-datamodel/references/data-model-schema.md +1 -1
- package/skills/logicprobe-structure/SKILL.md +95 -0
- package/skills/logicprobe-structure/references/structure-review-guide.md +177 -0
- package/skills/logicprobe-uml/SKILL.md +55 -0
- package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +120 -8
- package/src/baseline.ts +180 -0
- package/src/client.js +56 -1
- package/src/compose-tool.ts +47 -47
- package/src/concurrency.ts +23 -0
- package/src/data-engine.ts +51 -9
- package/src/engine.ts +441 -67
- package/src/export-tool.ts +73 -48
- package/src/granularity.ts +312 -0
- package/src/index.ts +63 -41
- package/src/report-diff-tool.ts +43 -0
- package/src/structure-tool.ts +68 -0
- package/src/structure.ts +724 -0
- package/src/tool.ts +3 -3
- package/src/uml-tool.ts +38 -8
- package/src/uml.ts +275 -24
- package/skills/logicprobe/references/logicprobe-engine.py +0 -4943
- package/skills/logicprobe/references/verification-harness.py +0 -959
- package/skills/logicprobe-datamodel/references/data-model-harness.py +0 -528
package/lib/uml.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*
|
|
31
31
|
* @module logicprobe-uml
|
|
32
32
|
*/
|
|
33
|
-
import { validateModel, modelHash } from './engine.js';
|
|
33
|
+
import { REPORT_SCHEMAS, validateModel, modelHash, metadataKeysOf, narrativeComplete, narrativeCoverageOf, verdictOfFindings, DEFAULT_HASH_SPEC } from './engine.js';
|
|
34
34
|
export const UML_NOTATIONS = ['mermaid', 'plantuml'];
|
|
35
35
|
export const UML_DIAGRAMS = ['state', 'activity', 'sequence'];
|
|
36
36
|
/** Marker every generated diagram carries; renderers ignore it, the parser uses it. */
|
|
@@ -41,7 +41,16 @@ const UNSAFE_LABEL = /[[\]/\n\r\t]/;
|
|
|
41
41
|
const RESERVED_NODE_IDS = new Set(['end', 'graph', 'subgraph', 'class', 'classDef', 'click', 'style', 'linkStyle', 'direction']);
|
|
42
42
|
/** Ids the notation can spell without an alias. */
|
|
43
43
|
const PLAIN_ID = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
44
|
+
/**
|
|
45
|
+
* A UML front-end refusal. `code` lets the tool report *why* it refused instead of
|
|
46
|
+
* collapsing every refusal into a generic input error.
|
|
47
|
+
*/
|
|
44
48
|
export class UmlError extends Error {
|
|
49
|
+
code;
|
|
50
|
+
constructor(message, code = 'UML_INPUT') {
|
|
51
|
+
super(message);
|
|
52
|
+
this.code = code;
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
// ---------------------------------------------------------------------------
|
|
47
56
|
// Shared helpers
|
|
@@ -538,11 +547,36 @@ function parseTransitionLabel(label, warnings) {
|
|
|
538
547
|
throw new UmlError('transition label "' + label + '" carries no event name; label the arrow as `event [guard] / actions`');
|
|
539
548
|
return { event, ...(guard === undefined ? {} : { guard }), ...(updates === undefined ? {} : { updates }) };
|
|
540
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Mermaid diagram-family headers that are neither a state machine nor a flow.
|
|
552
|
+
* Recognizing them matters twice: the text must not be reported as "cannot tell
|
|
553
|
+
* whether this is Mermaid or PlantUML", and it must not be parsed as a state
|
|
554
|
+
* diagram — every line of it would be ignored or misread, and the empty model that
|
|
555
|
+
* comes out would be presented as if it described the file.
|
|
556
|
+
*/
|
|
557
|
+
const MERMAID_OTHER_FAMILIES = [
|
|
558
|
+
'classDiagram', 'erDiagram', 'gantt', 'pie', 'journey', 'mindmap', 'gitGraph',
|
|
559
|
+
'C4Context', 'C4Container', 'C4Component', 'C4Dynamic', 'C4Deployment',
|
|
560
|
+
'requirementDiagram', 'timeline', 'quadrantChart', 'sankey-beta', 'block-beta',
|
|
561
|
+
'packet-beta', 'architecture-beta', 'radar-beta', 'treemap-beta', 'xychart-beta',
|
|
562
|
+
];
|
|
563
|
+
/** The other-family header this text declares, if any. */
|
|
564
|
+
function otherMermaidFamily(text) {
|
|
565
|
+
for (const family of MERMAID_OTHER_FAMILIES) {
|
|
566
|
+
if (new RegExp('^\\s*' + family + '\\b', 'm').test(text))
|
|
567
|
+
return family;
|
|
568
|
+
}
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
541
571
|
function detectNotation(text) {
|
|
542
572
|
if (/^\s*@start/m.test(text))
|
|
543
573
|
return 'plantuml';
|
|
544
574
|
if (/^\s*(stateDiagram|stateDiagram-v2|flowchart|graph|sequenceDiagram)\b/m.test(text))
|
|
545
575
|
return 'mermaid';
|
|
576
|
+
// A known Mermaid family is still Mermaid: say so, and let detectDiagram refuse it
|
|
577
|
+
// by name instead of blaming the notation.
|
|
578
|
+
if (otherMermaidFamily(text) !== undefined)
|
|
579
|
+
return 'mermaid';
|
|
546
580
|
throw new UmlError('cannot tell whether this is Mermaid or PlantUML text: expected `stateDiagram-v2` / `flowchart` / `sequenceDiagram`, or `@startuml`');
|
|
547
581
|
}
|
|
548
582
|
function detectDiagram(text) {
|
|
@@ -552,6 +586,10 @@ function detectDiagram(text) {
|
|
|
552
586
|
return 'activity';
|
|
553
587
|
if (/^\s*sequenceDiagram\b/m.test(text))
|
|
554
588
|
return 'sequence';
|
|
589
|
+
const family = otherMermaidFamily(text);
|
|
590
|
+
if (family !== undefined) {
|
|
591
|
+
throw new UmlError('`' + family + '` is not a state or activity diagram: logicprobe models state machines and flows, so this diagram family cannot be parsed into a LogicModelV1. Discarded: ' + family + ' ×1. Render the machine with logicprobe_uml action=render and keep this diagram as its own view.', 'UML_NOT_A_STATE_DIAGRAM');
|
|
592
|
+
}
|
|
555
593
|
if (/^\s*@startuml/m.test(text)) {
|
|
556
594
|
// PlantUML declares the diagram kind by its body; the state keyword is the only
|
|
557
595
|
// structural one logicprobe emits, everything else in that family is a state diagram too.
|
|
@@ -767,10 +805,50 @@ function inferVariables(transitions, declared) {
|
|
|
767
805
|
}
|
|
768
806
|
return [...kinds.entries()].map(([name, kind]) => ({ name, kind, init: kind === 'boolean' ? false : 0 }));
|
|
769
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* PlantUML keywords that declare a construct LogicModelV1 has no place for. Their
|
|
810
|
+
* presence marks the text as a component, package, class, deployment or database
|
|
811
|
+
* diagram — a different diagram family. A state parser still reads *something* out of
|
|
812
|
+
* such a file (the arrows look like transitions), which is why the discarded
|
|
813
|
+
* declarations are collected and reported instead of being ignored.
|
|
814
|
+
*/
|
|
815
|
+
const PLANTUML_OTHER_CONSTRUCT = /^(component|package|class|interface|enum|enumeration|object|actor|usecase|node|artifact|database|rectangle|folder|frame|cloud|storage|collections|queue|stack|agent|boundary|control|entity|deployment|protocol|struct|exception|metaclass|stereotype|circle|hexagon|label|port|portin|portout)\b/i;
|
|
816
|
+
/** The node identifier a declaration introduces, when it has one (`… as ID`, or a bare `component ID`). */
|
|
817
|
+
function declaredIdentifier(text) {
|
|
818
|
+
const alias = /\bas\s+([A-Za-z_][A-Za-z0-9_]*)\s*;?$/.exec(text);
|
|
819
|
+
if (alias !== null)
|
|
820
|
+
return alias[1];
|
|
821
|
+
const bare = /^[A-Za-z]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*;?$/.exec(text);
|
|
822
|
+
return bare === null ? undefined : bare[1];
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Findings a parse result carries on its own. A diagram from another family parses
|
|
826
|
+
* into something; without this finding that something is presented as a model of the
|
|
827
|
+
* file, which is a false guarantee of exactly the kind this plugin exists to prevent.
|
|
828
|
+
*/
|
|
829
|
+
export function parseFindings(parsed) {
|
|
830
|
+
if (parsed.discardedConstructs.length === 0)
|
|
831
|
+
return [];
|
|
832
|
+
const counts = new Map();
|
|
833
|
+
for (const entry of parsed.discardedConstructs)
|
|
834
|
+
counts.set(entry.construct, (counts.get(entry.construct) ?? 0) + 1);
|
|
835
|
+
const summary = [...counts.entries()]
|
|
836
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
837
|
+
.map(([construct, count]) => construct + ' ×' + String(count))
|
|
838
|
+
.join(', ');
|
|
839
|
+
const shown = parsed.discardedConstructs.slice(0, 12).map((entry) => 'line ' + String(entry.line) + ': ' + entry.text).join(' | ');
|
|
840
|
+
return [{
|
|
841
|
+
code: 'UML_NOT_A_STATE_DIAGRAM',
|
|
842
|
+
severity: 'error',
|
|
843
|
+
message: 'the text is not a state or activity diagram: ' + String(parsed.discardedConstructs.length) + ' declaration(s) of unsupported construct(s) (' + summary + ') and ' + String(parsed.discardedEdges) + ' arrow(s) between them were read as states and transitions, so the parsed model is not this diagram.',
|
|
844
|
+
detail: shown + (parsed.discardedConstructs.length > 12 ? ' | … ' + String(parsed.discardedConstructs.length - 12) + ' more' : ''),
|
|
845
|
+
}];
|
|
846
|
+
}
|
|
770
847
|
function parseStateDiagram(text, notation) {
|
|
771
848
|
const lines = text.split(/\r?\n/);
|
|
772
849
|
const { directives, body } = readDirectives(lines, notation);
|
|
773
|
-
const raw = { states: [], display: new Map(), edges: [], terminals: [], finalMarks: [], warnings: [] };
|
|
850
|
+
const raw = { states: [], display: new Map(), edges: [], terminals: [], finalMarks: [], warnings: [], discarded: [], discardedNodes: new Set() };
|
|
851
|
+
let discardedEdges = 0;
|
|
774
852
|
const declared = new Set();
|
|
775
853
|
const declare = (name) => { if (!declared.has(name)) {
|
|
776
854
|
declared.add(name);
|
|
@@ -779,49 +857,74 @@ function parseStateDiagram(text, notation) {
|
|
|
779
857
|
const skip = notation === 'mermaid'
|
|
780
858
|
? /^(stateDiagram|stateDiagram-v2|direction\b|classDef\b|class\b|style\b|linkStyle\b|click\b|hide\b|scale\b|title\b|accTitle\b|accDescr\b|%%\{)/
|
|
781
859
|
: /^(@startuml|@enduml|scale\b|skinparam\b|title\b|hide\b|left to right direction|top to bottom direction|autonumber|!theme)/;
|
|
782
|
-
|
|
860
|
+
let inNote = false;
|
|
861
|
+
body.forEach((rawLine, index) => {
|
|
783
862
|
const line = rawLine.trim();
|
|
784
863
|
if (line === '' || (line.startsWith('--') && !line.includes('-->')))
|
|
785
|
-
|
|
864
|
+
return;
|
|
786
865
|
if (skip.test(line))
|
|
787
|
-
|
|
866
|
+
return;
|
|
867
|
+
const noteStart = /^note\b/i.test(line);
|
|
868
|
+
const noteEnd = /^end\s*note$/i.test(line);
|
|
869
|
+
if (notation === 'plantuml' && !inNote && !noteStart && !noteEnd) {
|
|
870
|
+
const other = PLANTUML_OTHER_CONSTRUCT.exec(line);
|
|
871
|
+
if (other !== null) {
|
|
872
|
+
raw.discarded.push({ construct: other[1].toLowerCase(), line: index + 1, text: line });
|
|
873
|
+
const id = declaredIdentifier(line);
|
|
874
|
+
if (id !== undefined)
|
|
875
|
+
raw.discardedNodes.add(id);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (noteEnd) {
|
|
880
|
+
inNote = false;
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
// A multi-line note body is prose, not a statement: skip it silently rather than
|
|
884
|
+
// reporting one ignored line per line of text.
|
|
885
|
+
if (inNote)
|
|
886
|
+
return;
|
|
887
|
+
if (noteStart && !/:\s*.+$/.test(line)) {
|
|
888
|
+
inNote = true;
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
788
891
|
const note = /^note\s+(?:over|right of|left of)\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)$/i.exec(line);
|
|
789
892
|
if (note !== null) {
|
|
790
893
|
declare(note[1]);
|
|
791
894
|
const existing = raw.display.get(note[1]);
|
|
792
895
|
if (existing === undefined)
|
|
793
896
|
raw.display.set(note[1], note[2].trim());
|
|
794
|
-
|
|
897
|
+
return;
|
|
795
898
|
}
|
|
796
|
-
if (
|
|
797
|
-
|
|
899
|
+
if (noteStart)
|
|
900
|
+
return;
|
|
798
901
|
const stateDecl = /^state\s+"([^"]*)"\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/.exec(line);
|
|
799
902
|
if (stateDecl !== null) {
|
|
800
903
|
declare(stateDecl[2]);
|
|
801
904
|
raw.display.set(stateDecl[2], stateDecl[1]);
|
|
802
|
-
|
|
905
|
+
return;
|
|
803
906
|
}
|
|
804
907
|
const bareState = /^state\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{?$/.exec(line);
|
|
805
908
|
if (bareState !== null) {
|
|
806
909
|
declare(bareState[1]);
|
|
807
|
-
|
|
910
|
+
return;
|
|
808
911
|
}
|
|
809
912
|
const concurrency = /^\}\s*$|^--\s*$/.test(line);
|
|
810
913
|
if (concurrency) {
|
|
811
914
|
raw.warnings.push('UML_PARSE_CONCURRENCY_FLATTENED: a concurrency region or composite block was flattened; LogicModelV1 has no region construct (use logicprobe_compose_verify for parallel machines).');
|
|
812
|
-
|
|
915
|
+
return;
|
|
813
916
|
}
|
|
814
917
|
const composite = /^state\s+(.+)\s*\{$/.exec(line);
|
|
815
918
|
if (composite !== null) {
|
|
816
919
|
raw.warnings.push('UML_PARSE_COMPOSITE_FLATTENED: composite state "' + composite[1].trim() + '" was flattened into its members.');
|
|
817
|
-
|
|
920
|
+
return;
|
|
818
921
|
}
|
|
819
922
|
const description = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)$/.exec(line);
|
|
820
923
|
if (description !== null) {
|
|
821
924
|
declare(description[1]);
|
|
822
925
|
if (!raw.display.has(description[1]))
|
|
823
926
|
raw.display.set(description[1], description[2].trim());
|
|
824
|
-
|
|
927
|
+
return;
|
|
825
928
|
}
|
|
826
929
|
const edge = /^(.+?)\s*-->\s*(.+?)(?:\s*:\s*(.*))?$/.exec(line);
|
|
827
930
|
if (edge !== null) {
|
|
@@ -835,27 +938,29 @@ function parseStateDiagram(text, notation) {
|
|
|
835
938
|
else {
|
|
836
939
|
raw.initialState = to;
|
|
837
940
|
}
|
|
838
|
-
|
|
941
|
+
return;
|
|
839
942
|
}
|
|
840
943
|
if (to === '[*]') {
|
|
841
944
|
raw.finalMarks.push(from);
|
|
842
|
-
|
|
945
|
+
return;
|
|
843
946
|
}
|
|
844
947
|
declare(from);
|
|
845
948
|
declare(to);
|
|
949
|
+
if (raw.discardedNodes.has(from) || raw.discardedNodes.has(to))
|
|
950
|
+
discardedEdges += 1;
|
|
846
951
|
raw.edges.push({ from, to, label });
|
|
847
|
-
|
|
952
|
+
return;
|
|
848
953
|
}
|
|
849
954
|
raw.warnings.push('UML_PARSE_IGNORED_LINE: "' + line + '" is not a state diagram statement; it was ignored.');
|
|
850
|
-
}
|
|
955
|
+
});
|
|
851
956
|
const model = rawToModel(raw, directives, raw.warnings);
|
|
852
957
|
const labels = mapLabels(raw.display, directives);
|
|
853
|
-
return { notation, diagram: 'state', model, labels, warnings: raw.warnings };
|
|
958
|
+
return { notation, diagram: 'state', model, labels, discardedConstructs: raw.discarded, discardedEdges, warnings: raw.warnings };
|
|
854
959
|
}
|
|
855
960
|
function parseActivityDiagram(text, notation) {
|
|
856
961
|
const lines = text.split(/\r?\n/);
|
|
857
962
|
const { directives, body } = readDirectives(lines, notation);
|
|
858
|
-
const raw = { states: [], display: new Map(), edges: [], terminals: [], finalMarks: [], warnings: [] };
|
|
963
|
+
const raw = { states: [], display: new Map(), edges: [], terminals: [], finalMarks: [], warnings: [], discarded: [], discardedNodes: new Set() };
|
|
859
964
|
const declared = new Set();
|
|
860
965
|
const declare = (name) => { if (!declared.has(name)) {
|
|
861
966
|
declared.add(name);
|
|
@@ -906,7 +1011,7 @@ function parseActivityDiagram(text, notation) {
|
|
|
906
1011
|
}
|
|
907
1012
|
const model = rawToModel(raw, directives, raw.warnings);
|
|
908
1013
|
const labels = mapLabels(raw.display, directives);
|
|
909
|
-
return { notation, diagram: 'activity', model, labels, warnings: raw.warnings };
|
|
1014
|
+
return { notation, diagram: 'activity', model, labels, discardedConstructs: [], discardedEdges: 0, warnings: raw.warnings };
|
|
910
1015
|
}
|
|
911
1016
|
/**
|
|
912
1017
|
* Read one node reference (`A`, `A["label"]`, `A(["label"])`) and register the id
|
|
@@ -944,6 +1049,47 @@ function mapLabels(display, directives) {
|
|
|
944
1049
|
out[directives.aliases.get(name) ?? name] = label;
|
|
945
1050
|
return out;
|
|
946
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* What the front end expects of labels and comments, so "why does my hand-drawn diagram
|
|
1054
|
+
* report label drift or ignored lines?" has a printed answer instead of a guess.
|
|
1055
|
+
* `tests/uml/run.mjs` feeds every claim here back through the parser, so this text cannot
|
|
1056
|
+
* drift away from the behaviour it describes.
|
|
1057
|
+
*/
|
|
1058
|
+
export function explainLabels(notation = 'mermaid') {
|
|
1059
|
+
const prefix = commentPrefix(notation);
|
|
1060
|
+
return {
|
|
1061
|
+
notation,
|
|
1062
|
+
directivePrefix: prefix + DIRECTIVE_NAMESPACE,
|
|
1063
|
+
directiveLines: [
|
|
1064
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'uml v1 notation=' + notation + ' diagram=state', meaning: 'declares the notation and diagram kind the file was rendered for' },
|
|
1065
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'init ID', meaning: 'pins the initial state (otherwise it is inferred as the only state nothing enters)' },
|
|
1066
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'terminal ID', meaning: 'marks a terminal state; comma-separate several' },
|
|
1067
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'alias NOTATION_ID model_id', meaning: 'restores a state id the notation cannot spell verbatim' },
|
|
1068
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'variable NAME integer', meaning: 'restores a variable and its kind' },
|
|
1069
|
+
],
|
|
1070
|
+
acceptedLabelForms: [
|
|
1071
|
+
{ form: 'bare id', example: 'state "IDLE" as IDLE', note: 'counts as NO meaning: the state stays undocumented (UML014)' },
|
|
1072
|
+
{ form: 'ID(meaning)', example: 'state "IDLE(waiting for power)" as IDLE', note: 'the form the renderer writes; full-width parentheses' },
|
|
1073
|
+
{ form: 'ID(meaning)', example: 'state "IDLE(waiting for power)" as IDLE', note: 'accepted as well' },
|
|
1074
|
+
{ form: 'a description line', example: 'IDLE : waiting for power', note: 'the same meaning written as a state description' },
|
|
1075
|
+
{ form: 'a single-line note', example: 'note right of IDLE : waiting for power', note: 'read as the state meaning, not as a comment' },
|
|
1076
|
+
{ form: 'any other text', example: 'state "waiting for power" as IDLE', note: 'taken as the meaning verbatim' },
|
|
1077
|
+
],
|
|
1078
|
+
renderedForm: 'state "ID(meaning)" as ID',
|
|
1079
|
+
ignoredLines: [
|
|
1080
|
+
{ pattern: 'a `' + prefix + '` comment line that is not a `logicprobe:` directive', behaviour: 'UML_PARSE_IGNORED_LINE warning: it carries no state-machine statement' },
|
|
1081
|
+
{ pattern: 'a `note …` block written over several lines', behaviour: 'skipped silently (a block is not a state meaning)' },
|
|
1082
|
+
{ pattern: 'notation chrome: @startuml/@enduml, stateDiagram-v2, direction, classDef/style/linkStyle/click, scale, skinparam, title, hide, autonumber', behaviour: 'skipped silently' },
|
|
1083
|
+
{ pattern: 'anything else the parser cannot read', behaviour: 'UML_PARSE_IGNORED_LINE warning, and the model is built from what it did read' },
|
|
1084
|
+
],
|
|
1085
|
+
rules: [
|
|
1086
|
+
'Label drift (UML015) compares the meaning inside the label with narrative.states[id]: the wrapper is stripped, the remaining text must match character for character.',
|
|
1087
|
+
'A meaning that differs only in wording is still drift — one of the two is stale, and the review cannot tell which.',
|
|
1088
|
+
'Keep non-directive comments out of the diagram: `logicprobe:` directives are the only comment lines the parser consumes.',
|
|
1089
|
+
'A missing label is not an error: the state renders as its bare id and is reported as undocumented (UML014).',
|
|
1090
|
+
],
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
947
1093
|
/**
|
|
948
1094
|
* Parse a Mermaid or PlantUML diagram back into a LogicModelV1.
|
|
949
1095
|
*
|
|
@@ -959,7 +1105,7 @@ export function parseUml(text, notation = 'auto') {
|
|
|
959
1105
|
const resolved = notation === 'auto' ? detectNotation(text) : notation;
|
|
960
1106
|
const kind = detectDiagram(text);
|
|
961
1107
|
if (kind === 'sequence') {
|
|
962
|
-
throw new UmlError('a sequence diagram is a trace, not a machine: parsing it would drop every branch the trace did not walk. Render diagram "state" or "activity" and parse that instead.');
|
|
1108
|
+
throw new UmlError('a sequence diagram is a trace, not a machine: parsing it would drop every branch the trace did not walk. Render diagram "state" or "activity" and parse that instead.', 'UML_NOT_A_STATE_DIAGRAM');
|
|
963
1109
|
}
|
|
964
1110
|
return kind === 'state' ? parseStateDiagram(text, resolved) : parseActivityDiagram(text, resolved);
|
|
965
1111
|
}
|
|
@@ -1198,9 +1344,24 @@ function structuralFindings(model, labels, warnings) {
|
|
|
1198
1344
|
code: 'UML013_NO_NARRATIVE',
|
|
1199
1345
|
severity: 'info',
|
|
1200
1346
|
message: 'the model carries no narrative block: no state, event or scenario has a natural-language meaning, so a reader must re-derive every symbol from the source.',
|
|
1201
|
-
detail: 'Add narrative.states / narrative.events / narrative.scenarios —
|
|
1347
|
+
detail: 'Add narrative.states / narrative.events / narrative.scenarios — a partial narrative is valid and its coverage is reported; write the states first and fill the rest in later.',
|
|
1202
1348
|
});
|
|
1203
1349
|
}
|
|
1350
|
+
else {
|
|
1351
|
+
// A narrative that covers part of the model is valid: states first, events and
|
|
1352
|
+
// scenarios later is the natural authoring order. The gap is reported as coverage
|
|
1353
|
+
// (and as this info finding), never as a validation failure.
|
|
1354
|
+
const coverage = narrativeCoverageOf(model);
|
|
1355
|
+
if (coverage !== undefined && !narrativeComplete(coverage)) {
|
|
1356
|
+
findings.push({
|
|
1357
|
+
code: 'UML027_NARRATIVE_PARTIAL',
|
|
1358
|
+
severity: 'info',
|
|
1359
|
+
message: 'the narrative covers part of the model: states ' + coverage.states + ', events ' + coverage.events + ', scenarios ' + coverage.scenarios + '. Every uncovered symbol still has to be re-derived from the source.',
|
|
1360
|
+
detail: 'narrativeCoverage carries the same numbers; complete the missing entries when the behaviour settles.',
|
|
1361
|
+
evidence: { narrativeCoverage: coverage },
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1204
1365
|
const documented = labels === undefined ? undefined : Object.keys(labels).filter((id) => documentedMeaning(labels[id], id) !== undefined);
|
|
1205
1366
|
if (documented !== undefined) {
|
|
1206
1367
|
const undocumented = model.states.map((state) => state.id).filter((id) => documentedMeaning(labels?.[id], id) === undefined);
|
|
@@ -1314,12 +1475,14 @@ function roundTripOf(model, notation, diagram, maxSteps) {
|
|
|
1314
1475
|
notation,
|
|
1315
1476
|
diagram,
|
|
1316
1477
|
ok: diffs.length === 0,
|
|
1478
|
+
hashSpec: DEFAULT_HASH_SPEC,
|
|
1317
1479
|
modelHash: modelHash(model),
|
|
1318
1480
|
parsedHash: modelHash(parsed.model),
|
|
1319
1481
|
diffs,
|
|
1320
1482
|
warnings: [...rendered.warnings, ...parsed.warnings],
|
|
1321
1483
|
},
|
|
1322
1484
|
primary: rendered.primary,
|
|
1485
|
+
parsed,
|
|
1323
1486
|
};
|
|
1324
1487
|
}
|
|
1325
1488
|
/** Compare two machines by structure — the fidelity measure behind the round-trip check. */
|
|
@@ -1395,18 +1558,28 @@ export function reviewUml(options) {
|
|
|
1395
1558
|
let parsed;
|
|
1396
1559
|
let primary;
|
|
1397
1560
|
let roundTrip = null;
|
|
1561
|
+
const metadataKeys = metadataKeysOf(hasModel ? options.model : undefined);
|
|
1562
|
+
const metadata = metadataKeys.length === 0 ? {} : { metadataKeys };
|
|
1563
|
+
let discardedConstructs = [];
|
|
1564
|
+
let discardedEdges = 0;
|
|
1398
1565
|
if (hasDiagram) {
|
|
1399
1566
|
try {
|
|
1400
1567
|
parsed = parseUml(options.diagram, options.notation ?? 'auto');
|
|
1401
1568
|
}
|
|
1402
1569
|
catch (error) {
|
|
1403
1570
|
const message = error instanceof Error ? error.message : String(error);
|
|
1571
|
+
const unreadable = { code: 'UML001_DIAGRAM_UNREADABLE', severity: 'error', message, detail: 'The diagram could not be read as a Mermaid/PlantUML state or activity diagram.' };
|
|
1404
1572
|
return {
|
|
1405
1573
|
ok: false,
|
|
1574
|
+
ran: true,
|
|
1575
|
+
...verdictOfFindings([unreadable]),
|
|
1576
|
+
schema: REPORT_SCHEMAS.umlReview,
|
|
1406
1577
|
source: hasModel ? 'model+diagram' : 'diagram',
|
|
1407
1578
|
summary: { errors: 1, warnings: 0, info: 0, states: 0, events: 0, transitions: 0, terminalStates: 0, reachableStates: 0, documentedStates: 0 },
|
|
1408
|
-
findings: [
|
|
1579
|
+
findings: [unreadable],
|
|
1409
1580
|
roundTrip: null,
|
|
1581
|
+
...metadata,
|
|
1582
|
+
hashes: { hashSpec: DEFAULT_HASH_SPEC },
|
|
1410
1583
|
warnings,
|
|
1411
1584
|
nextSteps: ['Fix the diagram syntax (or render one from a model with logicprobe_uml action=render) and review again.'],
|
|
1412
1585
|
};
|
|
@@ -1414,6 +1587,13 @@ export function reviewUml(options) {
|
|
|
1414
1587
|
notation = parsed.notation;
|
|
1415
1588
|
labels = parsed.labels;
|
|
1416
1589
|
warnings.push(...parsed.warnings);
|
|
1590
|
+
// A diagram from another family parses into something; that something must never
|
|
1591
|
+
// be read as a model of this file, so the parse defects are review findings here too.
|
|
1592
|
+
findings.push(...parseFindings(parsed));
|
|
1593
|
+
if (parsed.discardedConstructs.length > 0) {
|
|
1594
|
+
discardedConstructs = parsed.discardedConstructs;
|
|
1595
|
+
discardedEdges = parsed.discardedEdges;
|
|
1596
|
+
}
|
|
1417
1597
|
if (hasModel) {
|
|
1418
1598
|
model = compiledModel(options.model);
|
|
1419
1599
|
const diffs = diffModels(model, parsed.model);
|
|
@@ -1421,6 +1601,7 @@ export function reviewUml(options) {
|
|
|
1421
1601
|
notation: parsed.notation,
|
|
1422
1602
|
diagram: parsed.diagram,
|
|
1423
1603
|
ok: diffs.length === 0,
|
|
1604
|
+
hashSpec: DEFAULT_HASH_SPEC,
|
|
1424
1605
|
modelHash: modelHash(model),
|
|
1425
1606
|
parsedHash: modelHash(parsed.model),
|
|
1426
1607
|
diffs,
|
|
@@ -1511,9 +1692,12 @@ export function reviewUml(options) {
|
|
|
1511
1692
|
? (model.narrative?.states === undefined ? 0 : Object.keys(model.narrative.states).length)
|
|
1512
1693
|
: Object.keys(labels).filter((id) => documentedMeaning(labels?.[id], id) !== undefined).length;
|
|
1513
1694
|
const events = new Set(model.transitions.map((transition) => transition.event));
|
|
1695
|
+
const narrativeCoverage = narrativeCoverageOf(model);
|
|
1514
1696
|
const nextSteps = [];
|
|
1515
1697
|
if (errors > 0)
|
|
1516
1698
|
nextSteps.push('Resolve the error findings first — a diagram that cannot be read (or that disagrees with its model) will mislead every later review.');
|
|
1699
|
+
if (findings.some((finding) => finding.code === 'UML_NOT_A_STATE_DIAGRAM'))
|
|
1700
|
+
nextSteps.push('This text is not a state or activity diagram: keep it as its own view and model the machine as a state/activity diagram before reviewing it.');
|
|
1517
1701
|
nextSteps.push('Run logicprobe_verify on this model for the behavioural checks (S1-S8 structural, A1-A14 adversarial); the review above covers modelling, not behaviour.');
|
|
1518
1702
|
if (model.narrative === undefined)
|
|
1519
1703
|
nextSteps.push('Add narrative.states/events/scenarios so the diagram is readable against the code.');
|
|
@@ -1521,6 +1705,9 @@ export function reviewUml(options) {
|
|
|
1521
1705
|
nextSteps.push('Declare min/max (or boundaryChecks) before relying on A5 boundary probes.');
|
|
1522
1706
|
return {
|
|
1523
1707
|
ok: true,
|
|
1708
|
+
ran: true,
|
|
1709
|
+
...verdictOfFindings(findings),
|
|
1710
|
+
schema: REPORT_SCHEMAS.umlReview,
|
|
1524
1711
|
source: hasModel && hasDiagram ? 'model+diagram' : (hasDiagram ? 'diagram' : 'model'),
|
|
1525
1712
|
summary: {
|
|
1526
1713
|
errors,
|
|
@@ -1538,6 +1725,13 @@ export function reviewUml(options) {
|
|
|
1538
1725
|
...(labels === undefined ? {} : { labels }),
|
|
1539
1726
|
...(hasDiagram ? { model } : {}),
|
|
1540
1727
|
...(primary === undefined ? {} : { primary }),
|
|
1728
|
+
...metadata,
|
|
1729
|
+
...(narrativeCoverage === undefined ? {} : { narrativeCoverage }),
|
|
1730
|
+
hashes: {
|
|
1731
|
+
hashSpec: DEFAULT_HASH_SPEC,
|
|
1732
|
+
...(roundTrip === null ? {} : { modelHash: roundTrip.modelHash, parsedHash: roundTrip.parsedHash }),
|
|
1733
|
+
},
|
|
1734
|
+
...(discardedConstructs.length === 0 ? {} : { discardedConstructs, discardedEdges }),
|
|
1541
1735
|
warnings,
|
|
1542
1736
|
nextSteps,
|
|
1543
1737
|
};
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-logicprobe",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Verifies claims about designs and code. Facts go against the source. Behaviour goes through an executable model: state machines, protocols, composition, data models, migrations. Structure diagrams are read as dependency graphs, at one level or several. Two reports can be compared for regression. Exports UPPAAL, TLA+, PRISM and SPIN input. Ships a native DeepSeek Harness bundle that injects the verification gate into the first model step.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
},
|
|
13
13
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
14
|
"./client": "./lib/client.js",
|
|
15
|
-
"./package.json": "./package.json"
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
"./locale/*.json": "./locale/*.json"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"lib",
|
|
19
20
|
"src",
|
|
20
21
|
"skills",
|
|
21
|
-
"cordis.patch.yml"
|
|
22
|
+
"cordis.patch.yml",
|
|
23
|
+
"locale"
|
|
22
24
|
],
|
|
23
25
|
"dsh": {
|
|
24
26
|
"engines": {
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
]
|
|
39
41
|
},
|
|
40
42
|
"compatibility": {
|
|
41
|
-
"dsh": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.2-alpha.3 || ^0.1.2-alpha.4 || ^0.1.2-alpha.5 || ^0.1.2-rc.1 || ^0.1.3-alpha.1 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1 || ^0.1.5-rc.1 || ^0.1.5-alpha.2 || ^0.1.5-rc.2 || ^0.1.5-rc.3 || ^0.1.6-alpha.1 || ^0.1.6-alpha.2 || ^0.1.7-alpha.1 || ^0.1.7-alpha.2 || ^0.1.7-rc.1 || ^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
|
|
43
|
+
"dsh": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.2-alpha.3 || ^0.1.2-alpha.4 || ^0.1.2-alpha.5 || ^0.1.2-rc.1 || ^0.1.3-alpha.1 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1 || ^0.1.5-rc.1 || ^0.1.5-alpha.2 || ^0.1.5-rc.2 || ^0.1.5-rc.3 || ^0.1.6-alpha.1 || ^0.1.6-alpha.2 || ^0.1.7-alpha.1 || ^0.1.7-alpha.2 || ^0.1.7-rc.1 || ^0.1.7-rc.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2",
|
|
42
44
|
"dshReleases": {
|
|
43
45
|
"0.1.0-rc.7": "compatible",
|
|
44
46
|
"0.1.0-rc.8": "compatible",
|
|
@@ -64,7 +66,8 @@
|
|
|
64
66
|
"0.1.7-rc.2": "compatible",
|
|
65
67
|
"0.2.0-rc.1": "compatible",
|
|
66
68
|
"0.2.0-rc.2": "compatible",
|
|
67
|
-
"0.2.1-alpha.1": "compatible"
|
|
69
|
+
"0.2.1-alpha.1": "compatible",
|
|
70
|
+
"0.2.1-alpha.2": "compatible"
|
|
68
71
|
},
|
|
69
72
|
"profiles": [
|
|
70
73
|
"headless",
|
|
@@ -76,17 +79,17 @@
|
|
|
76
79
|
"build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
|
|
77
80
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
78
81
|
"bump": "node scripts/bump-version.mjs",
|
|
79
|
-
"test:engine": "npm run build && node tests/engine/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/dsh-client-half.test.mjs && node tests/dsh-volatile-fallback.test.mjs && node tests/exporters/run.mjs && node tests/external/run.mjs && node tests/python/run.mjs",
|
|
82
|
+
"test:engine": "npm run build && node tests/engine/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/dsh-client-half.test.mjs && node tests/dsh-volatile-fallback.test.mjs && node tests/claude-session-start-hook.test.mjs && node tests/skills/run.mjs && node tests/structure/run.mjs && node tests/baseline/run.mjs && node tests/granularity/run.mjs && node tests/exporters/run.mjs && node tests/external/run.mjs && node tests/python/run.mjs",
|
|
80
83
|
"test:full": "npm run build && node tests/full-suite.mjs",
|
|
81
84
|
"test:python": "npm run build && node tests/python/run.mjs"
|
|
82
85
|
},
|
|
83
86
|
"peerDependencies": {
|
|
84
87
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
85
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
|
|
86
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
|
|
87
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
|
|
88
|
-
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
|
|
89
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-alpha.1",
|
|
88
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2",
|
|
89
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2",
|
|
90
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2",
|
|
91
|
+
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2",
|
|
92
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.2.0-rc.1 || ^0.2.1-alpha.1 || ^0.2.1-alpha.2",
|
|
90
93
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
91
94
|
},
|
|
92
95
|
"devDependencies": {
|
|
@@ -104,11 +107,10 @@
|
|
|
104
107
|
"@deepseek-ai/dsh-skill": "^0.2.0-rc.2",
|
|
105
108
|
"@deepseek-ai/dsh-skill-filesystem": "^0.2.0-rc.2",
|
|
106
109
|
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|
|
107
|
-
"@deepseek-ai/dsh-timeout": "^0.2.0-rc.2",
|
|
108
110
|
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
|
|
109
111
|
"@deepseek-ai/dsh-user-approval": "^0.2.0-rc.2",
|
|
110
112
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
111
|
-
"@types/node": "^26.6.
|
|
113
|
+
"@types/node": "^26.6.4",
|
|
112
114
|
"typescript": "^7.0.2"
|
|
113
115
|
},
|
|
114
116
|
"author": {
|