dsh-logicprobe 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-logicprobe",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
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",
@@ -203,6 +203,25 @@ Turn the verdict into an answer, never into a summary of `ok`:
203
203
 
204
204
  Exit codes: `0` for `pass` / `pass_with_findings`, `2` for `fail`, a refusal, or an unreadable input. In DSH the native tools return the report as data — read `verdict`, and treat `errorCode`/`error` as the refusal path.
205
205
 
206
+ ### Writing the Answer: Name the Finding, Not Its Code
207
+
208
+ `S2_NO_TRANSITIONS`, `UML019`, `DA2` are values of `findings[].code`, not names. A reader
209
+ who has not memorized the schema learns nothing from one, and cannot tell whether their
210
+ machine is sound or stuck.
211
+
212
+ - **Say what it means**, in the user's language and in the terms of their own code:
213
+ 「启动超时后 ERROR 没有任何出口迁移,机器会停在那里」, not 「S2 命中」. The finding's
214
+ `message` is written for the report; the sentence for the user is written from it.
215
+ - **Keep the identifier where the identifier is the subject**: a report field quoted
216
+ verbatim, a baseline diff, a test name, or a question about the check itself.
217
+ - **Name the property, not the checking.** "This path locks without unlocking" beats
218
+ "A4 failed". A check's name earns its place when the reader asked about the checking.
219
+ - **Carry the verdict in words.** "Not verified" or "verified, within the scope of this
220
+ model" — never a bare pass, and never a warning presented without its label.
221
+
222
+ A reader who cannot tell what failed will either trust a report they cannot read, or
223
+ ignore one they should act on.
224
+
206
225
  ### Slice Acceptance: Baseline Diffs
207
226
 
208
227
  The acceptance criterion for a refactoring slice is usually "no **new** findings", not
@@ -42,3 +42,4 @@ The full routing table, including hard-real-time, hybrid-control and execution-c
42
42
  3. **One counterexample is enough to refute.** If a race is suspected, the deliverable is the interleaving that breaks it plus the harness that demonstrates it (TSan/Helgrind output), not an argument.
43
43
  4. **Sequential targets are out of scope.** If nothing runs concurrently, say so and stop.
44
44
  5. **The scanner is dsh-only.** `logicprobe_concurrency_scan` has no mirror subcommand. Without it, mine the text by hand against the keyword table above and route each claim the same way.
45
+ 6. **Name the claim and its routing in words.** 「QoS 1 只投递一次」plus the tool that decides it, never a bare check name. A reader who has not memorized this skill's tables learns nothing from an enumeration. The full rule is under "Writing the Answer" in the `logicprobe` skill.
@@ -92,6 +92,11 @@ The **DataModelV1 shape** is in [`references/data-model-schema.md`](references/d
92
92
  | DD3 | Delta summary | Warning |
93
93
  | DD4 | Breaking change regression | Error/Warning |
94
94
 
95
+ Every `#` in these tables is a report field, not a name. Write what the check found for the
96
+ reader (「回滚与备份不对称」), in their language; keep the id for a report field quoted
97
+ verbatim, a baseline diff or a test name. The full rule is under "Writing the Answer" in the
98
+ `logicprobe` skill.
99
+
95
100
  ## Extraction Rule
96
101
 
97
102
  Before writing any verification code, output a data-model table:
@@ -91,5 +91,6 @@ This tool audits the **diagram**; an include or dependency scan audits the **cod
91
91
  2. **Quote the rule id for every judged edge.** "Not allowed" without the rule is an opinion; `matchedRules` makes it a finding.
92
92
  3. **Run the numbers, never the impression.** An isolated node and a back edge are exactly what a reader misses by eye; that is why the check exists.
93
93
  4. **Report the diagram-code difference, not just the diagram.** A structure review that never touches the source is a review of a drawing.
94
+ 5. **Report the rule id with its meaning, never the id alone.** Rule 2 keeps the id on every judged edge, so the finding is defensible; the sentence the reader gets says what the edge violated (「HAL 反向依赖了上层」), in their language. The full rule is under "Writing the Answer" in the `logicprobe` skill.
94
95
 
95
96
  The full matrix schema, a worked example and the reconciliation worksheet are in [`references/structure-review-guide.md`](references/structure-review-guide.md).
@@ -32,6 +32,8 @@ code flow → model (citation per element) → logicprobe_uml action=render →
32
32
  - **review** — it answers one of three questions. Give it a model: is the machine well-modelled? Give it a diagram: what does the diagram say? Give it both: does the diagram match the model?
33
33
  - **explain-labels** — the parser's own account of labels and comments: the accepted spellings, the form the renderer writes, and which lines it ignores. No diagram needed. It answers "why does my hand-drawn diagram report drift or ignored lines?" without reading the source.
34
34
 
35
+ The view follows the question, not the machine, and no tool picks it for you. Declare the question with `intent` (`present` / `verify` / `fidelity`) and a view that cannot serve it is refused before any work, with a code that names why. The ordered procedure, the intent-by-action table and those codes are under [Choosing the view](references/uml-modeling-guide.md#choosing-the-view).
36
+
35
37
  A host without the tools runs the same four actions through the repository engine. The engine is deliberately single-source — do not copy it into this skill.
36
38
 
37
39
  ## A Structure Diagram Is the Dangerous Case, Because It Parses
@@ -53,5 +55,6 @@ Structural defects: unreachable states, dead ends, ambiguous or non-exhaustive b
53
55
  3. **Check the verdict before the picture.** `ok: true` with `verdict: "fail"` means the review failed; a diagram whose review failed is not something to show as if it were checked.
54
56
  4. **The review never replaces verification.** It covers the modelling. S1-S8 and A1-A14 cover the behaviour, and each finding names the check that settles it — hand those over to the `logicprobe` skill.
55
57
  5. **Keep the narrative with the model.** Write `narrative.states`, `narrative.events` and `narrative.scenarios`. Then the diagram stays readable against the code, and label drift shows up as a finding instead of as a stale picture.
58
+ 6. **Name a finding for the reader, not by its code.** `UML017_ROUND_TRIP_MISMATCH` is a report field; the sentence the reader gets says what disagreed (「图里少了 ERROR→COOLDOWN 这条迁移」), in their language. Keep the code for a report field quoted verbatim, a diff or a test name. The full rule is under "Writing the Answer" in the `logicprobe` skill.
56
59
 
57
60
  The full checklist (codes `UML001` to `UML019` plus `UML_NOT_A_STATE_DIAGRAM`), the directive format generated diagrams carry, a worked example, and the limits of each view are in [`references/uml-modeling-guide.md`](references/uml-modeling-guide.md).
@@ -4,7 +4,17 @@ This guide covers two jobs: drawing a code flow as UML, and reviewing that drawi
4
4
 
5
5
  One rule runs through the whole guide. **A diagram is a model, and a model can be wrong.** A tidy flow chart is not evidence about the code. Worse, a diagram that disagrees with the model it came from will mislead every reader who trusts it.
6
6
 
7
- ## When to use this
7
+ ## Choosing the view
8
+
9
+ The view follows the question, not the machine. Decide in this order.
10
+
11
+ **1. Is the question about where dependencies point?** Architecture, layering, module
12
+ structure, "may this call that" — that question belongs to the `logicprobe-structure`
13
+ skill, which reads the same component, package, class or deployment text as a dependency
14
+ graph. The state front end refuses that text by name
15
+ (`UML_NOT_A_STATE_DIAGRAM`) instead of reviewing it.
16
+
17
+ **2. Otherwise fix the boundary first, then pick the view by the reader's question.**
8
18
 
9
19
  | Situation | View that answers it |
10
20
  |---|---|
@@ -14,7 +24,65 @@ One rule runs through the whole guide. **A diagram is a model, and a model can b
14
24
  | "Somebody gave me a diagram. Is it right?" | Any view. Feed it to the review and compare it against the code. |
15
25
  | "This diagram and this model disagree." | Feed both to the review. The round-trip check names every difference. |
16
26
 
17
- Do not use this guide for a behavioural claim about a specific machine. That is the job of `logicprobe_verify` (S1-S8 and A1-A14). The review here covers the modelling, not the behaviour. Every finding it produces names the engine check that settles the behavioural half.
27
+ **3. Check the choice against the next step.** A view that cannot feed the check you are
28
+ about to run is the wrong view, however readable it looks.
29
+
30
+ | Next step | Views that can feed it |
31
+ |---|---|
32
+ | `logicprobe_verify` (S1-S8 and A1-A14) | State or activity: `parse` reads either into a LogicModelV1 |
33
+ | `logicprobe_uml action=review` with a model, for the fidelity check | State or activity: only those round-trip |
34
+ | `logicprobe_uml action=review` with a model alone | Any view. With `kind: "sequence"` the trace is rendered and the fidelity check is reported inapplicable (`UML019`) |
35
+ | A human reading a protocol exchange | Sequence, which is the honest view there |
36
+
37
+ **4. Say that out loud, and the tool refuses a pairing that cannot serve it.** `intent`
38
+ declares what the artifact is for. The check runs before any work, so an impossible
39
+ pairing is a refusal with a code rather than a report about the wrong artifact.
40
+
41
+ | Intent | What it means | Actions that serve it | View it needs |
42
+ |---|---|---|---|
43
+ | `present` | a human is going to read the diagram | render, review | any view the notation can draw |
44
+ | `verify` | it must be parseable into a LogicModelV1 for the engine | render, parse, review | state or activity: a trace cannot be parsed back |
45
+ | `fidelity` | the point is the render-and-reparse proof | review | state or activity: only those round-trip |
46
+
47
+ | `errorCode` | What happened |
48
+ |---|---|
49
+ | `UML_INTENT_UNSERVABLE` | The view exists but cannot serve the intent — a sequence view for `verify` or `fidelity`. |
50
+ | `UML_VIEW_UNREPRESENTABLE` | The `(notation, view)` cell does not exist — PlantUML activity. |
51
+ | `UML_INTENT_MISMATCH` | The intent names something this action does not do — `fidelity` on render (render proves nothing), `present` or `fidelity` on parse (parse returns a model). |
52
+
53
+ Omitting `intent` changes nothing: a sequence view still renders, and a review of one
54
+ still reports `UML019`. The intent is an opt-in statement of purpose, and nothing infers
55
+ it. The CLI mirror takes the same values: `--intent present|verify|fidelity` on
56
+ `uml-render`, `uml-parse` and `uml-review`.
57
+
58
+ When the question is still unclear, model the state machine. It is the safe default, and
59
+ the other views render from the same model afterwards.
60
+
61
+ ### Nothing picks the view for you
62
+
63
+ `render` takes `kind` from the caller and defaults to `state`; it never guesses. A guess
64
+ between state and activity would be a presentation decision, and the machine cannot make
65
+ it: the same guarded branch is a state machine to one reader and an error path to another.
66
+ `parse` does detect the notation and the diagram family from the text, and it refuses the
67
+ families it cannot model, by name.
68
+
69
+ Mechanically decidable, and nothing else:
70
+
71
+ - the notation and the family the text declares — a Mermaid header (`stateDiagram`,
72
+ `flowchart`, `sequenceDiagram`, or a known other-family header) or a PlantUML body
73
+ keyword;
74
+ - whether the requested notation-and-kind combination exists at all (PlantUML has no
75
+ activity view here);
76
+ - whether a view can carry the fidelity check (a sequence cannot).
77
+
78
+ Everything else is a fact about the question, not about the artifact. A model can tell you
79
+ its shape — its state count, its guarded branches, its cycles — and its shape does not
80
+ choose the view.
81
+
82
+ Do not use this guide for a behavioural claim about a specific machine. That is the job of
83
+ `logicprobe_verify` (S1-S8 and A1-A14). The review here covers the modelling, not the
84
+ behaviour. Every finding it produces names the engine check that settles the behavioural
85
+ half.
18
86
 
19
87
  ## The three views
20
88
 
package/src/uml-tool.ts CHANGED
@@ -2,7 +2,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
2
2
  import { createHash } from 'node:crypto'
3
3
  import type { JsonValue } from './json-value.js'
4
4
  import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, verdictOf, verdictOfFindings, type VerdictSummary } from './engine.js'
5
- import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, UmlError, type UmlDiagram, type UmlFinding, type UmlNotation, type UmlParseResult } from './uml.js'
5
+ import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, checkUmlIntent, UmlError, type UmlDiagram, type UmlFinding, type UmlIntent, type UmlNotation, type UmlParseResult } from './uml.js'
6
6
  import { renderReportText } from './report-view.js'
7
7
 
8
8
  export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml'
@@ -22,7 +22,7 @@ export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml'
22
22
  export const logicProbeUmlTool = defineTool({
23
23
  name: LOGICPROBE_UML_TOOL_NAME,
24
24
  description:
25
- '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. action="explain-labels" prints the label and comment conventions the parser accepts, without needing a diagram. 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.',
25
+ '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. action="explain-labels" prints the label and comment conventions the parser accepts, without needing a diagram. 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. Declare `intent` (present, verify, fidelity) to say what the artifact is for: a view that cannot serve it is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes. Read `verdict`, not `ok`: error findings mean a failed review.',
26
26
  parameters: {
27
27
  action: {
28
28
  type: 'string',
@@ -48,6 +48,11 @@ export const logicProbeUmlTool = defineTool({
48
48
  enum: ['state', 'activity', 'sequence'],
49
49
  description: 'Diagram kind to render. Default state. sequence is one BFS trace, not the whole machine. PlantUML activity is refused (its structured-flowchart syntax cannot faithfully carry a graph with merges or cycles) — use mermaid for that view.',
50
50
  },
51
+ intent: {
52
+ type: 'string',
53
+ enum: ['present', 'verify', 'fidelity'],
54
+ description: 'What the artifact is for, declared up front: present = a human will read the diagram (any view), verify = it must be parseable into a LogicModelV1 for the engine (state or activity), fidelity = the point is the render-and-reparse proof (review only, state or activity). A view that cannot serve the intent is refused before any work, with errorCode UML_INTENT_UNSERVABLE, UML_VIEW_UNREPRESENTABLE or UML_INTENT_MISMATCH. Omit it and nothing changes: a sequence view is still rendered, and review still reports UML019.',
55
+ },
51
56
  roundTrip: {
52
57
  type: 'boolean',
53
58
  description: 'review only: render and re-parse the model to prove the diagram carries it. Default true.',
@@ -79,11 +84,12 @@ export const logicProbeUmlTool = defineTool({
79
84
  try {
80
85
  if (args.action === 'render') {
81
86
  if (args.model === undefined) return errorResult('action=render needs `model` (a LogicModelV1 object)')
82
- const result = renderUml(args.model, (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation, (args.kind ?? 'state') as UmlDiagram, args.maxSteps)
87
+ const result = renderUml(args.model, (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation, (args.kind ?? 'state') as UmlDiagram, args.maxSteps, args.intent as UmlIntent | undefined)
83
88
  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
84
89
  }
85
90
  if (args.action === 'parse') {
86
91
  if (args.diagram === undefined) return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)')
92
+ checkUmlIntent(args.intent as UmlIntent | undefined, 'parse')
87
93
  const result = parseUml(args.diagram, args.notation ?? 'auto')
88
94
  // A diagram from another family still parses into *something*. `parseFindings`
89
95
  // says so, and the verdict is what stops that something from being trusted:
@@ -109,6 +115,7 @@ export const logicProbeUmlTool = defineTool({
109
115
  // R9: the label contract was documented in the library and in `explainLabels()`, but no
110
116
  // tool exposed it, so it was reachable only by reading the source or the docs.
111
117
  if (args.action === 'explain-labels') {
118
+ checkUmlIntent(args.intent as UmlIntent | undefined, 'labels')
112
119
  const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation
113
120
  return {
114
121
  ok: true,
@@ -126,6 +133,7 @@ export const logicProbeUmlTool = defineTool({
126
133
  diagramKind: (args.kind ?? 'state') as UmlDiagram,
127
134
  roundTrip: args.roundTrip,
128
135
  maxSteps: args.maxSteps,
136
+ intent: args.intent as UmlIntent | undefined,
129
137
  })
130
138
  return report as unknown as JsonValue
131
139
  } catch (error) {
package/src/uml.ts CHANGED
@@ -42,6 +42,20 @@ export const UML_NOTATIONS: readonly UmlNotation[] = ['mermaid', 'plantuml']
42
42
 
43
43
  export const UML_DIAGRAMS: readonly UmlDiagram[] = ['state', 'activity', 'sequence']
44
44
 
45
+ /**
46
+ * What the caller is going to do with the artifact, declared up front.
47
+ *
48
+ * The view follows the question, not the machine, so the tool cannot pick it. What it
49
+ * can do is refuse a pairing that cannot serve the question — before the work, not as
50
+ * a finding after it.
51
+ */
52
+ export type UmlIntent = 'present' | 'verify' | 'fidelity'
53
+
54
+ export const UML_INTENTS: readonly UmlIntent[] = ['present', 'verify', 'fidelity']
55
+
56
+ /** The tool actions, for the entry check. `labels` is `action=explain-labels`. */
57
+ export type UmlIntentAction = 'render' | 'parse' | 'review' | 'labels'
58
+
45
59
  /** Marker every generated diagram carries; renderers ignore it, the parser uses it. */
46
60
  const DIRECTIVE_NAMESPACE = 'logicprobe:'
47
61
 
@@ -401,26 +415,92 @@ function renderSequence(context: RenderContext, notation: UmlNotation, maxSteps:
401
415
  return { text: lines.join('\n') + '\n', truncated }
402
416
  }
403
417
 
418
+ /** What each action hands back, so a declared intent can be matched against it. */
419
+ const ACTION_PRODUCES: Record<UmlIntentAction, string> = {
420
+ render: 'a diagram',
421
+ parse: 'a LogicModelV1 read out of a diagram',
422
+ review: 'an audit of the modelling',
423
+ labels: 'the label and comment conventions the parser accepts',
424
+ }
425
+
426
+ /**
427
+ * The intents each action can serve.
428
+ *
429
+ * `fidelity` is a review job: render produces a diagram, and a diagram nothing has
430
+ * compared back to its model proves nothing about it.
431
+ */
432
+ const ACTION_INTENTS: Record<UmlIntentAction, readonly UmlIntent[]> = {
433
+ render: ['present', 'verify'],
434
+ parse: ['verify'],
435
+ review: ['present', 'verify', 'fidelity'],
436
+ labels: [],
437
+ }
438
+
439
+ /**
440
+ * The `(notation, view)` cells this module can draw.
441
+ *
442
+ * PlantUML activity is the cell that does not exist: its activity syntax is a
443
+ * structured flowchart language, so a graph with merges or cycles needs a while/if
444
+ * reconstruction this module does not perform. Refusing is the honest outcome —
445
+ * quietly emitting a state diagram under an "activity" request would mislabel the
446
+ * model.
447
+ */
448
+ export function assertUmlView(notation: UmlNotation, diagram: UmlDiagram): void {
449
+ if (!UML_NOTATIONS.includes(notation)) throw new UmlError('unknown notation "' + String(notation) + '"; expected ' + UML_NOTATIONS.join(' | '))
450
+ if (!UML_DIAGRAMS.includes(diagram)) throw new UmlError('unknown diagram "' + String(diagram) + '"; expected ' + UML_DIAGRAMS.join(' | '))
451
+ if (notation === 'plantuml' && diagram === 'activity') {
452
+ throw new UmlError('plantuml has no faithful activity view here (its activity syntax is a structured flowchart language; a graph with merges or cycles needs a while/if reconstruction logicprobe does not perform) — use notation "mermaid" for the activity view, or diagram "state"', 'UML_VIEW_UNREPRESENTABLE')
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Refuse, before any work, a request whose declared purpose the view cannot serve.
458
+ *
459
+ * Without an intent this changes nothing: the review still reports `UML019` when the
460
+ * fidelity check does not apply to the view it was given. With one, an impossible
461
+ * pairing is an error carrying a code, instead of a report about the wrong artifact —
462
+ * which is the failure this check exists to prevent.
463
+ *
464
+ * @param intent - the declared purpose, or `undefined` to leave the behaviour unchanged.
465
+ * @param action - which action is about to run.
466
+ * @param view - the requested `(notation, diagram)`, for the actions that have one.
467
+ */
468
+ export function checkUmlIntent(intent: UmlIntent | undefined, action: UmlIntentAction, view?: { notation: UmlNotation; diagram: UmlDiagram }): void {
469
+ if (intent === undefined) return
470
+ if (!UML_INTENTS.includes(intent)) throw new UmlError('unknown intent "' + String(intent) + '"; expected ' + UML_INTENTS.join(' | '))
471
+ const served = ACTION_INTENTS[action]
472
+ if (!served.includes(intent)) {
473
+ throw new UmlError(
474
+ 'intent "' + intent + '" cannot be served by action=' + action + ', which returns ' + ACTION_PRODUCES[action] + '; it serves ' +
475
+ (served.length === 0 ? 'no intent — leave `intent` off' : served.join(' | ')),
476
+ 'UML_INTENT_MISMATCH')
477
+ }
478
+ if (view === undefined) return
479
+ assertUmlView(view.notation, view.diagram)
480
+ if (intent === 'present' || view.diagram !== 'sequence') return
481
+ throw new UmlError(
482
+ 'intent "' + intent + '" needs a machine, and a sequence view is one BFS trace: ' +
483
+ (intent === 'fidelity'
484
+ ? 'the render/parse fidelity check does not apply to a trace, so nothing would be proven. Review kind "state" or "activity" to have the diagram checked against the model, and render the trace separately with intent "present".'
485
+ : '`parse` refuses a trace because it cannot reconstruct a machine, so the diagram cannot feed verification. Model the machine as a state or activity diagram, and render the trace separately with intent "present".'),
486
+ 'UML_INTENT_UNSERVABLE')
487
+ }
488
+
404
489
  /**
405
490
  * Render a LogicModelV1 as UML.
406
491
  *
407
- * PlantUML has no faithful activity view here: its activity syntax is a
408
- * structured flowchart language, so a graph with merges or cycles needs a
409
- * while/if reconstruction this module does not perform. Refusing is the honest
410
- * outcome — quietly emitting a state diagram under an "activity" request would
411
- * mislabel the model. Mermaid covers all three views.
492
+ * Mermaid covers all three views; PlantUML covers state and sequence (see
493
+ * `assertUmlView` for why activity is refused).
412
494
  *
413
495
  * @param input - candidate LogicModelV1.
414
496
  * @param notation - `mermaid` (default) or `plantuml`.
415
497
  * @param diagram - `state` (default), `activity`, or `sequence`.
416
498
  * @param maxSteps - cap on the sequence trace length.
499
+ * @param intent - what the diagram is for; a view that cannot serve it is refused first.
417
500
  */
418
- export function renderUml(input: unknown, notation: UmlNotation = 'mermaid', diagram: UmlDiagram = 'state', maxSteps = 60): UmlRenderResult {
419
- if (!UML_NOTATIONS.includes(notation)) throw new UmlError('unknown notation "' + String(notation) + '"; expected ' + UML_NOTATIONS.join(' | '))
420
- if (!UML_DIAGRAMS.includes(diagram)) throw new UmlError('unknown diagram "' + String(diagram) + '"; expected ' + UML_DIAGRAMS.join(' | '))
421
- if (notation === 'plantuml' && diagram === 'activity') {
422
- throw new UmlError('plantuml has no faithful activity view here (its activity syntax is a structured flowchart language; a graph with merges or cycles needs a while/if reconstruction logicprobe does not perform) — use notation "mermaid" for the activity view, or diagram "state"')
423
- }
501
+ export function renderUml(input: unknown, notation: UmlNotation = 'mermaid', diagram: UmlDiagram = 'state', maxSteps = 60, intent?: UmlIntent): UmlRenderResult {
502
+ assertUmlView(notation, diagram)
503
+ checkUmlIntent(intent, 'render', { notation, diagram })
424
504
  const context = prepareRender(input)
425
505
  const warnings = [...context.warnings]
426
506
  let primary: string
@@ -1524,6 +1604,8 @@ export interface UmlReviewOptions {
1524
1604
  roundTrip?: boolean
1525
1605
  /** Cap on the sequence trace used for the fidelity check. */
1526
1606
  maxSteps?: number
1607
+ /** What the review is for; a view that cannot serve it is refused before any work. */
1608
+ intent?: UmlIntent
1527
1609
  }
1528
1610
 
1529
1611
  /**
@@ -1549,6 +1631,7 @@ export function reviewUml(options: UmlReviewOptions): UmlReviewReport {
1549
1631
 
1550
1632
  let notation: UmlNotation = options.notation === undefined || options.notation === 'auto' ? 'mermaid' : options.notation
1551
1633
  const kind: UmlDiagram = options.diagramKind ?? 'state'
1634
+ checkUmlIntent(options.intent, 'review', { notation, diagram: kind })
1552
1635
  let model: LogicModelV1 | undefined
1553
1636
  let labels: Record<string, string> | undefined
1554
1637
  let parsed: UmlParseResult | undefined