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/src/tool.ts
CHANGED
|
@@ -10,13 +10,13 @@ export const LOGICPROBE_VERIFY_TOOL_NAME = 'logicprobe_verify'
|
|
|
10
10
|
* the 22-check report (S1-S8 structural, A1-A14), or a 26-check
|
|
11
11
|
* report when beforeModel is supplied (D1 behavioral preservation, D2
|
|
12
12
|
* invariant continuity, D3 regression delta, D4 deadlock/liveness regression).
|
|
13
|
-
* This is the dsh-native
|
|
14
|
-
*
|
|
13
|
+
* This is the dsh-native path: the model is built and run here, with no
|
|
14
|
+
* template to fill in by hand.
|
|
15
15
|
*/
|
|
16
16
|
export const logicProbeVerifyTool = defineTool({
|
|
17
17
|
name: LOGICPROBE_VERIFY_TOOL_NAME,
|
|
18
18
|
description:
|
|
19
|
-
'Run
|
|
19
|
+
'Run state-machine verification on a LogicModelV1 object. Model shape: schemaVersion 1, init, states ({id, terminal?}), transitions ({from, event, to, guard?, updates?, cost?}), plus optional variables, invariants, concurrentPairs, boundaryChecks, resourcePairs, idempotentEvents and narrative. Guards are structured: {variable, op, value}, {all}, {any} or {not}. Invariants cover never-states, var-in-range, event-before-state, leads-to, sequence, atomicity, budget, probability and deadline. `cost` defaults to 1 and drives the A12 budget check. `weight` defaults to 1 and makes the model a DTMC for A13. A state with `maxTicks` needs top-level `tickEvents`; A14 reports over-residency. The schema is closed: an undeclared key is a validation error. Keys starting with `_` are metadata: ignored, kept out of `modelHash`, and echoed as `metadataKeys`. Variables support monotonic inc and dec. Pass `beforeModel` and `stateMapping` to add the D1-D4 regression. Read `verdict`, never `ok`. The full schema is in the `logicprobe` skill: references/dsh-model-schema.md.',
|
|
20
20
|
parameters: {
|
|
21
21
|
model: {
|
|
22
22
|
type: 'json',
|
package/src/uml-tool.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
2
3
|
import type { JsonValue } from './json-value.js'
|
|
3
|
-
import {
|
|
4
|
+
import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, verdictOf, verdictOfFindings, type VerdictSummary } from './engine.js'
|
|
5
|
+
import { renderUml, parseUml, parseFindings, reviewUml, UmlError, type UmlDiagram, type UmlFinding, type UmlNotation, type UmlParseResult } from './uml.js'
|
|
4
6
|
|
|
5
7
|
export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml'
|
|
6
8
|
|
|
@@ -19,7 +21,7 @@ export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml'
|
|
|
19
21
|
export const logicProbeUmlTool = defineTool({
|
|
20
22
|
name: LOGICPROBE_UML_TOOL_NAME,
|
|
21
23
|
description:
|
|
22
|
-
'Model a code flow as UML, and
|
|
24
|
+
'Model a code flow as UML, and audit the modelling. action="render" turns a LogicModelV1 into diagram source: mermaid (state, activity, sequence) or plantuml (state, sequence). action="parse" reads Mermaid or PlantUML state or activity text back into a LogicModelV1, so a hand-drawn diagram can be verified. A sequence diagram is refused: a trace cannot reconstruct a machine. So is any other diagram family (class, ER, gantt, mindmap). That refusal carries errorCode "UML_NOT_A_STATE_DIAGRAM". A PlantUML component, package or deployment diagram declares no kind, so the parser detects those constructs itself. It returns a by-product model and reports the error UML_NOT_A_STATE_DIAGRAM, with `discardedConstructs` and `discardedEdges`. Never treat that model as a model of the file. action="review" audits the modelling: structural defects (UML002-UML009), documentation gaps (UML010-UML015), and a fidelity check that re-parses the rendered diagram (UML017). Give it a model, a diagram, or both. Rendering never invents structure. A construct the notation cannot express becomes a warning. For dependency questions on a component or class diagram, use `logicprobe_structure_verify` instead. Read `verdict`, not `ok`: error findings mean a failed review.',
|
|
23
25
|
parameters: {
|
|
24
26
|
action: {
|
|
25
27
|
type: 'string',
|
|
@@ -79,12 +81,31 @@ export const logicProbeUmlTool = defineTool({
|
|
|
79
81
|
if (args.action === 'render') {
|
|
80
82
|
if (args.model === undefined) return errorResult('action=render needs `model` (a LogicModelV1 object)')
|
|
81
83
|
const result = renderUml(args.model, (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation, (args.kind ?? 'state') as UmlDiagram, args.maxSteps)
|
|
82
|
-
return { ok: true, action: 'render', notation: result.notation, kind: result.diagram, diagram: result.primary, warnings: result.warnings } as unknown as JsonValue
|
|
84
|
+
return { ok: true, ran: true, ...verdictOf(0, result.warnings.length), schema: REPORT_SCHEMAS.umlRender, action: 'render', notation: result.notation, kind: result.diagram, diagram: result.primary, hashes: { diagram: createHash('sha256').update(result.primary).digest('hex') }, warnings: result.warnings } as unknown as JsonValue
|
|
83
85
|
}
|
|
84
86
|
if (args.action === 'parse') {
|
|
85
87
|
if (args.diagram === undefined) return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)')
|
|
86
88
|
const result = parseUml(args.diagram, args.notation ?? 'auto')
|
|
87
|
-
|
|
89
|
+
// A diagram from another family still parses into *something*. `parseFindings`
|
|
90
|
+
// says so, and the verdict is what stops that something from being trusted:
|
|
91
|
+
// `ok: true` only means the parser ran.
|
|
92
|
+
const findings = parseFindings(result)
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
ran: true,
|
|
96
|
+
...parseVerdict(result, findings),
|
|
97
|
+
schema: REPORT_SCHEMAS.umlParse,
|
|
98
|
+
action: 'parse',
|
|
99
|
+
notation: result.notation,
|
|
100
|
+
kind: result.diagram,
|
|
101
|
+
model: result.model,
|
|
102
|
+
labels: result.labels,
|
|
103
|
+
findings,
|
|
104
|
+
discardedConstructs: result.discardedConstructs,
|
|
105
|
+
discardedEdges: result.discardedEdges,
|
|
106
|
+
hashes: { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(result.model), diagram: createHash('sha256').update(args.diagram).digest('hex') },
|
|
107
|
+
warnings: result.warnings,
|
|
108
|
+
} as unknown as JsonValue
|
|
88
109
|
}
|
|
89
110
|
const report = reviewUml({
|
|
90
111
|
model: args.model,
|
|
@@ -96,11 +117,20 @@ export const logicProbeUmlTool = defineTool({
|
|
|
96
117
|
})
|
|
97
118
|
return report as unknown as JsonValue
|
|
98
119
|
} catch (error) {
|
|
99
|
-
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
121
|
+
return errorResult(message, error instanceof UmlError ? error.code : undefined) as unknown as JsonValue
|
|
100
122
|
}
|
|
101
123
|
},
|
|
102
124
|
})
|
|
103
125
|
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
/** The parse verdict counts parser notes as findings: an ignored line is information the model does not carry. */
|
|
127
|
+
function parseVerdict(result: UmlParseResult, findings: UmlFinding[]): VerdictSummary {
|
|
128
|
+
const errors = findings.filter((finding) => finding.severity === 'error').length
|
|
129
|
+
const warnings = findings.filter((finding) => finding.severity === 'warning').length + result.warnings.length
|
|
130
|
+
const firstCode = findings.find((finding) => finding.severity === 'error')?.code ?? findings.find((finding) => finding.severity === 'warning')?.code
|
|
131
|
+
return verdictOf(errors, warnings, firstCode)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function errorResult(message: string, errorCode = 'UML_INPUT'): JsonValue {
|
|
135
|
+
return { ok: false, ran: false, ...refusalVerdict(message), errorCode, error: message } as unknown as JsonValue
|
|
106
136
|
}
|
package/src/uml.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* @module logicprobe-uml
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import { validateModel, modelHash } from './engine.js'
|
|
34
|
+
import { REPORT_SCHEMAS, validateModel, modelHash, metadataKeysOf, narrativeComplete, narrativeCoverageOf, verdictOfFindings, DEFAULT_HASH_SPEC, type HashSpec, type NarrativeCoverage, type ReportSchema, type Verdict } from './engine.js'
|
|
35
35
|
import type { GuardNode, GuardOp, LeafGuard, LogicModelV1, StateSpec, TransitionSpec, UpdateSpec, VariableSpec } from './engine.js'
|
|
36
36
|
|
|
37
37
|
export type UmlNotation = 'mermaid' | 'plantuml'
|
|
@@ -72,9 +72,31 @@ export interface UmlParseResult {
|
|
|
72
72
|
* symbol, which the review reports.
|
|
73
73
|
*/
|
|
74
74
|
labels: Record<string, string>
|
|
75
|
+
/**
|
|
76
|
+
* Declarations found in the text that LogicModelV1 cannot represent. A non-empty
|
|
77
|
+
* list means the parsed model is NOT this diagram: the text belongs to another
|
|
78
|
+
* diagram family (component, package, class, deployment, …), so whatever came
|
|
79
|
+
* out is a by-product of reading keywords the parser does not own.
|
|
80
|
+
*/
|
|
81
|
+
discardedConstructs: DiscardedConstruct[]
|
|
82
|
+
/**
|
|
83
|
+
* Arrows whose endpoints were declared by a discarded construct. They survive
|
|
84
|
+
* parsing as state transitions, which is exactly why they are counted out loud.
|
|
85
|
+
*/
|
|
86
|
+
discardedEdges: number
|
|
75
87
|
warnings: string[]
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
/** One declaration the parser could not represent, with the source line that carried it. */
|
|
91
|
+
export interface DiscardedConstruct {
|
|
92
|
+
/** The construct keyword, lower-cased (`component`, `package`, `classDiagram`). */
|
|
93
|
+
construct: string
|
|
94
|
+
/** 1-based line number in the diagram source. */
|
|
95
|
+
line: number
|
|
96
|
+
/** The source line, trimmed. */
|
|
97
|
+
text: string
|
|
98
|
+
}
|
|
99
|
+
|
|
78
100
|
export interface UmlFinding {
|
|
79
101
|
code: string
|
|
80
102
|
severity: 'error' | 'warning' | 'info'
|
|
@@ -83,12 +105,16 @@ export interface UmlFinding {
|
|
|
83
105
|
events?: string[]
|
|
84
106
|
transitions?: Array<{ from: string; event: string; to: string }>
|
|
85
107
|
detail?: string
|
|
108
|
+
/** Machine-readable context for the finding (ids, coverage, rule matches), same shape as the engine's findings. */
|
|
109
|
+
evidence?: Record<string, unknown>
|
|
86
110
|
}
|
|
87
111
|
|
|
88
112
|
export interface UmlRoundTripReport {
|
|
89
113
|
notation: UmlNotation
|
|
90
114
|
diagram: UmlDiagram
|
|
91
115
|
ok: boolean
|
|
116
|
+
/** Which published hash specification the two hashes follow (see references/hash-spec.md). */
|
|
117
|
+
hashSpec: HashSpec
|
|
92
118
|
modelHash: string
|
|
93
119
|
parsedHash: string
|
|
94
120
|
diffs: string[]
|
|
@@ -96,7 +122,13 @@ export interface UmlRoundTripReport {
|
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
export interface UmlReviewReport {
|
|
125
|
+
/** The review ran and produced this report. It does NOT mean the modelling passed — read `verdict`. */
|
|
99
126
|
ok: boolean
|
|
127
|
+
ran: boolean
|
|
128
|
+
verdict: Verdict
|
|
129
|
+
verdictReason: string
|
|
130
|
+
/** The versioned report contract this result follows. */
|
|
131
|
+
schema: ReportSchema
|
|
100
132
|
source: 'model' | 'diagram' | 'model+diagram'
|
|
101
133
|
summary: {
|
|
102
134
|
errors: number
|
|
@@ -117,11 +149,31 @@ export interface UmlReviewReport {
|
|
|
117
149
|
model?: LogicModelV1
|
|
118
150
|
/** Diagram rendered from the model, when only a model was given. */
|
|
119
151
|
primary?: string
|
|
152
|
+
/** Paths of the `_`-prefixed metadata keys found in the supplied model, when it carried any. */
|
|
153
|
+
metadataKeys?: string[]
|
|
154
|
+
/** How much of the model the narrative documents; absent when there is no narrative. */
|
|
155
|
+
narrativeCoverage?: NarrativeCoverage
|
|
156
|
+
/** Hashes of what was reviewed, in one place. */
|
|
157
|
+
hashes: { hashSpec: HashSpec; modelHash?: string; parsedHash?: string }
|
|
158
|
+
/** Declarations the parser could not represent; a non-empty list fails the review. */
|
|
159
|
+
discardedConstructs?: DiscardedConstruct[]
|
|
160
|
+
/** Arrows whose endpoints came from a discarded construct. */
|
|
161
|
+
discardedEdges?: number
|
|
120
162
|
warnings: string[]
|
|
121
163
|
nextSteps: string[]
|
|
122
164
|
}
|
|
123
165
|
|
|
124
|
-
|
|
166
|
+
/**
|
|
167
|
+
* A UML front-end refusal. `code` lets the tool report *why* it refused instead of
|
|
168
|
+
* collapsing every refusal into a generic input error.
|
|
169
|
+
*/
|
|
170
|
+
export class UmlError extends Error {
|
|
171
|
+
readonly code: string
|
|
172
|
+
constructor(message: string, code = 'UML_INPUT') {
|
|
173
|
+
super(message)
|
|
174
|
+
this.code = code
|
|
175
|
+
}
|
|
176
|
+
}
|
|
125
177
|
|
|
126
178
|
// ---------------------------------------------------------------------------
|
|
127
179
|
// Shared helpers
|
|
@@ -557,9 +609,34 @@ interface DiagramLine {
|
|
|
557
609
|
diagram: 'state' | 'activity' | 'sequence'
|
|
558
610
|
}
|
|
559
611
|
|
|
612
|
+
/**
|
|
613
|
+
* Mermaid diagram-family headers that are neither a state machine nor a flow.
|
|
614
|
+
* Recognizing them matters twice: the text must not be reported as "cannot tell
|
|
615
|
+
* whether this is Mermaid or PlantUML", and it must not be parsed as a state
|
|
616
|
+
* diagram — every line of it would be ignored or misread, and the empty model that
|
|
617
|
+
* comes out would be presented as if it described the file.
|
|
618
|
+
*/
|
|
619
|
+
const MERMAID_OTHER_FAMILIES: readonly string[] = [
|
|
620
|
+
'classDiagram', 'erDiagram', 'gantt', 'pie', 'journey', 'mindmap', 'gitGraph',
|
|
621
|
+
'C4Context', 'C4Container', 'C4Component', 'C4Dynamic', 'C4Deployment',
|
|
622
|
+
'requirementDiagram', 'timeline', 'quadrantChart', 'sankey-beta', 'block-beta',
|
|
623
|
+
'packet-beta', 'architecture-beta', 'radar-beta', 'treemap-beta', 'xychart-beta',
|
|
624
|
+
]
|
|
625
|
+
|
|
626
|
+
/** The other-family header this text declares, if any. */
|
|
627
|
+
function otherMermaidFamily(text: string): string | undefined {
|
|
628
|
+
for (const family of MERMAID_OTHER_FAMILIES) {
|
|
629
|
+
if (new RegExp('^\\s*' + family + '\\b', 'm').test(text)) return family
|
|
630
|
+
}
|
|
631
|
+
return undefined
|
|
632
|
+
}
|
|
633
|
+
|
|
560
634
|
function detectNotation(text: string): UmlNotation {
|
|
561
635
|
if (/^\s*@start/m.test(text)) return 'plantuml'
|
|
562
636
|
if (/^\s*(stateDiagram|stateDiagram-v2|flowchart|graph|sequenceDiagram)\b/m.test(text)) return 'mermaid'
|
|
637
|
+
// A known Mermaid family is still Mermaid: say so, and let detectDiagram refuse it
|
|
638
|
+
// by name instead of blaming the notation.
|
|
639
|
+
if (otherMermaidFamily(text) !== undefined) return 'mermaid'
|
|
563
640
|
throw new UmlError('cannot tell whether this is Mermaid or PlantUML text: expected `stateDiagram-v2` / `flowchart` / `sequenceDiagram`, or `@startuml`')
|
|
564
641
|
}
|
|
565
642
|
|
|
@@ -567,6 +644,10 @@ function detectDiagram(text: string): 'state' | 'activity' | 'sequence' {
|
|
|
567
644
|
if (/^\s*stateDiagram/m.test(text)) return 'state'
|
|
568
645
|
if (/^\s*(flowchart|graph)\b/m.test(text)) return 'activity'
|
|
569
646
|
if (/^\s*sequenceDiagram\b/m.test(text)) return 'sequence'
|
|
647
|
+
const family = otherMermaidFamily(text)
|
|
648
|
+
if (family !== undefined) {
|
|
649
|
+
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')
|
|
650
|
+
}
|
|
570
651
|
if (/^\s*@startuml/m.test(text)) {
|
|
571
652
|
// PlantUML declares the diagram kind by its body; the state keyword is the only
|
|
572
653
|
// structural one logicprobe emits, everything else in that family is a state diagram too.
|
|
@@ -648,6 +729,10 @@ interface RawDiagram {
|
|
|
648
729
|
terminals: string[]
|
|
649
730
|
finalMarks: string[]
|
|
650
731
|
warnings: string[]
|
|
732
|
+
/** Declarations that belong to another diagram family (component, package, class, …). */
|
|
733
|
+
discarded: DiscardedConstruct[]
|
|
734
|
+
/** Node names those declarations introduced; arrows touching them are counted as discarded edges. */
|
|
735
|
+
discardedNodes: Set<string>
|
|
651
736
|
}
|
|
652
737
|
|
|
653
738
|
function rawToModel(raw: RawDiagram, directives: Directives, warnings: string[]): LogicModelV1 {
|
|
@@ -762,37 +847,94 @@ function inferVariables(transitions: TransitionSpec[], declared: Map<string, 'in
|
|
|
762
847
|
return [...kinds.entries()].map(([name, kind]) => ({ name, kind, init: kind === 'boolean' ? false : 0 }))
|
|
763
848
|
}
|
|
764
849
|
|
|
850
|
+
/**
|
|
851
|
+
* PlantUML keywords that declare a construct LogicModelV1 has no place for. Their
|
|
852
|
+
* presence marks the text as a component, package, class, deployment or database
|
|
853
|
+
* diagram — a different diagram family. A state parser still reads *something* out of
|
|
854
|
+
* such a file (the arrows look like transitions), which is why the discarded
|
|
855
|
+
* declarations are collected and reported instead of being ignored.
|
|
856
|
+
*/
|
|
857
|
+
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
|
|
858
|
+
|
|
859
|
+
/** The node identifier a declaration introduces, when it has one (`… as ID`, or a bare `component ID`). */
|
|
860
|
+
function declaredIdentifier(text: string): string | undefined {
|
|
861
|
+
const alias = /\bas\s+([A-Za-z_][A-Za-z0-9_]*)\s*;?$/.exec(text)
|
|
862
|
+
if (alias !== null) return alias[1]
|
|
863
|
+
const bare = /^[A-Za-z]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*;?$/.exec(text)
|
|
864
|
+
return bare === null ? undefined : bare[1]
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Findings a parse result carries on its own. A diagram from another family parses
|
|
869
|
+
* into something; without this finding that something is presented as a model of the
|
|
870
|
+
* file, which is a false guarantee of exactly the kind this plugin exists to prevent.
|
|
871
|
+
*/
|
|
872
|
+
export function parseFindings(parsed: UmlParseResult): UmlFinding[] {
|
|
873
|
+
if (parsed.discardedConstructs.length === 0) return []
|
|
874
|
+
const counts = new Map<string, number>()
|
|
875
|
+
for (const entry of parsed.discardedConstructs) counts.set(entry.construct, (counts.get(entry.construct) ?? 0) + 1)
|
|
876
|
+
const summary = [...counts.entries()]
|
|
877
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
878
|
+
.map(([construct, count]) => construct + ' ×' + String(count))
|
|
879
|
+
.join(', ')
|
|
880
|
+
const shown = parsed.discardedConstructs.slice(0, 12).map((entry) => 'line ' + String(entry.line) + ': ' + entry.text).join(' | ')
|
|
881
|
+
return [{
|
|
882
|
+
code: 'UML_NOT_A_STATE_DIAGRAM',
|
|
883
|
+
severity: 'error',
|
|
884
|
+
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.',
|
|
885
|
+
detail: shown + (parsed.discardedConstructs.length > 12 ? ' | … ' + String(parsed.discardedConstructs.length - 12) + ' more' : ''),
|
|
886
|
+
}]
|
|
887
|
+
}
|
|
888
|
+
|
|
765
889
|
function parseStateDiagram(text: string, notation: UmlNotation): UmlParseResult {
|
|
766
890
|
const lines = text.split(/\r?\n/)
|
|
767
891
|
const { directives, body } = readDirectives(lines, notation)
|
|
768
|
-
const raw: RawDiagram = { states: [], display: new Map(), edges: [], terminals: [], finalMarks: [], warnings: [] }
|
|
892
|
+
const raw: RawDiagram = { states: [], display: new Map(), edges: [], terminals: [], finalMarks: [], warnings: [], discarded: [], discardedNodes: new Set() }
|
|
893
|
+
let discardedEdges = 0
|
|
769
894
|
const declared = new Set<string>()
|
|
770
895
|
const declare = (name: string): void => { if (!declared.has(name)) { declared.add(name); raw.states.push(name) } }
|
|
771
896
|
const skip = notation === 'mermaid'
|
|
772
897
|
? /^(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|%%\{)/
|
|
773
898
|
: /^(@startuml|@enduml|scale\b|skinparam\b|title\b|hide\b|left to right direction|top to bottom direction|autonumber|!theme)/
|
|
774
|
-
|
|
899
|
+
let inNote = false
|
|
900
|
+
body.forEach((rawLine, index) => {
|
|
775
901
|
const line = rawLine.trim()
|
|
776
|
-
if (line === '' || (line.startsWith('--') && !line.includes('-->')))
|
|
777
|
-
if (skip.test(line))
|
|
902
|
+
if (line === '' || (line.startsWith('--') && !line.includes('-->'))) return
|
|
903
|
+
if (skip.test(line)) return
|
|
904
|
+
const noteStart = /^note\b/i.test(line)
|
|
905
|
+
const noteEnd = /^end\s*note$/i.test(line)
|
|
906
|
+
if (notation === 'plantuml' && !inNote && !noteStart && !noteEnd) {
|
|
907
|
+
const other = PLANTUML_OTHER_CONSTRUCT.exec(line)
|
|
908
|
+
if (other !== null) {
|
|
909
|
+
raw.discarded.push({ construct: other[1].toLowerCase(), line: index + 1, text: line })
|
|
910
|
+
const id = declaredIdentifier(line)
|
|
911
|
+
if (id !== undefined) raw.discardedNodes.add(id)
|
|
912
|
+
return
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (noteEnd) { inNote = false; return }
|
|
916
|
+
// A multi-line note body is prose, not a statement: skip it silently rather than
|
|
917
|
+
// reporting one ignored line per line of text.
|
|
918
|
+
if (inNote) return
|
|
919
|
+
if (noteStart && !/:\s*.+$/.test(line)) { inNote = true; return }
|
|
778
920
|
const note = /^note\s+(?:over|right of|left of)\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)$/i.exec(line)
|
|
779
921
|
if (note !== null) {
|
|
780
922
|
declare(note[1])
|
|
781
923
|
const existing = raw.display.get(note[1])
|
|
782
924
|
if (existing === undefined) raw.display.set(note[1], note[2].trim())
|
|
783
|
-
|
|
925
|
+
return
|
|
784
926
|
}
|
|
785
|
-
if (
|
|
927
|
+
if (noteStart) return
|
|
786
928
|
const stateDecl = /^state\s+"([^"]*)"\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/.exec(line)
|
|
787
|
-
if (stateDecl !== null) { declare(stateDecl[2]); raw.display.set(stateDecl[2], stateDecl[1]);
|
|
929
|
+
if (stateDecl !== null) { declare(stateDecl[2]); raw.display.set(stateDecl[2], stateDecl[1]); return }
|
|
788
930
|
const bareState = /^state\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{?$/.exec(line)
|
|
789
|
-
if (bareState !== null) { declare(bareState[1]);
|
|
931
|
+
if (bareState !== null) { declare(bareState[1]); return }
|
|
790
932
|
const concurrency = /^\}\s*$|^--\s*$/.test(line)
|
|
791
|
-
if (concurrency) { 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).');
|
|
933
|
+
if (concurrency) { 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).'); return }
|
|
792
934
|
const composite = /^state\s+(.+)\s*\{$/.exec(line)
|
|
793
|
-
if (composite !== null) { raw.warnings.push('UML_PARSE_COMPOSITE_FLATTENED: composite state "' + composite[1].trim() + '" was flattened into its members.');
|
|
935
|
+
if (composite !== null) { raw.warnings.push('UML_PARSE_COMPOSITE_FLATTENED: composite state "' + composite[1].trim() + '" was flattened into its members.'); return }
|
|
794
936
|
const description = /^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)$/.exec(line)
|
|
795
|
-
if (description !== null) { declare(description[1]); if (!raw.display.has(description[1])) raw.display.set(description[1], description[2].trim());
|
|
937
|
+
if (description !== null) { declare(description[1]); if (!raw.display.has(description[1])) raw.display.set(description[1], description[2].trim()); return }
|
|
796
938
|
const edge = /^(.+?)\s*-->\s*(.+?)(?:\s*:\s*(.*))?$/.exec(line)
|
|
797
939
|
if (edge !== null) {
|
|
798
940
|
const from = edge[1].trim()
|
|
@@ -804,25 +946,26 @@ function parseStateDiagram(text: string, notation: UmlNotation): UmlParseResult
|
|
|
804
946
|
} else {
|
|
805
947
|
raw.initialState = to
|
|
806
948
|
}
|
|
807
|
-
|
|
949
|
+
return
|
|
808
950
|
}
|
|
809
|
-
if (to === '[*]') { raw.finalMarks.push(from);
|
|
951
|
+
if (to === '[*]') { raw.finalMarks.push(from); return }
|
|
810
952
|
declare(from)
|
|
811
953
|
declare(to)
|
|
954
|
+
if (raw.discardedNodes.has(from) || raw.discardedNodes.has(to)) discardedEdges += 1
|
|
812
955
|
raw.edges.push({ from, to, label })
|
|
813
|
-
|
|
956
|
+
return
|
|
814
957
|
}
|
|
815
958
|
raw.warnings.push('UML_PARSE_IGNORED_LINE: "' + line + '" is not a state diagram statement; it was ignored.')
|
|
816
|
-
}
|
|
959
|
+
})
|
|
817
960
|
const model = rawToModel(raw, directives, raw.warnings)
|
|
818
961
|
const labels = mapLabels(raw.display, directives)
|
|
819
|
-
return { notation, diagram: 'state', model, labels, warnings: raw.warnings }
|
|
962
|
+
return { notation, diagram: 'state', model, labels, discardedConstructs: raw.discarded, discardedEdges, warnings: raw.warnings }
|
|
820
963
|
}
|
|
821
964
|
|
|
822
965
|
function parseActivityDiagram(text: string, notation: UmlNotation): UmlParseResult {
|
|
823
966
|
const lines = text.split(/\r?\n/)
|
|
824
967
|
const { directives, body } = readDirectives(lines, notation)
|
|
825
|
-
const raw: RawDiagram = { states: [], display: new Map(), edges: [], terminals: [], finalMarks: [], warnings: [] }
|
|
968
|
+
const raw: RawDiagram = { states: [], display: new Map(), edges: [], terminals: [], finalMarks: [], warnings: [], discarded: [], discardedNodes: new Set() }
|
|
826
969
|
const declared = new Set<string>()
|
|
827
970
|
const declare = (name: string): void => { if (!declared.has(name)) { declared.add(name); raw.states.push(name) } }
|
|
828
971
|
const skip = /^(flowchart|graph)\b|^(classDef|class|style|linkStyle|click|direction)\b|^%%\{/
|
|
@@ -858,7 +1001,7 @@ function parseActivityDiagram(text: string, notation: UmlNotation): UmlParseResu
|
|
|
858
1001
|
}
|
|
859
1002
|
const model = rawToModel(raw, directives, raw.warnings)
|
|
860
1003
|
const labels = mapLabels(raw.display, directives)
|
|
861
|
-
return { notation, diagram: 'activity', model, labels, warnings: raw.warnings }
|
|
1004
|
+
return { notation, diagram: 'activity', model, labels, discardedConstructs: [], discardedEdges: 0, warnings: raw.warnings }
|
|
862
1005
|
}
|
|
863
1006
|
|
|
864
1007
|
/**
|
|
@@ -892,6 +1035,68 @@ function mapLabels(display: Map<string, string>, directives: Directives): Record
|
|
|
892
1035
|
return out
|
|
893
1036
|
}
|
|
894
1037
|
|
|
1038
|
+
/** One accepted way of writing a state's meaning into a diagram label. */
|
|
1039
|
+
export interface LabelForm {
|
|
1040
|
+
form: string
|
|
1041
|
+
example: string
|
|
1042
|
+
note: string
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export interface LabelExplanation {
|
|
1046
|
+
notation: UmlNotation
|
|
1047
|
+
/** The comment prefix a `logicprobe:` directive uses in this notation. */
|
|
1048
|
+
directivePrefix: string
|
|
1049
|
+
directiveLines: Array<{ example: string; meaning: string }>
|
|
1050
|
+
/** Accepted label spellings, in the order `documentedMeaning` accepts them. */
|
|
1051
|
+
acceptedLabelForms: LabelForm[]
|
|
1052
|
+
/** What the renderer itself writes, so a hand-edited diagram can match it. */
|
|
1053
|
+
renderedForm: string
|
|
1054
|
+
ignoredLines: Array<{ pattern: string; behaviour: string }>
|
|
1055
|
+
rules: string[]
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* What the front end expects of labels and comments, so "why does my hand-drawn diagram
|
|
1060
|
+
* report label drift or ignored lines?" has a printed answer instead of a guess.
|
|
1061
|
+
* `tests/uml/run.mjs` feeds every claim here back through the parser, so this text cannot
|
|
1062
|
+
* drift away from the behaviour it describes.
|
|
1063
|
+
*/
|
|
1064
|
+
export function explainLabels(notation: UmlNotation = 'mermaid'): LabelExplanation {
|
|
1065
|
+
const prefix = commentPrefix(notation)
|
|
1066
|
+
return {
|
|
1067
|
+
notation,
|
|
1068
|
+
directivePrefix: prefix + DIRECTIVE_NAMESPACE,
|
|
1069
|
+
directiveLines: [
|
|
1070
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'uml v1 notation=' + notation + ' diagram=state', meaning: 'declares the notation and diagram kind the file was rendered for' },
|
|
1071
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'init ID', meaning: 'pins the initial state (otherwise it is inferred as the only state nothing enters)' },
|
|
1072
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'terminal ID', meaning: 'marks a terminal state; comma-separate several' },
|
|
1073
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'alias NOTATION_ID model_id', meaning: 'restores a state id the notation cannot spell verbatim' },
|
|
1074
|
+
{ example: prefix + DIRECTIVE_NAMESPACE + 'variable NAME integer', meaning: 'restores a variable and its kind' },
|
|
1075
|
+
],
|
|
1076
|
+
acceptedLabelForms: [
|
|
1077
|
+
{ form: 'bare id', example: 'state "IDLE" as IDLE', note: 'counts as NO meaning: the state stays undocumented (UML014)' },
|
|
1078
|
+
{ form: 'ID(meaning)', example: 'state "IDLE(waiting for power)" as IDLE', note: 'the form the renderer writes; full-width parentheses' },
|
|
1079
|
+
{ form: 'ID(meaning)', example: 'state "IDLE(waiting for power)" as IDLE', note: 'accepted as well' },
|
|
1080
|
+
{ form: 'a description line', example: 'IDLE : waiting for power', note: 'the same meaning written as a state description' },
|
|
1081
|
+
{ form: 'a single-line note', example: 'note right of IDLE : waiting for power', note: 'read as the state meaning, not as a comment' },
|
|
1082
|
+
{ form: 'any other text', example: 'state "waiting for power" as IDLE', note: 'taken as the meaning verbatim' },
|
|
1083
|
+
],
|
|
1084
|
+
renderedForm: 'state "ID(meaning)" as ID',
|
|
1085
|
+
ignoredLines: [
|
|
1086
|
+
{ pattern: 'a `' + prefix + '` comment line that is not a `logicprobe:` directive', behaviour: 'UML_PARSE_IGNORED_LINE warning: it carries no state-machine statement' },
|
|
1087
|
+
{ pattern: 'a `note …` block written over several lines', behaviour: 'skipped silently (a block is not a state meaning)' },
|
|
1088
|
+
{ pattern: 'notation chrome: @startuml/@enduml, stateDiagram-v2, direction, classDef/style/linkStyle/click, scale, skinparam, title, hide, autonumber', behaviour: 'skipped silently' },
|
|
1089
|
+
{ pattern: 'anything else the parser cannot read', behaviour: 'UML_PARSE_IGNORED_LINE warning, and the model is built from what it did read' },
|
|
1090
|
+
],
|
|
1091
|
+
rules: [
|
|
1092
|
+
'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.',
|
|
1093
|
+
'A meaning that differs only in wording is still drift — one of the two is stale, and the review cannot tell which.',
|
|
1094
|
+
'Keep non-directive comments out of the diagram: `logicprobe:` directives are the only comment lines the parser consumes.',
|
|
1095
|
+
'A missing label is not an error: the state renders as its bare id and is reported as undocumented (UML014).',
|
|
1096
|
+
],
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
895
1100
|
/**
|
|
896
1101
|
* Parse a Mermaid or PlantUML diagram back into a LogicModelV1.
|
|
897
1102
|
*
|
|
@@ -907,7 +1112,7 @@ export function parseUml(text: string, notation: UmlNotation | 'auto' = 'auto'):
|
|
|
907
1112
|
const resolved = notation === 'auto' ? detectNotation(text) : notation
|
|
908
1113
|
const kind = detectDiagram(text)
|
|
909
1114
|
if (kind === 'sequence') {
|
|
910
|
-
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.')
|
|
1115
|
+
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')
|
|
911
1116
|
}
|
|
912
1117
|
return kind === 'state' ? parseStateDiagram(text, resolved) : parseActivityDiagram(text, resolved)
|
|
913
1118
|
}
|
|
@@ -1137,8 +1342,22 @@ function structuralFindings(model: LogicModelV1, labels: Record<string, string>
|
|
|
1137
1342
|
code: 'UML013_NO_NARRATIVE',
|
|
1138
1343
|
severity: 'info',
|
|
1139
1344
|
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.',
|
|
1140
|
-
detail: 'Add narrative.states / narrative.events / narrative.scenarios —
|
|
1345
|
+
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.',
|
|
1141
1346
|
})
|
|
1347
|
+
} else {
|
|
1348
|
+
// A narrative that covers part of the model is valid: states first, events and
|
|
1349
|
+
// scenarios later is the natural authoring order. The gap is reported as coverage
|
|
1350
|
+
// (and as this info finding), never as a validation failure.
|
|
1351
|
+
const coverage = narrativeCoverageOf(model)
|
|
1352
|
+
if (coverage !== undefined && !narrativeComplete(coverage)) {
|
|
1353
|
+
findings.push({
|
|
1354
|
+
code: 'UML027_NARRATIVE_PARTIAL',
|
|
1355
|
+
severity: 'info',
|
|
1356
|
+
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.',
|
|
1357
|
+
detail: 'narrativeCoverage carries the same numbers; complete the missing entries when the behaviour settles.',
|
|
1358
|
+
evidence: { narrativeCoverage: coverage },
|
|
1359
|
+
})
|
|
1360
|
+
}
|
|
1142
1361
|
}
|
|
1143
1362
|
|
|
1144
1363
|
const documented = labels === undefined ? undefined : Object.keys(labels).filter((id) => documentedMeaning(labels[id], id) !== undefined)
|
|
@@ -1229,7 +1448,7 @@ function compiledModel(input: unknown): LogicModelV1 {
|
|
|
1229
1448
|
return validation.model
|
|
1230
1449
|
}
|
|
1231
1450
|
|
|
1232
|
-
function roundTripOf(model: LogicModelV1, notation: UmlNotation, diagram: UmlDiagram, maxSteps: number): { report: UmlRoundTripReport; primary: string } {
|
|
1451
|
+
function roundTripOf(model: LogicModelV1, notation: UmlNotation, diagram: UmlDiagram, maxSteps: number): { report: UmlRoundTripReport; primary: string; parsed: UmlParseResult } {
|
|
1233
1452
|
const rendered = renderUml(model, notation, diagram, maxSteps)
|
|
1234
1453
|
const parsed = parseUml(rendered.primary, notation)
|
|
1235
1454
|
const diffs = diffModels(model, parsed.model)
|
|
@@ -1238,12 +1457,14 @@ function roundTripOf(model: LogicModelV1, notation: UmlNotation, diagram: UmlDia
|
|
|
1238
1457
|
notation,
|
|
1239
1458
|
diagram,
|
|
1240
1459
|
ok: diffs.length === 0,
|
|
1460
|
+
hashSpec: DEFAULT_HASH_SPEC,
|
|
1241
1461
|
modelHash: modelHash(model),
|
|
1242
1462
|
parsedHash: modelHash(parsed.model),
|
|
1243
1463
|
diffs,
|
|
1244
1464
|
warnings: [...rendered.warnings, ...parsed.warnings],
|
|
1245
1465
|
},
|
|
1246
1466
|
primary: rendered.primary,
|
|
1467
|
+
parsed,
|
|
1247
1468
|
}
|
|
1248
1469
|
}
|
|
1249
1470
|
|
|
@@ -1320,18 +1541,28 @@ export function reviewUml(options: UmlReviewOptions): UmlReviewReport {
|
|
|
1320
1541
|
let parsed: UmlParseResult | undefined
|
|
1321
1542
|
let primary: string | undefined
|
|
1322
1543
|
let roundTrip: UmlRoundTripReport | null = null
|
|
1544
|
+
const metadataKeys = metadataKeysOf(hasModel ? options.model : undefined)
|
|
1545
|
+
const metadata = metadataKeys.length === 0 ? {} : { metadataKeys }
|
|
1546
|
+
let discardedConstructs: DiscardedConstruct[] = []
|
|
1547
|
+
let discardedEdges = 0
|
|
1323
1548
|
|
|
1324
1549
|
if (hasDiagram) {
|
|
1325
1550
|
try {
|
|
1326
1551
|
parsed = parseUml(options.diagram as string, options.notation ?? 'auto')
|
|
1327
1552
|
} catch (error) {
|
|
1328
1553
|
const message = error instanceof Error ? error.message : String(error)
|
|
1554
|
+
const unreadable: UmlFinding = { code: 'UML001_DIAGRAM_UNREADABLE', severity: 'error', message, detail: 'The diagram could not be read as a Mermaid/PlantUML state or activity diagram.' }
|
|
1329
1555
|
return {
|
|
1330
1556
|
ok: false,
|
|
1557
|
+
ran: true,
|
|
1558
|
+
...verdictOfFindings([unreadable]),
|
|
1559
|
+
schema: REPORT_SCHEMAS.umlReview,
|
|
1331
1560
|
source: hasModel ? 'model+diagram' : 'diagram',
|
|
1332
1561
|
summary: { errors: 1, warnings: 0, info: 0, states: 0, events: 0, transitions: 0, terminalStates: 0, reachableStates: 0, documentedStates: 0 },
|
|
1333
|
-
findings: [
|
|
1562
|
+
findings: [unreadable],
|
|
1334
1563
|
roundTrip: null,
|
|
1564
|
+
...metadata,
|
|
1565
|
+
hashes: { hashSpec: DEFAULT_HASH_SPEC },
|
|
1335
1566
|
warnings,
|
|
1336
1567
|
nextSteps: ['Fix the diagram syntax (or render one from a model with logicprobe_uml action=render) and review again.'],
|
|
1337
1568
|
}
|
|
@@ -1339,6 +1570,13 @@ export function reviewUml(options: UmlReviewOptions): UmlReviewReport {
|
|
|
1339
1570
|
notation = parsed.notation
|
|
1340
1571
|
labels = parsed.labels
|
|
1341
1572
|
warnings.push(...parsed.warnings)
|
|
1573
|
+
// A diagram from another family parses into something; that something must never
|
|
1574
|
+
// be read as a model of this file, so the parse defects are review findings here too.
|
|
1575
|
+
findings.push(...parseFindings(parsed))
|
|
1576
|
+
if (parsed.discardedConstructs.length > 0) {
|
|
1577
|
+
discardedConstructs = parsed.discardedConstructs
|
|
1578
|
+
discardedEdges = parsed.discardedEdges
|
|
1579
|
+
}
|
|
1342
1580
|
if (hasModel) {
|
|
1343
1581
|
model = compiledModel(options.model)
|
|
1344
1582
|
const diffs = diffModels(model, parsed.model)
|
|
@@ -1346,6 +1584,7 @@ export function reviewUml(options: UmlReviewOptions): UmlReviewReport {
|
|
|
1346
1584
|
notation: parsed.notation,
|
|
1347
1585
|
diagram: parsed.diagram,
|
|
1348
1586
|
ok: diffs.length === 0,
|
|
1587
|
+
hashSpec: DEFAULT_HASH_SPEC,
|
|
1349
1588
|
modelHash: modelHash(model),
|
|
1350
1589
|
parsedHash: modelHash(parsed.model),
|
|
1351
1590
|
diffs,
|
|
@@ -1431,13 +1670,18 @@ export function reviewUml(options: UmlReviewOptions): UmlReviewReport {
|
|
|
1431
1670
|
? (model.narrative?.states === undefined ? 0 : Object.keys(model.narrative.states).length)
|
|
1432
1671
|
: Object.keys(labels).filter((id) => documentedMeaning(labels?.[id], id) !== undefined).length
|
|
1433
1672
|
const events = new Set(model.transitions.map((transition) => transition.event))
|
|
1673
|
+
const narrativeCoverage = narrativeCoverageOf(model)
|
|
1434
1674
|
const nextSteps: string[] = []
|
|
1435
1675
|
if (errors > 0) nextSteps.push('Resolve the error findings first — a diagram that cannot be read (or that disagrees with its model) will mislead every later review.')
|
|
1676
|
+
if (findings.some((finding) => finding.code === 'UML_NOT_A_STATE_DIAGRAM')) 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.')
|
|
1436
1677
|
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.')
|
|
1437
1678
|
if (model.narrative === undefined) nextSteps.push('Add narrative.states/events/scenarios so the diagram is readable against the code.')
|
|
1438
1679
|
if (findings.some((finding) => finding.code === 'UML011_UNBOUNDED_VARIABLE')) nextSteps.push('Declare min/max (or boundaryChecks) before relying on A5 boundary probes.')
|
|
1439
1680
|
return {
|
|
1440
1681
|
ok: true,
|
|
1682
|
+
ran: true,
|
|
1683
|
+
...verdictOfFindings(findings),
|
|
1684
|
+
schema: REPORT_SCHEMAS.umlReview,
|
|
1441
1685
|
source: hasModel && hasDiagram ? 'model+diagram' : (hasDiagram ? 'diagram' : 'model'),
|
|
1442
1686
|
summary: {
|
|
1443
1687
|
errors,
|
|
@@ -1455,6 +1699,13 @@ export function reviewUml(options: UmlReviewOptions): UmlReviewReport {
|
|
|
1455
1699
|
...(labels === undefined ? {} : { labels }),
|
|
1456
1700
|
...(hasDiagram ? { model } : {}),
|
|
1457
1701
|
...(primary === undefined ? {} : { primary }),
|
|
1702
|
+
...metadata,
|
|
1703
|
+
...(narrativeCoverage === undefined ? {} : { narrativeCoverage }),
|
|
1704
|
+
hashes: {
|
|
1705
|
+
hashSpec: DEFAULT_HASH_SPEC,
|
|
1706
|
+
...(roundTrip === null ? {} : { modelHash: roundTrip.modelHash, parsedHash: roundTrip.parsedHash }),
|
|
1707
|
+
},
|
|
1708
|
+
...(discardedConstructs.length === 0 ? {} : { discardedConstructs, discardedEdges }),
|
|
1458
1709
|
warnings,
|
|
1459
1710
|
nextSteps,
|
|
1460
1711
|
}
|