import { defineTool } from '@deepseek-ai/dsh-tools' import { createHash } from 'node:crypto' import type { JsonValue } from './json-value.js' import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, verdictOf, verdictOfFindings, type VerdictSummary } from './engine.js' import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, checkUmlIntent, UmlError, type UmlDiagram, type UmlFinding, type UmlIntent, type UmlNotation, type UmlParseResult } from './uml.js' import { renderReportText } from './report-view.js' export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml' /** * DSH tool wrapping the UML front end: model a code flow as a UML diagram * (`action: "render"`), read a UML diagram back into a LogicModelV1 * (`action: "parse"`), or review the modelling itself (`action: "review"`). * * review is the half that makes the feature a check rather than a drawing * utility: it reports structural defects the diagram would present as valid * flow (unreachable states, dead ends, ambiguous and non-exhaustive branches, * self-loops with no exit), documentation gaps (a symbol no reader can map back * to code), and — the fidelity check — whether the rendered diagram reads back * as the model it was drawn from. */ export const logicProbeUmlTool = defineTool({ name: LOGICPROBE_UML_TOOL_NAME, description: '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.', parameters: { action: { type: 'string', required: true, enum: ['render', 'parse', 'review', 'explain-labels'], description: 'render = model → UML source; parse = UML source → model; review = audit the modelling (and its fidelity to the model); explain-labels = print the label and comment conventions this parser accepts.', }, model: { type: 'json', description: 'LogicModelV1 machine. Required for render; accepted by review (alone, or next to diagram to check the two against each other).', }, diagram: { type: 'string', description: 'UML source text. Required for parse; accepted by review.', }, notation: { type: 'string', enum: ['auto', 'mermaid', 'plantuml'], description: 'Diagram language. Default auto: detected from the text for parse/review, mermaid for render.', }, kind: { type: 'string', enum: ['state', 'activity', 'sequence'], 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.', }, intent: { type: 'string', enum: ['present', 'verify', 'fidelity'], 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.', }, roundTrip: { type: 'boolean', description: 'review only: render and re-parse the model to prove the diagram carries it. Default true.', }, maxSteps: { type: 'integer', description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.', }, reportView: { type: 'string', enum: ['digest', 'full'], description: 'digest (default) prints the diagram or report body; full prints the structured result as JSON.', }, }, output: { schema: { type: 'json', description: 'Render result (diagram source), parse result (LogicModelV1 + labels), or the modelling review report with findings, round-trip diff and next steps.', }, render(args, value) { // A rendered diagram is source text: the report body prints it verbatim, once. // Echoing the whole result as JSON printed the same diagram a second time, escaped. return [{ type: 'text' as const, text: renderReportText(value, { full: args.reportView === 'full' }) }] }, }, timeoutMs: 10_000, isConcurrencySafe: () => true, async execute(args) { try { if (args.action === 'render') { if (args.model === undefined) return errorResult('action=render needs `model` (a LogicModelV1 object)') 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) 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 } if (args.action === 'parse') { if (args.diagram === undefined) return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)') checkUmlIntent(args.intent as UmlIntent | undefined, 'parse') const result = parseUml(args.diagram, args.notation ?? 'auto') // A diagram from another family still parses into *something*. `parseFindings` // says so, and the verdict is what stops that something from being trusted: // `ok: true` only means the parser ran. const findings = parseFindings(result) return { ok: true, ran: true, ...parseVerdict(result, findings), schema: REPORT_SCHEMAS.umlParse, action: 'parse', notation: result.notation, kind: result.diagram, model: result.model, labels: result.labels, findings, discardedConstructs: result.discardedConstructs, discardedEdges: result.discardedEdges, hashes: { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(result.model), diagram: createHash('sha256').update(args.diagram).digest('hex') }, warnings: result.warnings, } as unknown as JsonValue } // R9: the label contract was documented in the library and in `explainLabels()`, but no // tool exposed it, so it was reachable only by reading the source or the docs. if (args.action === 'explain-labels') { checkUmlIntent(args.intent as UmlIntent | undefined, 'labels') const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation) as UmlNotation return { ok: true, ran: true, ...verdictOf(0, 0), schema: REPORT_SCHEMAS.umlExplainLabels, action: 'explain-labels', ...explainLabels(notation), } as unknown as JsonValue } const report = reviewUml({ model: args.model, diagram: args.diagram, notation: args.notation ?? 'auto', diagramKind: (args.kind ?? 'state') as UmlDiagram, roundTrip: args.roundTrip, maxSteps: args.maxSteps, intent: args.intent as UmlIntent | undefined, }) return report as unknown as JsonValue } catch (error) { const message = error instanceof Error ? error.message : String(error) return errorResult(message, error instanceof UmlError ? error.code : undefined) as unknown as JsonValue } }, }) /** The parse verdict counts parser notes as findings: an ignored line is information the model does not carry. */ function parseVerdict(result: UmlParseResult, findings: UmlFinding[]): VerdictSummary { const errors = findings.filter((finding) => finding.severity === 'error').length const warnings = findings.filter((finding) => finding.severity === 'warning').length + result.warnings.length const firstCode = findings.find((finding) => finding.severity === 'error')?.code ?? findings.find((finding) => finding.severity === 'warning')?.code return verdictOf(errors, warnings, firstCode) } function errorResult(message: string, errorCode = 'UML_INPUT'): JsonValue { return { ok: false, ran: false, ...refusalVerdict(message), errorCode, error: message } as unknown as JsonValue }