dsh-logicprobe 0.9.0 → 1.1.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 +128 -66
- package/README.md +128 -66
- 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 +3151 -2801
- 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 +419 -277
- 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 +146 -104
- package/lib/uml.js +1748 -1544
- 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 +102 -10
- package/skills/logicprobe/references/gap-routing-guide.md +37 -36
- package/skills/logicprobe/references/hash-spec.md +153 -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 +57 -0
- package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +121 -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 +503 -72
- 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 +53 -10
- package/src/uml.ts +288 -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/baseline.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report diffing: "did this slice add findings, or remove them?"
|
|
3
|
+
*
|
|
4
|
+
* The acceptance criterion of a refactoring slice is usually *not* "zero findings" —
|
|
5
|
+
* it is "no NEW findings, and none of the old ones got worse". Comparing two JSON
|
|
6
|
+
* reports by hand is how that gets fudged, so the comparison is code.
|
|
7
|
+
*
|
|
8
|
+
* A finding's identity is its machine-readable locator, never its prose:
|
|
9
|
+
* `check|code|<canonical evidence + path>`. Findings that carry neither evidence nor a
|
|
10
|
+
* path fall back to their message, because an engine that reports nothing structured
|
|
11
|
+
* has nothing stabler to match on. Two runs over the same input must produce zero
|
|
12
|
+
* added and zero removed findings.
|
|
13
|
+
*
|
|
14
|
+
* @module logicprobe-baseline
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { canonicalJson, REPORT_SCHEMAS, verdictOf, type Finding, type Verdict, type VerdictSummary } from './engine.js'
|
|
18
|
+
|
|
19
|
+
/** A finding as it appears in any report family, after flattening. */
|
|
20
|
+
export interface ReportFinding extends Finding {
|
|
21
|
+
/** The check that emitted it (verify/datamodel: `S2`, `C1`, …); absent for flat families. */
|
|
22
|
+
check?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BaselineChange {
|
|
26
|
+
identity: string
|
|
27
|
+
code: string
|
|
28
|
+
before: { severity: string; message: string }
|
|
29
|
+
after: { severity: string; message: string }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BaselineReport {
|
|
33
|
+
ok: boolean
|
|
34
|
+
ran: boolean
|
|
35
|
+
verdict: Verdict
|
|
36
|
+
verdictReason: string
|
|
37
|
+
/** The versioned report contract this result follows. */
|
|
38
|
+
schema: string
|
|
39
|
+
/** The current report's own verdict, echoed so "no new findings" cannot hide a failing run. */
|
|
40
|
+
currentVerdict: Verdict
|
|
41
|
+
added: ReportFinding[]
|
|
42
|
+
removed: ReportFinding[]
|
|
43
|
+
changed: BaselineChange[]
|
|
44
|
+
summary: {
|
|
45
|
+
baseline: number
|
|
46
|
+
current: number
|
|
47
|
+
added: number
|
|
48
|
+
removed: number
|
|
49
|
+
changed: number
|
|
50
|
+
addedErrors: number
|
|
51
|
+
addedWarnings: number
|
|
52
|
+
}
|
|
53
|
+
nextSteps: string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The stable identity of a finding: check + file + code + a canonical locator. Prose is
|
|
58
|
+
* excluded so a reworded message reads as `changed`, not as a new finding.
|
|
59
|
+
*/
|
|
60
|
+
export function findingIdentity(finding: ReportFinding): string {
|
|
61
|
+
const locator = canonicalJson({
|
|
62
|
+
...(finding.evidence === undefined ? {} : { evidence: finding.evidence }),
|
|
63
|
+
...(finding.path === undefined ? {} : { path: finding.path }),
|
|
64
|
+
})
|
|
65
|
+
const where = locator === '{}' ? 'message:' + finding.message : locator
|
|
66
|
+
const file = finding.file === undefined ? '' : finding.file + '|'
|
|
67
|
+
return (finding.check === undefined ? '' : finding.check + '|') + file + finding.code + '|' + where
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Flatten a report of any family into one finding list (`checks[].findings` or `findings[]`). */
|
|
71
|
+
export function flattenReportFindings(report: unknown): ReportFinding[] {
|
|
72
|
+
if (report === null || typeof report !== 'object') return []
|
|
73
|
+
const record = report as Record<string, unknown>
|
|
74
|
+
const flat: ReportFinding[] = []
|
|
75
|
+
if (Array.isArray(record.checks)) {
|
|
76
|
+
for (const check of record.checks as Array<Record<string, unknown>>) {
|
|
77
|
+
const id = typeof check.id === 'string' ? check.id : undefined
|
|
78
|
+
for (const finding of (Array.isArray(check.findings) ? check.findings : []) as Finding[]) {
|
|
79
|
+
flat.push(id === undefined ? finding : { ...finding, check: id })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(record.findings)) {
|
|
84
|
+
for (const finding of record.findings as Finding[]) flat.push(finding)
|
|
85
|
+
}
|
|
86
|
+
return flat
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The report's own verdict, whatever family it came from. */
|
|
90
|
+
function verdictOfReport(report: unknown): Verdict {
|
|
91
|
+
if (report === null || typeof report !== 'object') return 'fail'
|
|
92
|
+
const value = (report as Record<string, unknown>).verdict
|
|
93
|
+
return value === 'pass' || value === 'pass_with_findings' || value === 'fail' ? value : 'fail'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function diffOf(added: number, removed: number, addedErrors: number, firstCode?: string): VerdictSummary {
|
|
97
|
+
if (addedErrors > 0) {
|
|
98
|
+
return {
|
|
99
|
+
verdict: 'fail',
|
|
100
|
+
verdictReason: String(added) + ' new finding(s) (' + String(addedErrors) + ' error)' + (firstCode === undefined ? '' : ' (first: ' + firstCode + ')'),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (added > 0) {
|
|
104
|
+
return { verdict: 'pass_with_findings', verdictReason: String(added) + ' new warning finding(s), no new errors' }
|
|
105
|
+
}
|
|
106
|
+
if (removed > 0) {
|
|
107
|
+
return { verdict: 'pass', verdictReason: 'no new findings; ' + String(removed) + ' finding(s) resolved' }
|
|
108
|
+
}
|
|
109
|
+
return { verdict: 'pass', verdictReason: 'no new findings, nothing resolved' }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compare a baseline report with the current one. The verdict describes the *delta*
|
|
114
|
+
* (a new error fails it), while `currentVerdict` keeps the absolute result visible.
|
|
115
|
+
*/
|
|
116
|
+
export function diffReports(baseline: unknown, current: unknown): BaselineReport {
|
|
117
|
+
const before = flattenReportFindings(baseline)
|
|
118
|
+
const after = flattenReportFindings(current)
|
|
119
|
+
const beforeByIdentity = new Map<string, ReportFinding>()
|
|
120
|
+
for (const finding of before) beforeByIdentity.set(findingIdentity(finding), finding)
|
|
121
|
+
const seen = new Set<string>()
|
|
122
|
+
const added: ReportFinding[] = []
|
|
123
|
+
const changed: BaselineChange[] = []
|
|
124
|
+
for (const finding of after) {
|
|
125
|
+
const identity = findingIdentity(finding)
|
|
126
|
+
seen.add(identity)
|
|
127
|
+
const previous = beforeByIdentity.get(identity)
|
|
128
|
+
if (previous === undefined) {
|
|
129
|
+
added.push(finding)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
if (previous.severity !== finding.severity || previous.message !== finding.message) {
|
|
133
|
+
changed.push({
|
|
134
|
+
identity,
|
|
135
|
+
code: finding.code,
|
|
136
|
+
before: { severity: previous.severity, message: previous.message },
|
|
137
|
+
after: { severity: finding.severity, message: finding.message },
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const removed = before.filter((finding) => !seen.has(findingIdentity(finding)))
|
|
142
|
+
const addedErrors = added.filter((finding) => finding.severity === 'error')
|
|
143
|
+
const addedWarnings = added.filter((finding) => finding.severity === 'warning')
|
|
144
|
+
const delta = diffOf(added.length, removed.length, addedErrors.length, addedErrors[0]?.code)
|
|
145
|
+
|
|
146
|
+
const nextSteps: string[] = []
|
|
147
|
+
if (addedErrors.length > 0) {
|
|
148
|
+
nextSteps.push('Fix or explicitly accept the ' + String(addedErrors.length) + ' new error finding(s) before calling this slice done: the baseline is what "not worse" is measured against.')
|
|
149
|
+
} else if (added.length > 0) {
|
|
150
|
+
nextSteps.push('The new warnings are not failures, but they are new: decide per finding whether the change introduced them or the baseline was incomplete.')
|
|
151
|
+
} else {
|
|
152
|
+
nextSteps.push('No new findings: re-record the baseline with this report if the slice is accepted, so the next comparison starts from it.')
|
|
153
|
+
}
|
|
154
|
+
if (changed.length > 0) nextSteps.push('Review the ' + String(changed.length) + ' changed finding(s): the same locator now reports something different, which usually means the fix moved the problem.')
|
|
155
|
+
if (removed.length > 0) nextSteps.push(String(removed.length) + ' finding(s) from the baseline are gone — confirm they were fixed rather than renamed out of the report (a renamed check or a reworded message changes the identity).')
|
|
156
|
+
if (verdictOfReport(current) === 'fail') nextSteps.push('The current report still fails on its own terms; the delta being clean does not make the run clean.')
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
ran: true,
|
|
161
|
+
...delta,
|
|
162
|
+
schema: REPORT_SCHEMAS.baseline,
|
|
163
|
+
currentVerdict: verdictOfReport(current),
|
|
164
|
+
added,
|
|
165
|
+
removed,
|
|
166
|
+
changed,
|
|
167
|
+
summary: {
|
|
168
|
+
baseline: before.length,
|
|
169
|
+
current: after.length,
|
|
170
|
+
added: added.length,
|
|
171
|
+
removed: removed.length,
|
|
172
|
+
changed: changed.length,
|
|
173
|
+
addedErrors: addedErrors.length,
|
|
174
|
+
addedWarnings: addedWarnings.length,
|
|
175
|
+
},
|
|
176
|
+
nextSteps,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type { VerdictSummary }
|
package/src/client.js
CHANGED
|
@@ -30,7 +30,61 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
id: 'dsh-logicprobe',
|
|
31
31
|
factory: (require) => {
|
|
32
32
|
const React = require('react')
|
|
33
|
-
|
|
33
|
+
// The host's Switch and Button, vendored. A plain-JS client bundle must not
|
|
34
|
+
// require a Harness Client package: it changes without notice, we get no type
|
|
35
|
+
// check, and a throw blanks this slot entry. Markup, classes, token usage and
|
|
36
|
+
// the aria behaviour follow the host primitive; classes carry our own prefix.
|
|
37
|
+
const CONTROLS_CSS = [
|
|
38
|
+
'.lp-switch { box-sizing: border-box; position: relative; flex: 0 0 auto; width: 36px; height: 20px; padding: 2px; border: 0; border-radius: 999px; corner-shape: round; background: var(--dsw-alias-border-l3); cursor: pointer; }',
|
|
39
|
+
".lp-switch[aria-checked='true'] { background: var(--dsw-alias-brand-primary); }",
|
|
40
|
+
'.lp-switch:disabled { cursor: default; opacity: 0.5; }',
|
|
41
|
+
'.lp-switch:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }',
|
|
42
|
+
'.lp-switch-thumb { display: block; width: 16px; height: 16px; border-radius: 50%; corner-shape: round; background: var(--dsw-alias-label-primary-foreground); transition: transform 120ms ease; }',
|
|
43
|
+
".lp-switch[aria-checked='false'] .lp-switch-thumb { background: var(--dsw-alias-switch-thumb); }",
|
|
44
|
+
".lp-switch[aria-checked='true'] .lp-switch-thumb { transform: translateX(16px); }",
|
|
45
|
+
'.lp-btn { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 4px; border: none; border-radius: var(--dsw-radius-md); cursor: pointer; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary); background: transparent; padding: 0 14px; }',
|
|
46
|
+
'.lp-btn:disabled { cursor: not-allowed; opacity: 0.4; }',
|
|
47
|
+
'.lp-btn-sm { height: 28px; font-size: 12px; line-height: 18px; padding: 0 10px; border-radius: var(--dsw-radius-sm); }',
|
|
48
|
+
'.lp-btn-outline { border: 0.5px solid var(--dsw-alias-border-l3); background: transparent; }',
|
|
49
|
+
'.lp-btn-outline:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }',
|
|
50
|
+
].join('\n')
|
|
51
|
+
|
|
52
|
+
/** Rendered inside the component tree, so unmounting removes the styles. */
|
|
53
|
+
function Controls() {
|
|
54
|
+
return React.createElement('style', null, CONTROLS_CSS)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Switch(props) {
|
|
58
|
+
return React.createElement(
|
|
59
|
+
'button',
|
|
60
|
+
{
|
|
61
|
+
type: 'button',
|
|
62
|
+
role: 'switch',
|
|
63
|
+
'aria-checked': props.checked === true,
|
|
64
|
+
'aria-label': props.label,
|
|
65
|
+
className: 'lp-switch',
|
|
66
|
+
disabled: props.disabled === true,
|
|
67
|
+
onClick: () => {
|
|
68
|
+
if (props.disabled !== true) props.onChange(!(props.checked === true))
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
React.createElement('span', { className: 'lp-switch-thumb' }),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Switch.displayName = 'dsh-logicprobe:Switch'
|
|
76
|
+
Button.displayName = 'dsh-logicprobe:Button'
|
|
77
|
+
|
|
78
|
+
function Button(props) {
|
|
79
|
+
const classes = ['lp-btn']
|
|
80
|
+
if (props.size === 'sm') classes.push('lp-btn-sm')
|
|
81
|
+
if (props.variant === 'outline') classes.push('lp-btn-outline')
|
|
82
|
+
return React.createElement(
|
|
83
|
+
'button',
|
|
84
|
+
{ type: 'button', className: classes.join(' '), disabled: props.disabled === true, onClick: props.onClick },
|
|
85
|
+
props.children,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
34
88
|
|
|
35
89
|
/** Settings namespace: the Loader entry id this bundle's patch declares. */
|
|
36
90
|
const NS = 'logicprobe'
|
|
@@ -122,6 +176,7 @@ window.__ModuleLoader__.load({
|
|
|
122
176
|
const locked = state.writable !== true || pending
|
|
123
177
|
|
|
124
178
|
const children = [
|
|
179
|
+
React.createElement(Controls, { key: 'styles' }),
|
|
125
180
|
React.createElement('h4', { key: 'title', style: TITLE }, t('title')),
|
|
126
181
|
React.createElement('div', { key: 'row', style: ROW }, [
|
|
127
182
|
React.createElement('span', { key: 'label', style: LABEL }, t('label')),
|
package/src/compose-tool.ts
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
-
import type { JsonValue } from './json-value.js'
|
|
3
|
-
import { runCompositionVerification } from './engine.js'
|
|
4
|
-
|
|
5
|
-
export const LOGICPROBE_COMPOSE_TOOL_NAME = 'logicprobe_compose_verify'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* DSH tool wrapping multi-machine composition verification:
|
|
9
|
-
* product-space reachability with rendezvous handshakes (C1 deadlock, C2 sync).
|
|
10
|
-
*/
|
|
11
|
-
export const logicProbeComposeTool = defineTool({
|
|
12
|
-
name: LOGICPROBE_COMPOSE_TOOL_NAME,
|
|
13
|
-
description:
|
|
14
|
-
'
|
|
15
|
-
parameters: {
|
|
16
|
-
machines: {
|
|
17
|
-
type: 'json',
|
|
18
|
-
required: true,
|
|
19
|
-
description: 'Array of LogicModelV1 state-machine models to compose (two or more).',
|
|
20
|
-
},
|
|
21
|
-
rendezvous: {
|
|
22
|
-
type: 'json',
|
|
23
|
-
description: 'Optional array of handshake event names shared by the machines.',
|
|
24
|
-
},
|
|
25
|
-
maxStates: {
|
|
26
|
-
type: 'integer',
|
|
27
|
-
description: 'Maximum composite states to explore. Default 10000.',
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
output: {
|
|
31
|
-
schema: {
|
|
32
|
-
type: 'json',
|
|
33
|
-
description: 'Composition verification report with C1/C2 findings.',
|
|
34
|
-
},
|
|
35
|
-
render(_args, value) {
|
|
36
|
-
return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
timeoutMs: 10_000,
|
|
40
|
-
isConcurrencySafe: () => true,
|
|
41
|
-
async execute(args) {
|
|
42
|
-
return runCompositionVerification(args.machines as unknown[], {
|
|
43
|
-
rendezvous: args.rendezvous as string[] | undefined,
|
|
44
|
-
maxStates: args.maxStates,
|
|
45
|
-
}) as unknown as JsonValue
|
|
46
|
-
},
|
|
47
|
-
})
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
+
import type { JsonValue } from './json-value.js'
|
|
3
|
+
import { runCompositionVerification } from './engine.js'
|
|
4
|
+
|
|
5
|
+
export const LOGICPROBE_COMPOSE_TOOL_NAME = 'logicprobe_compose_verify'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DSH tool wrapping multi-machine composition verification:
|
|
9
|
+
* product-space reachability with rendezvous handshakes (C1 deadlock, C2 sync).
|
|
10
|
+
*/
|
|
11
|
+
export const logicProbeComposeTool = defineTool({
|
|
12
|
+
name: LOGICPROBE_COMPOSE_TOOL_NAME,
|
|
13
|
+
description:
|
|
14
|
+
'Check two or more LogicModelV1 machines together. Pass the models in `machines`. Pass `rendezvous` for handshake events: these fire only when every declaring machine is enabled at once, and a terminal machine stops taking part. Other events advance one machine, so a machine waiting on a handshake can take any number of its own steps first. There is no rate ratio and no fairness bound. Returns C1 deadlocks (no machine can advance, and one is not terminal) and C2 handshakes that never fire. Both name the reason per machine, and C1 gives the shortest counterexample. `maxStates` caps the search; a capped run says so. Read `verdict` and `verdictReason`.',
|
|
15
|
+
parameters: {
|
|
16
|
+
machines: {
|
|
17
|
+
type: 'json',
|
|
18
|
+
required: true,
|
|
19
|
+
description: 'Array of LogicModelV1 state-machine models to compose (two or more).',
|
|
20
|
+
},
|
|
21
|
+
rendezvous: {
|
|
22
|
+
type: 'json',
|
|
23
|
+
description: 'Optional array of handshake event names shared by the machines.',
|
|
24
|
+
},
|
|
25
|
+
maxStates: {
|
|
26
|
+
type: 'integer',
|
|
27
|
+
description: 'Maximum composite states to explore. Default 10000.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
output: {
|
|
31
|
+
schema: {
|
|
32
|
+
type: 'json',
|
|
33
|
+
description: 'Composition verification report with C1/C2 findings.',
|
|
34
|
+
},
|
|
35
|
+
render(_args, value) {
|
|
36
|
+
return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
timeoutMs: 10_000,
|
|
40
|
+
isConcurrencySafe: () => true,
|
|
41
|
+
async execute(args) {
|
|
42
|
+
return runCompositionVerification(args.machines as unknown[], {
|
|
43
|
+
rendezvous: args.rendezvous as string[] | undefined,
|
|
44
|
+
maxStates: args.maxStates,
|
|
45
|
+
}) as unknown as JsonValue
|
|
46
|
+
},
|
|
47
|
+
})
|
package/src/concurrency.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { REPORT_SCHEMAS, verdictOfFindings, type ReportSchema, type Verdict } from './engine.js'
|
|
1
2
|
export interface ConcurrencyFinding {
|
|
2
3
|
code: 'CONCURRENCY_KEYWORD' | 'CONCURRENCY_ABSOLUTE_CLAIM'
|
|
3
4
|
severity: 'warning' | 'error'
|
|
@@ -11,6 +12,11 @@ export interface ConcurrencyFinding {
|
|
|
11
12
|
|
|
12
13
|
export interface ConcurrencyScanReport {
|
|
13
14
|
ok: boolean
|
|
15
|
+
ran: boolean
|
|
16
|
+
verdict: Verdict
|
|
17
|
+
verdictReason: string
|
|
18
|
+
/** The versioned report contract this result follows. */
|
|
19
|
+
schema: ReportSchema
|
|
14
20
|
findings: ConcurrencyFinding[]
|
|
15
21
|
summary: {
|
|
16
22
|
lines: number
|
|
@@ -19,6 +25,10 @@ export interface ConcurrencyScanReport {
|
|
|
19
25
|
warnings: number
|
|
20
26
|
errors: number
|
|
21
27
|
}
|
|
28
|
+
/** This scan hashes no model; the field is present so every report has the same shape. */
|
|
29
|
+
hashes: Record<string, string>
|
|
30
|
+
/** What to do next, derived from the findings — never empty. */
|
|
31
|
+
nextSteps: string[]
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
interface KeywordRule {
|
|
@@ -100,8 +110,19 @@ export function runConcurrencyScan(text: string): ConcurrencyScanReport {
|
|
|
100
110
|
const errors = findings.filter((finding) => finding.severity === 'error').length
|
|
101
111
|
const warnings = findings.filter((finding) => finding.severity === 'warning').length
|
|
102
112
|
const absoluteClaims = findings.filter((finding) => finding.code === 'CONCURRENCY_ABSOLUTE_CLAIM').length
|
|
113
|
+
const nextSteps: string[] = []
|
|
114
|
+
if (absoluteClaims > 0) {
|
|
115
|
+
nextSteps.push('Every absolute claim must either cite dedicated evidence (a TSan/Helgrind run, a CBMC harness, a TLA+ model of the interleaving) or be restated as a bounded, testable property.')
|
|
116
|
+
}
|
|
117
|
+
if (findings.length > absoluteClaims) {
|
|
118
|
+
nextSteps.push('Review each flagged keyword: a plan that names shared state, a mutex or an interrupt without a synchronization argument is incomplete, not merely verbose.')
|
|
119
|
+
}
|
|
120
|
+
nextSteps.push('This scan mines claims; it does not prove concurrency safety — route each one to the tool named in its suggestions.')
|
|
103
121
|
return {
|
|
104
122
|
ok: true,
|
|
123
|
+
ran: true,
|
|
124
|
+
...verdictOfFindings(findings),
|
|
125
|
+
schema: REPORT_SCHEMAS.concurrency,
|
|
105
126
|
findings,
|
|
106
127
|
summary: {
|
|
107
128
|
lines: lines.length,
|
|
@@ -110,5 +131,7 @@ export function runConcurrencyScan(text: string): ConcurrencyScanReport {
|
|
|
110
131
|
warnings,
|
|
111
132
|
errors,
|
|
112
133
|
},
|
|
134
|
+
hashes: {},
|
|
135
|
+
nextSteps,
|
|
113
136
|
}
|
|
114
137
|
}
|
package/src/data-engine.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import type
|
|
2
|
+
import { REPORT_SCHEMAS, verdictOfFindings, type CheckResult, type Finding, type ReportSchema, type Verdict } from './engine.js'
|
|
3
3
|
|
|
4
4
|
export const DATA_ENGINE_SCHEMA_VERSION = 1
|
|
5
5
|
|
|
@@ -129,9 +129,17 @@ export interface DataComparisonSummary {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export interface DataVerificationReport {
|
|
132
|
+
/** The engine ran and the input was well-formed enough to produce this report. Read `verdict` for the review outcome. */
|
|
132
133
|
ok: boolean
|
|
134
|
+
ran: boolean
|
|
135
|
+
verdict: Verdict
|
|
136
|
+
verdictReason: string
|
|
137
|
+
/** The versioned report contract this result follows. */
|
|
138
|
+
schema: ReportSchema
|
|
133
139
|
schemaVersion: 1
|
|
134
140
|
modelHash: string
|
|
141
|
+
/** Every hash this report carries, in one place. The data-model hash follows the same normalization as the published spec, but is not covered by it. */
|
|
142
|
+
hashes: { modelHash: string }
|
|
135
143
|
summary: {
|
|
136
144
|
entities: number
|
|
137
145
|
fields: number
|
|
@@ -142,6 +150,8 @@ export interface DataVerificationReport {
|
|
|
142
150
|
}
|
|
143
151
|
checks: CheckResult[]
|
|
144
152
|
comparison?: DataComparisonSummary
|
|
153
|
+
/** What to do next, derived from the findings — never empty. */
|
|
154
|
+
nextSteps: string[]
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
const KNOWN_TYPES = new Set<string>([
|
|
@@ -913,18 +923,25 @@ function buildDataComparisonSummary(before: DataModelV1, after: DataModelV1, opt
|
|
|
913
923
|
export function runDataVerification(input: unknown, options: DataVerificationOptions = {}): DataVerificationReport {
|
|
914
924
|
const validation = validateDataModel(input)
|
|
915
925
|
if (!validation.ok) {
|
|
926
|
+
const findings: Finding[] = validation.errors.map((message) => ({ code: 'DATA_MODEL_INVALID', severity: 'error', message }))
|
|
927
|
+
const modelCheck: CheckResult = {
|
|
928
|
+
id: 'DATA_MODEL',
|
|
929
|
+
name: 'Data Model Validation',
|
|
930
|
+
status: 'fail',
|
|
931
|
+
detail: 'Data model schema validation failed: ' + validation.errors.length + ' errors',
|
|
932
|
+
findings,
|
|
933
|
+
}
|
|
916
934
|
return {
|
|
917
935
|
ok: false,
|
|
936
|
+
ran: true,
|
|
937
|
+
...verdictOfFindings(findings),
|
|
938
|
+
schema: REPORT_SCHEMAS.datamodel,
|
|
918
939
|
schemaVersion: 1,
|
|
919
940
|
modelHash: '',
|
|
941
|
+
hashes: { modelHash: '' },
|
|
920
942
|
summary: { entities: 0, fields: 0, errors: validation.errors.length, warnings: 0, checksRun: 0 },
|
|
921
|
-
checks: [
|
|
922
|
-
|
|
923
|
-
name: 'Data Model Validation',
|
|
924
|
-
status: 'fail',
|
|
925
|
-
detail: 'Data model schema validation failed: ' + validation.errors.length + ' errors',
|
|
926
|
-
findings: validation.errors.map((message) => ({ code: 'DATA_MODEL_INVALID', severity: 'error', message })),
|
|
927
|
-
}],
|
|
943
|
+
checks: [modelCheck],
|
|
944
|
+
nextSteps: dataNextSteps([modelCheck]),
|
|
928
945
|
}
|
|
929
946
|
}
|
|
930
947
|
const model = validation.model
|
|
@@ -950,10 +967,15 @@ export function runDataVerification(input: unknown, options: DataVerificationOpt
|
|
|
950
967
|
if (options.beforeModel !== undefined) {
|
|
951
968
|
const beforeValidation = validateDataModel(options.beforeModel)
|
|
952
969
|
if (!beforeValidation.ok) {
|
|
970
|
+
const beforeFindings: Finding[] = beforeValidation.errors.map((message) => ({ code: 'BEFORE_DATA_MODEL_INVALID', severity: 'error', message }))
|
|
953
971
|
return {
|
|
954
972
|
ok: false,
|
|
973
|
+
ran: true,
|
|
974
|
+
...verdictOfFindings([...checks.flatMap((check) => check.findings), ...beforeFindings]),
|
|
975
|
+
schema: REPORT_SCHEMAS.datamodel,
|
|
955
976
|
schemaVersion: 1,
|
|
956
977
|
modelHash: dataModelHash(model),
|
|
978
|
+
hashes: { modelHash: dataModelHash(model) },
|
|
957
979
|
summary: {
|
|
958
980
|
entities: model.entities.length,
|
|
959
981
|
fields: allFieldPaths(model).length,
|
|
@@ -968,9 +990,10 @@ export function runDataVerification(input: unknown, options: DataVerificationOpt
|
|
|
968
990
|
name: 'Before Data Model Validation',
|
|
969
991
|
status: 'fail',
|
|
970
992
|
detail: 'Before data model schema validation failed: ' + beforeValidation.errors.length + ' errors',
|
|
971
|
-
findings:
|
|
993
|
+
findings: beforeFindings,
|
|
972
994
|
},
|
|
973
995
|
],
|
|
996
|
+
nextSteps: dataNextSteps(checks),
|
|
974
997
|
}
|
|
975
998
|
}
|
|
976
999
|
const before = beforeValidation.model
|
|
@@ -986,8 +1009,12 @@ export function runDataVerification(input: unknown, options: DataVerificationOpt
|
|
|
986
1009
|
const warnings = checks.reduce((sum, check) => sum + check.findings.filter((finding) => finding.severity === 'warning').length, 0)
|
|
987
1010
|
return {
|
|
988
1011
|
ok: true,
|
|
1012
|
+
ran: true,
|
|
1013
|
+
...verdictOfFindings(checks.flatMap((check) => check.findings)),
|
|
1014
|
+
schema: REPORT_SCHEMAS.datamodel,
|
|
989
1015
|
schemaVersion: 1,
|
|
990
1016
|
modelHash: dataModelHash(model),
|
|
1017
|
+
hashes: { modelHash: dataModelHash(model) },
|
|
991
1018
|
summary: {
|
|
992
1019
|
entities: model.entities.length,
|
|
993
1020
|
fields: allFieldPaths(model).length,
|
|
@@ -996,6 +1023,21 @@ export function runDataVerification(input: unknown, options: DataVerificationOpt
|
|
|
996
1023
|
checksRun: checks.length,
|
|
997
1024
|
},
|
|
998
1025
|
checks,
|
|
1026
|
+
nextSteps: dataNextSteps(checks),
|
|
999
1027
|
...(comparison === undefined ? {} : { comparison }),
|
|
1000
1028
|
}
|
|
1001
1029
|
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* The next-steps list every data report carries, derived only from the checks so two
|
|
1033
|
+
* runs over the same model produce the same list.
|
|
1034
|
+
*/
|
|
1035
|
+
function dataNextSteps(checks: CheckResult[]): string[] {
|
|
1036
|
+
const steps: string[] = []
|
|
1037
|
+
const failed = checks.filter((check) => check.status === 'fail').map((check) => check.id + ' ' + check.name)
|
|
1038
|
+
const warningCodes = [...new Set(checks.flatMap((check) => check.findings).filter((finding) => finding.severity === 'warning').map((finding) => finding.code))].sort()
|
|
1039
|
+
if (failed.length > 0) steps.push('Resolve the failing checks first: ' + failed.join(', ') + '.')
|
|
1040
|
+
if (warningCodes.length > 0) steps.push('Review the warning findings (' + warningCodes.join(', ') + '): a nullable field or a missing index is a decision, not silence.')
|
|
1041
|
+
steps.push('Re-run with beforeModel plus the mapping/copy/migration options after the change to prove the data behaviour did not regress (DD1-DD4).')
|
|
1042
|
+
return steps
|
|
1043
|
+
}
|