dsh-logicprobe 1.1.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.
Files changed (39) hide show
  1. package/README.en-US.md +17 -6
  2. package/README.md +17 -6
  3. package/lib/compose-tool.js +8 -2
  4. package/lib/concurrency-tool.js +8 -2
  5. package/lib/data-tool.js +8 -2
  6. package/lib/engine.js +3151 -3151
  7. package/lib/export-tool.js +8 -2
  8. package/lib/index.js +28 -40
  9. package/lib/report-diff-tool.js +8 -2
  10. package/lib/report-view.js +295 -0
  11. package/lib/structure-tool.js +8 -2
  12. package/lib/tool.js +8 -2
  13. package/lib/types/engine.d.ts +419 -419
  14. package/lib/types/index.d.ts +5 -4
  15. package/lib/types/report-view.d.ts +43 -0
  16. package/lib/types/uml.d.ts +293 -256
  17. package/lib/uml-tool.js +21 -14
  18. package/lib/uml.js +76 -12
  19. package/package.json +2 -2
  20. package/skills/logicprobe/SKILL.md +42 -6
  21. package/skills/logicprobe/references/dsh-model-schema.md +1 -1
  22. package/skills/logicprobe/references/gap-routing-guide.md +10 -9
  23. package/skills/logicprobe/references/logic-verification-guide.md +1 -1
  24. package/skills/logicprobe-concurrency/SKILL.md +2 -0
  25. package/skills/logicprobe-datamodel/SKILL.md +5 -0
  26. package/skills/logicprobe-structure/SKILL.md +1 -0
  27. package/skills/logicprobe-uml/SKILL.md +3 -0
  28. package/skills/logicprobe-uml/references/uml-modeling-guide.md +70 -2
  29. package/src/compose-tool.ts +8 -2
  30. package/src/concurrency-tool.ts +8 -2
  31. package/src/data-tool.ts +9 -3
  32. package/src/export-tool.ts +8 -2
  33. package/src/index.ts +28 -40
  34. package/src/report-diff-tool.ts +8 -2
  35. package/src/report-view.ts +343 -0
  36. package/src/structure-tool.ts +8 -2
  37. package/src/tool.ts +8 -2
  38. package/src/uml-tool.ts +21 -14
  39. package/src/uml.ts +94 -11
package/src/uml-tool.ts CHANGED
@@ -2,7 +2,8 @@ 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
+ import { renderReportText } from './report-view.js'
6
7
 
7
8
  export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml'
8
9
 
@@ -21,7 +22,7 @@ export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml'
21
22
  export const logicProbeUmlTool = defineTool({
22
23
  name: LOGICPROBE_UML_TOOL_NAME,
23
24
  description:
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. 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.',
25
26
  parameters: {
26
27
  action: {
27
28
  type: 'string',
@@ -47,6 +48,11 @@ export const logicProbeUmlTool = defineTool({
47
48
  enum: ['state', 'activity', 'sequence'],
48
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.',
49
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
+ },
50
56
  roundTrip: {
51
57
  type: 'boolean',
52
58
  description: 'review only: render and re-parse the model to prove the diagram carries it. Default true.',
@@ -55,23 +61,21 @@ export const logicProbeUmlTool = defineTool({
55
61
  type: 'integer',
56
62
  description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.',
57
63
  },
64
+ reportView: {
65
+ type: 'string',
66
+ enum: ['digest', 'full'],
67
+ description: 'digest (default) prints the diagram or report body; full prints the structured result as JSON.',
68
+ },
58
69
  },
59
70
  output: {
60
71
  schema: {
61
72
  type: 'json',
62
73
  description: 'Render result (diagram source), parse result (LogicModelV1 + labels), or the modelling review report with findings, round-trip diff and next steps.',
63
74
  },
64
- render(_args, value) {
65
- const record = value as unknown as Record<string, unknown>
66
- // A rendered diagram is source text: print it verbatim so it can be copied
67
- // into a file, then the structured result (warnings, notation, kind).
68
- if (typeof record.diagram === 'string') {
69
- return [
70
- { type: 'text' as const, text: record.diagram },
71
- { type: 'text' as const, text: JSON.stringify(value, null, 2) },
72
- ]
73
- }
74
- return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
75
+ render(args, value) {
76
+ // A rendered diagram is source text: the report body prints it verbatim, once.
77
+ // Echoing the whole result as JSON printed the same diagram a second time, escaped.
78
+ return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }]
75
79
  },
76
80
  },
77
81
  timeoutMs: 10_000,
@@ -80,11 +84,12 @@ export const logicProbeUmlTool = defineTool({
80
84
  try {
81
85
  if (args.action === 'render') {
82
86
  if (args.model === undefined) return errorResult('action=render needs `model` (a LogicModelV1 object)')
83
- 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)
84
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
85
89
  }
86
90
  if (args.action === 'parse') {
87
91
  if (args.diagram === undefined) return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)')
92
+ checkUmlIntent(args.intent as UmlIntent | undefined, 'parse')
88
93
  const result = parseUml(args.diagram, args.notation ?? 'auto')
89
94
  // A diagram from another family still parses into *something*. `parseFindings`
90
95
  // says so, and the verdict is what stops that something from being trusted:
@@ -110,6 +115,7 @@ export const logicProbeUmlTool = defineTool({
110
115
  // R9: the label contract was documented in the library and in `explainLabels()`, but no
111
116
  // tool exposed it, so it was reachable only by reading the source or the docs.
112
117
  if (args.action === 'explain-labels') {
118
+ checkUmlIntent(args.intent as UmlIntent | undefined, 'labels')
113
119
  const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation
114
120
  return {
115
121
  ok: true,
@@ -127,6 +133,7 @@ export const logicProbeUmlTool = defineTool({
127
133
  diagramKind: (args.kind ?? 'state') as UmlDiagram,
128
134
  roundTrip: args.roundTrip,
129
135
  maxSteps: args.maxSteps,
136
+ intent: args.intent as UmlIntent | undefined,
130
137
  })
131
138
  return report as unknown as JsonValue
132
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