dsh-logicprobe 1.1.0 → 1.2.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * logicprobe — DeepSeek Harness native plugin for the Logic Probe toolbox.
3
- * Injects the session-start gate text (what this plugin checks, the 1% Rule,
4
- * the Red Flags, the routing to the skills, the tool list, how to read a report)
3
+ * Injects the session-start gate text (what this plugin checks, the routing to
4
+ * the skills, the tool list, how to read a report)
5
5
  * into the first model step of every agent session, mirroring the SessionStart
6
6
  * hook the Claude Code plugin installs.
7
7
  * The skill ships in this package's `skills/` directory and is registered at
@@ -18,8 +18,9 @@
18
18
  * to the first step after their promotion, and the history guard re-injects it
19
19
  * there. The default gate text is the dsh-native adaptation of
20
20
  * `hooks/session-start-content.md`. Both are a guide to calling the tools: what
21
- * this plugin checks, the 1% Rule and the Red Flags that raise the chance of a
22
- * call, the routing to the five skills, the tool list, and how to read a report.
21
+ * this plugin checks, the routing to the sibling skills, the tool list, and how to
22
+ * read a report. The Red Flags table is enforcement scaffolding and lives in the
23
+ * skill body instead, where a session that needs it can load it.
23
24
  * They stay in sync. Method, doctrine and per-domain detail live in the skills
24
25
  * and their references, not here. Deployments override via Config.
25
26
  *
@@ -0,0 +1,43 @@
1
+ /** Rendering limits. Defaults are the shipped behaviour; tests pin them. */
2
+ export interface ReportViewOptions {
3
+ /** Maximum finding lines per check. Error findings are never dropped, only warnings are capped. */
4
+ maxFindingsPerCheck?: number;
5
+ /** Evidence is inlined for `error` findings only, truncated to this many characters. */
6
+ maxEvidenceChars?: number;
7
+ /** Include passing checks' bodies (default false: a passing check carries no actionable finding). */
8
+ includePassing?: boolean;
9
+ /**
10
+ * Print the whole structured report instead of the body. This is the escape hatch for a
11
+ * caller that must hand the report to another tool verbatim — `logicprobe_report_diff`
12
+ * matches on check + code + evidence, so a body cannot stand in for the object.
13
+ */
14
+ full?: boolean;
15
+ }
16
+ type Record_ = Record<string, unknown>;
17
+ /** One normalized finding. `evidence` is kept because a finding's identity depends on it. */
18
+ export interface ViewFinding {
19
+ code: string;
20
+ severity: string;
21
+ message: string;
22
+ evidence?: Record_;
23
+ file?: string;
24
+ line?: number;
25
+ check?: string;
26
+ }
27
+ /** Normalize one raw finding. Unknown fields are ignored; a missing message renders as the code. */
28
+ export declare function toViewFinding(value: unknown, check?: string): ViewFinding;
29
+ /**
30
+ * The fact a finding states, as a comparable key. The verify/datamodel families report the
31
+ * same (state, event) fact from more than one check, so the key ignores which check said it;
32
+ * anything else falls back to code + message.
33
+ */
34
+ export declare function factKey(finding: ViewFinding): string;
35
+ /**
36
+ * Render any logicprobe result as the text a model reads.
37
+ *
38
+ * Order of recognition: a `checks[]` report, a flat `findings[]` report, a baseline delta,
39
+ * a payload result (`diagram`/`primary`), and finally compact JSON so an unrecognized shape
40
+ * is still readable rather than silently empty.
41
+ */
42
+ export declare function renderReportText(value: unknown, options?: ReportViewOptions): string;
43
+ export {};
package/lib/uml-tool.js CHANGED
@@ -1,146 +1,145 @@
1
- import { defineTool } from '@deepseek-ai/dsh-tools';
2
- import { createHash } from 'node:crypto';
3
- import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, verdictOf, verdictOfFindings } from './engine.js';
4
- import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, UmlError } from './uml.js';
5
- export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml';
6
- /**
7
- * DSH tool wrapping the UML front end: model a code flow as a UML diagram
8
- * (`action: "render"`), read a UML diagram back into a LogicModelV1
9
- * (`action: "parse"`), or review the modelling itself (`action: "review"`).
10
- *
11
- * review is the half that makes the feature a check rather than a drawing
12
- * utility: it reports structural defects the diagram would present as valid
13
- * flow (unreachable states, dead ends, ambiguous and non-exhaustive branches,
14
- * self-loops with no exit), documentation gaps (a symbol no reader can map back
15
- * to code), and — the fidelity check — whether the rendered diagram reads back
16
- * as the model it was drawn from.
17
- */
18
- export const logicProbeUmlTool = defineTool({
19
- name: LOGICPROBE_UML_TOOL_NAME,
20
- 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. Read `verdict`, not `ok`: error findings mean a failed review.',
21
- parameters: {
22
- action: {
23
- type: 'string',
24
- required: true,
25
- enum: ['render', 'parse', 'review', 'explain-labels'],
26
- 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.',
27
- },
28
- model: {
29
- type: 'json',
30
- description: 'LogicModelV1 machine. Required for render; accepted by review (alone, or next to diagram to check the two against each other).',
31
- },
32
- diagram: {
33
- type: 'string',
34
- description: 'UML source text. Required for parse; accepted by review.',
35
- },
36
- notation: {
37
- type: 'string',
38
- enum: ['auto', 'mermaid', 'plantuml'],
39
- description: 'Diagram language. Default auto: detected from the text for parse/review, mermaid for render.',
40
- },
41
- kind: {
42
- type: 'string',
43
- enum: ['state', 'activity', 'sequence'],
44
- 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.',
45
- },
46
- roundTrip: {
47
- type: 'boolean',
48
- description: 'review only: render and re-parse the model to prove the diagram carries it. Default true.',
49
- },
50
- maxSteps: {
51
- type: 'integer',
52
- description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.',
53
- },
54
- },
55
- output: {
56
- schema: {
57
- type: 'json',
58
- description: 'Render result (diagram source), parse result (LogicModelV1 + labels), or the modelling review report with findings, round-trip diff and next steps.',
59
- },
60
- render(_args, value) {
61
- const record = value;
62
- // A rendered diagram is source text: print it verbatim so it can be copied
63
- // into a file, then the structured result (warnings, notation, kind).
64
- if (typeof record.diagram === 'string') {
65
- return [
66
- { type: 'text', text: record.diagram },
67
- { type: 'text', text: JSON.stringify(value, null, 2) },
68
- ];
69
- }
70
- return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
71
- },
72
- },
73
- timeoutMs: 10_000,
74
- isConcurrencySafe: () => true,
75
- async execute(args) {
76
- try {
77
- if (args.action === 'render') {
78
- if (args.model === undefined)
79
- return errorResult('action=render needs `model` (a LogicModelV1 object)');
80
- const result = renderUml(args.model, (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation), (args.kind ?? 'state'), args.maxSteps);
81
- 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 };
82
- }
83
- if (args.action === 'parse') {
84
- if (args.diagram === undefined)
85
- return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)');
86
- const result = parseUml(args.diagram, args.notation ?? 'auto');
87
- // A diagram from another family still parses into *something*. `parseFindings`
88
- // says so, and the verdict is what stops that something from being trusted:
89
- // `ok: true` only means the parser ran.
90
- const findings = parseFindings(result);
91
- return {
92
- ok: true,
93
- ran: true,
94
- ...parseVerdict(result, findings),
95
- schema: REPORT_SCHEMAS.umlParse,
96
- action: 'parse',
97
- notation: result.notation,
98
- kind: result.diagram,
99
- model: result.model,
100
- labels: result.labels,
101
- findings,
102
- discardedConstructs: result.discardedConstructs,
103
- discardedEdges: result.discardedEdges,
104
- hashes: { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(result.model), diagram: createHash('sha256').update(args.diagram).digest('hex') },
105
- warnings: result.warnings,
106
- };
107
- }
108
- // R9: the label contract was documented in the library and in `explainLabels()`, but no
109
- // tool exposed it, so it was reachable only by reading the source or the docs.
110
- if (args.action === 'explain-labels') {
111
- const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation);
112
- return {
113
- ok: true,
114
- ran: true,
115
- ...verdictOf(0, 0),
116
- schema: REPORT_SCHEMAS.umlExplainLabels,
117
- action: 'explain-labels',
118
- ...explainLabels(notation),
119
- };
120
- }
121
- const report = reviewUml({
122
- model: args.model,
123
- diagram: args.diagram,
124
- notation: args.notation ?? 'auto',
125
- diagramKind: (args.kind ?? 'state'),
126
- roundTrip: args.roundTrip,
127
- maxSteps: args.maxSteps,
128
- });
129
- return report;
130
- }
131
- catch (error) {
132
- const message = error instanceof Error ? error.message : String(error);
133
- return errorResult(message, error instanceof UmlError ? error.code : undefined);
134
- }
135
- },
136
- });
137
- /** The parse verdict counts parser notes as findings: an ignored line is information the model does not carry. */
138
- function parseVerdict(result, findings) {
139
- const errors = findings.filter((finding) => finding.severity === 'error').length;
140
- const warnings = findings.filter((finding) => finding.severity === 'warning').length + result.warnings.length;
141
- const firstCode = findings.find((finding) => finding.severity === 'error')?.code ?? findings.find((finding) => finding.severity === 'warning')?.code;
142
- return verdictOf(errors, warnings, firstCode);
143
- }
144
- function errorResult(message, errorCode = 'UML_INPUT') {
145
- return { ok: false, ran: false, ...refusalVerdict(message), errorCode, error: message };
146
- }
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { createHash } from 'node:crypto';
3
+ import { REPORT_SCHEMAS, DEFAULT_HASH_SPEC, modelHash, refusalVerdict, verdictOf, verdictOfFindings } from './engine.js';
4
+ import { renderUml, parseUml, parseFindings, reviewUml, explainLabels, UmlError } from './uml.js';
5
+ import { renderReportText } from './report-view.js';
6
+ export const LOGICPROBE_UML_TOOL_NAME = 'logicprobe_uml';
7
+ /**
8
+ * DSH tool wrapping the UML front end: model a code flow as a UML diagram
9
+ * (`action: "render"`), read a UML diagram back into a LogicModelV1
10
+ * (`action: "parse"`), or review the modelling itself (`action: "review"`).
11
+ *
12
+ * review is the half that makes the feature a check rather than a drawing
13
+ * utility: it reports structural defects the diagram would present as valid
14
+ * flow (unreachable states, dead ends, ambiguous and non-exhaustive branches,
15
+ * self-loops with no exit), documentation gaps (a symbol no reader can map back
16
+ * to code), and — the fidelity check — whether the rendered diagram reads back
17
+ * as the model it was drawn from.
18
+ */
19
+ export const logicProbeUmlTool = defineTool({
20
+ name: LOGICPROBE_UML_TOOL_NAME,
21
+ 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. Read `verdict`, not `ok`: error findings mean a failed review.',
22
+ parameters: {
23
+ action: {
24
+ type: 'string',
25
+ required: true,
26
+ enum: ['render', 'parse', 'review', 'explain-labels'],
27
+ 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.',
28
+ },
29
+ model: {
30
+ type: 'json',
31
+ description: 'LogicModelV1 machine. Required for render; accepted by review (alone, or next to diagram to check the two against each other).',
32
+ },
33
+ diagram: {
34
+ type: 'string',
35
+ description: 'UML source text. Required for parse; accepted by review.',
36
+ },
37
+ notation: {
38
+ type: 'string',
39
+ enum: ['auto', 'mermaid', 'plantuml'],
40
+ description: 'Diagram language. Default auto: detected from the text for parse/review, mermaid for render.',
41
+ },
42
+ kind: {
43
+ type: 'string',
44
+ enum: ['state', 'activity', 'sequence'],
45
+ 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.',
46
+ },
47
+ roundTrip: {
48
+ type: 'boolean',
49
+ description: 'review only: render and re-parse the model to prove the diagram carries it. Default true.',
50
+ },
51
+ maxSteps: {
52
+ type: 'integer',
53
+ description: 'Cap on the sequence trace length when rendering kind=sequence. Default 60.',
54
+ },
55
+ reportView: {
56
+ type: 'string',
57
+ enum: ['digest', 'full'],
58
+ description: 'digest (default) prints the diagram or report body; full prints the structured result as JSON.',
59
+ },
60
+ },
61
+ output: {
62
+ schema: {
63
+ type: 'json',
64
+ description: 'Render result (diagram source), parse result (LogicModelV1 + labels), or the modelling review report with findings, round-trip diff and next steps.',
65
+ },
66
+ render(args, value) {
67
+ // A rendered diagram is source text: the report body prints it verbatim, once.
68
+ // Echoing the whole result as JSON printed the same diagram a second time, escaped.
69
+ return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
70
+ },
71
+ },
72
+ timeoutMs: 10_000,
73
+ isConcurrencySafe: () => true,
74
+ async execute(args) {
75
+ try {
76
+ if (args.action === 'render') {
77
+ if (args.model === undefined)
78
+ return errorResult('action=render needs `model` (a LogicModelV1 object)');
79
+ const result = renderUml(args.model, (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation), (args.kind ?? 'state'), args.maxSteps);
80
+ 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 };
81
+ }
82
+ if (args.action === 'parse') {
83
+ if (args.diagram === undefined)
84
+ return errorResult('action=parse needs `diagram` (Mermaid or PlantUML text)');
85
+ const result = parseUml(args.diagram, args.notation ?? 'auto');
86
+ // A diagram from another family still parses into *something*. `parseFindings`
87
+ // says so, and the verdict is what stops that something from being trusted:
88
+ // `ok: true` only means the parser ran.
89
+ const findings = parseFindings(result);
90
+ return {
91
+ ok: true,
92
+ ran: true,
93
+ ...parseVerdict(result, findings),
94
+ schema: REPORT_SCHEMAS.umlParse,
95
+ action: 'parse',
96
+ notation: result.notation,
97
+ kind: result.diagram,
98
+ model: result.model,
99
+ labels: result.labels,
100
+ findings,
101
+ discardedConstructs: result.discardedConstructs,
102
+ discardedEdges: result.discardedEdges,
103
+ hashes: { hashSpec: DEFAULT_HASH_SPEC, modelHash: modelHash(result.model), diagram: createHash('sha256').update(args.diagram).digest('hex') },
104
+ warnings: result.warnings,
105
+ };
106
+ }
107
+ // R9: the label contract was documented in the library and in `explainLabels()`, but no
108
+ // tool exposed it, so it was reachable only by reading the source or the docs.
109
+ if (args.action === 'explain-labels') {
110
+ const notation = (args.notation === undefined || args.notation === 'auto' ? 'mermaid' : args.notation);
111
+ return {
112
+ ok: true,
113
+ ran: true,
114
+ ...verdictOf(0, 0),
115
+ schema: REPORT_SCHEMAS.umlExplainLabels,
116
+ action: 'explain-labels',
117
+ ...explainLabels(notation),
118
+ };
119
+ }
120
+ const report = reviewUml({
121
+ model: args.model,
122
+ diagram: args.diagram,
123
+ notation: args.notation ?? 'auto',
124
+ diagramKind: (args.kind ?? 'state'),
125
+ roundTrip: args.roundTrip,
126
+ maxSteps: args.maxSteps,
127
+ });
128
+ return report;
129
+ }
130
+ catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
132
+ return errorResult(message, error instanceof UmlError ? error.code : undefined);
133
+ }
134
+ },
135
+ });
136
+ /** The parse verdict counts parser notes as findings: an ignored line is information the model does not carry. */
137
+ function parseVerdict(result, findings) {
138
+ const errors = findings.filter((finding) => finding.severity === 'error').length;
139
+ const warnings = findings.filter((finding) => finding.severity === 'warning').length + result.warnings.length;
140
+ const firstCode = findings.find((finding) => finding.severity === 'error')?.code ?? findings.find((finding) => finding.severity === 'warning')?.code;
141
+ return verdictOf(errors, warnings, firstCode);
142
+ }
143
+ function errorResult(message, errorCode = 'UML_INPUT') {
144
+ return { ok: false, ran: false, ...refusalVerdict(message), errorCode, error: message };
145
+ }