dsh-model-everything 0.0.0-stage → 0.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.
- package/LICENSE +21 -0
- package/README.en-US.md +332 -0
- package/README.md +332 -3
- package/cordis.patch.yml +20 -0
- package/examples/README.md +48 -0
- package/examples/control-dispatcher.json +113 -0
- package/examples/control-dispatcher.overbudget.json +113 -0
- package/examples/ecommerce-data-model.json +185 -0
- package/examples/order-state-machine.after.json +116 -0
- package/examples/order-state-machine.before.json +101 -0
- package/examples/user-migration.after.json +29 -0
- package/examples/user-migration.before.json +29 -0
- package/gate/locale/en.json +6 -0
- package/gate/locale/zh.json +6 -0
- package/gate/package.json +5 -0
- package/lib/baseline.js +249 -0
- package/lib/compose-tool.js +80 -0
- package/lib/concurrency-tool.js +40 -0
- package/lib/concurrency.js +206 -0
- package/lib/data-engine.js +1141 -0
- package/lib/data-tool.js +87 -0
- package/lib/design-engine.js +1149 -0
- package/lib/design-tool.js +63 -0
- package/lib/engine.js +4343 -0
- package/lib/export-tool.js +78 -0
- package/lib/exporters.js +434 -0
- package/lib/gate.js +274 -0
- package/lib/granularity.js +237 -0
- package/lib/index.js +237 -0
- package/lib/json-value.js +18 -0
- package/lib/model-file.js +48 -0
- package/lib/option-shape.js +62 -0
- package/lib/report-diff-tool.js +63 -0
- package/lib/report-view.js +433 -0
- package/lib/structure-tool.js +91 -0
- package/lib/structure.js +711 -0
- package/lib/tool.js +94 -0
- package/lib/types/baseline.d.ts +65 -0
- package/lib/types/compose-tool.d.ts +7 -0
- package/lib/types/concurrency-tool.d.ts +8 -0
- package/lib/types/concurrency.d.ts +37 -0
- package/lib/types/data-engine.d.ts +211 -0
- package/lib/types/data-tool.d.ts +10 -0
- package/lib/types/design-engine.d.ts +215 -0
- package/lib/types/design-tool.d.ts +11 -0
- package/lib/types/engine.d.ts +494 -0
- package/lib/types/export-tool.d.ts +7 -0
- package/lib/types/exporters.d.ts +37 -0
- package/lib/types/gate.d.ts +91 -0
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +45 -0
- package/lib/types/json-value.d.ts +21 -0
- package/lib/types/model-file.d.ts +7 -0
- package/lib/types/option-shape.d.ts +22 -0
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/report-view.d.ts +50 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +152 -0
- package/lib/types/tool.d.ts +11 -0
- package/lib/types/uml-communication.d.ts +109 -0
- package/lib/types/uml-composite.d.ts +107 -0
- package/lib/types/uml-interaction-overview.d.ts +91 -0
- package/lib/types/uml-profile.d.ts +112 -0
- package/lib/types/uml-structure.d.ts +53 -0
- package/lib/types/uml-syntax.d.ts +296 -0
- package/lib/types/uml-timing.d.ts +152 -0
- package/lib/types/uml-tool.d.ts +12 -0
- package/lib/types/uml-usecase.d.ts +58 -0
- package/lib/types/uml.d.ts +303 -0
- package/lib/uml-communication.js +353 -0
- package/lib/uml-composite.js +442 -0
- package/lib/uml-interaction-overview.js +277 -0
- package/lib/uml-profile.js +373 -0
- package/lib/uml-structure.js +306 -0
- package/lib/uml-syntax.js +545 -0
- package/lib/uml-timing.js +974 -0
- package/lib/uml-tool.js +213 -0
- package/lib/uml-usecase.js +532 -0
- package/lib/uml.js +2110 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +128 -6
- package/skills/model-everything/SKILL.md +450 -0
- package/skills/model-everything/references/dsh-model-schema.md +437 -0
- package/skills/model-everything/references/gap-routing-guide.md +42 -0
- package/skills/model-everything/references/hash-spec.md +152 -0
- package/skills/model-everything/references/logic-verification-guide.md +422 -0
- package/skills/model-everything-datamodel/SKILL.md +130 -0
- package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
- package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
- package/skills/model-everything-design/SKILL.md +115 -0
- package/skills/model-everything-interleaving/SKILL.md +63 -0
- package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
- package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
- package/skills/model-everything-report-reading/SKILL.md +137 -0
- package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
- package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
- package/skills/model-everything-structure/SKILL.md +96 -0
- package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
- package/skills/model-everything-uml/SKILL.md +60 -0
- package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
- package/src/baseline.ts +288 -0
- package/src/compose-tool.ts +82 -0
- package/src/concurrency-tool.ts +44 -0
- package/src/concurrency.ts +254 -0
- package/src/data-engine.ts +1208 -0
- package/src/data-tool.ts +91 -0
- package/src/design-engine.ts +1263 -0
- package/src/design-tool.ts +68 -0
- package/src/engine.ts +4564 -0
- package/src/export-tool.ts +81 -0
- package/src/exporters.ts +399 -0
- package/src/gate.ts +330 -0
- package/src/granularity.ts +312 -0
- package/src/index.ts +289 -0
- package/src/json-value.ts +20 -0
- package/src/model-file.ts +49 -0
- package/src/option-shape.ts +64 -0
- package/src/report-diff-tool.ts +67 -0
- package/src/report-view.ts +483 -0
- package/src/structure-tool.ts +93 -0
- package/src/structure.ts +786 -0
- package/src/tool.ts +103 -0
- package/src/uml-communication.ts +428 -0
- package/src/uml-composite.ts +520 -0
- package/src/uml-interaction-overview.ts +321 -0
- package/src/uml-profile.ts +439 -0
- package/src/uml-structure.ts +317 -0
- package/src/uml-syntax.ts +764 -0
- package/src/uml-timing.ts +1106 -0
- package/src/uml-tool.ts +230 -0
- package/src/uml-usecase.ts +566 -0
- package/src/uml.ts +2139 -0
package/lib/baseline.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
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 dsh-model-everything-baseline
|
|
15
|
+
*/
|
|
16
|
+
import { canonicalJson, REPORT_SCHEMAS, verdictOf } from './engine.js';
|
|
17
|
+
/**
|
|
18
|
+
* The stable identity of a finding: check + file + code + a canonical locator. Prose is
|
|
19
|
+
* excluded so a reworded message reads as `changed`, not as a new finding.
|
|
20
|
+
*/
|
|
21
|
+
export function findingIdentity(finding) {
|
|
22
|
+
const locator = canonicalJson({
|
|
23
|
+
...(finding.evidence === undefined ? {} : { evidence: finding.evidence }),
|
|
24
|
+
...(finding.path === undefined ? {} : { path: finding.path }),
|
|
25
|
+
});
|
|
26
|
+
const where = locator === '{}' ? 'message:' + finding.message : locator;
|
|
27
|
+
const file = finding.file === undefined ? '' : finding.file + '|';
|
|
28
|
+
return (finding.check === undefined ? '' : finding.check + '|') + file + finding.code + '|' + where;
|
|
29
|
+
}
|
|
30
|
+
/** Flatten a report of any family into one finding list (`checks[].findings` or `findings[]`). */
|
|
31
|
+
export function flattenReportFindings(report) {
|
|
32
|
+
if (report === null || typeof report !== 'object')
|
|
33
|
+
return [];
|
|
34
|
+
const record = report;
|
|
35
|
+
const flat = [];
|
|
36
|
+
if (Array.isArray(record.checks)) {
|
|
37
|
+
for (const check of record.checks) {
|
|
38
|
+
const id = typeof check.id === 'string' ? check.id : undefined;
|
|
39
|
+
for (const finding of (Array.isArray(check.findings) ? check.findings : [])) {
|
|
40
|
+
flat.push(id === undefined ? finding : { ...finding, check: id });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(record.findings)) {
|
|
45
|
+
for (const finding of record.findings)
|
|
46
|
+
flat.push(finding);
|
|
47
|
+
}
|
|
48
|
+
return flat;
|
|
49
|
+
}
|
|
50
|
+
/** The report's own verdict, whatever family it came from. */
|
|
51
|
+
function verdictOfReport(report) {
|
|
52
|
+
if (report === null || typeof report !== 'object')
|
|
53
|
+
return 'fail';
|
|
54
|
+
const value = report.verdict;
|
|
55
|
+
return value === 'pass' || value === 'pass_with_findings' || value === 'fail' ? value : 'fail';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The delta's verdict, from the four ways a comparison can get worse or better.
|
|
59
|
+
*
|
|
60
|
+
* `escalated` is the one that used to be missing: a finding that keeps its identity and moves from warning to
|
|
61
|
+
* error lives in `changed`, not in `added`, so folding severities into `addedErrors` only left the comparison
|
|
62
|
+
* answering `pass` over a change that made the report strictly worse. A de-escalation is not a regression: it is
|
|
63
|
+
* still a change, so it is named, and the verdict stays `pass`.
|
|
64
|
+
*/
|
|
65
|
+
function diffOf(added, removed, addedErrors, escalated, deescalated, firstCode) {
|
|
66
|
+
const errors = addedErrors + escalated;
|
|
67
|
+
if (errors > 0) {
|
|
68
|
+
const bits = [];
|
|
69
|
+
if (added > 0)
|
|
70
|
+
bits.push(String(added) + ' new finding(s)');
|
|
71
|
+
if (escalated > 0)
|
|
72
|
+
bits.push(String(escalated) + ' finding(s) escalated to error');
|
|
73
|
+
return {
|
|
74
|
+
verdict: 'fail',
|
|
75
|
+
// The count of errors is the sum: a reader must not have to add "new" and "escalated" in their head.
|
|
76
|
+
verdictReason: bits.join(', ') + ' (' + String(errors) + ' error)' + (firstCode === undefined ? '' : ' (first error: ' + firstCode + ')'),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (added > 0) {
|
|
80
|
+
return {
|
|
81
|
+
verdict: 'pass_with_findings',
|
|
82
|
+
verdictReason: String(added) + ' new warning finding(s), no new errors' + (deescalated > 0 ? '; ' + String(deescalated) + ' finding(s) moved to a lower severity' : ''),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (removed > 0) {
|
|
86
|
+
return { verdict: 'pass', verdictReason: 'no new findings; ' + String(removed) + ' finding(s) resolved' };
|
|
87
|
+
}
|
|
88
|
+
if (deescalated > 0) {
|
|
89
|
+
return { verdict: 'pass', verdictReason: 'no new findings; ' + String(deescalated) + ' finding(s) moved to a lower severity' };
|
|
90
|
+
}
|
|
91
|
+
return { verdict: 'pass', verdictReason: 'no new findings, nothing resolved' };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Compare a baseline report with the current one. The verdict describes the *delta*
|
|
95
|
+
* (a new error fails it), while `currentVerdict` keeps the absolute result visible.
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* Whether a value is a report this can compare.
|
|
99
|
+
*
|
|
100
|
+
* `diffReports(null, null)` answered `verdict: 'pass'`. With no inputs, every comparison found
|
|
101
|
+
* nothing to compare and the report said the two agree - the worst shape of a false green, because
|
|
102
|
+
* the absence of an input reads as agreement. A caller who passed the wrong variable would take
|
|
103
|
+
* that verdict at face value.
|
|
104
|
+
*
|
|
105
|
+
* The first fix refused only `null` and `undefined`, on the argument that a present-but-wrong value
|
|
106
|
+
* still carries `currentVerdict: 'fail'` and the delta is honestly empty. Running the installed
|
|
107
|
+
* plugin disproved that: `report_diff` with two `'junk'` strings answered `verdict: pass`, and
|
|
108
|
+
* `verdict` is the field a reader is told to act on. A mitigation in a field the reader is told to
|
|
109
|
+
* ignore is not a mitigation.
|
|
110
|
+
*
|
|
111
|
+
* The second fix required "an object", which moved the hole rather than closing it: `{}` is an object,
|
|
112
|
+
* has no findings, and two of them compare as agreement. A guard is only as deep as its requirement,
|
|
113
|
+
* so the requirement is now what the name promises - an object that carries at least one report field.
|
|
114
|
+
* A partial report still passes, because `findings` alone is one.
|
|
115
|
+
*/
|
|
116
|
+
const REPORT_FIELDS = ['schema', 'verdict', 'ran', 'ok', 'checks', 'findings', 'summary', 'hashes', 'currentVerdict', 'added', 'removed'];
|
|
117
|
+
function requireReport(value, role) {
|
|
118
|
+
if (value === null || value === undefined) {
|
|
119
|
+
throw new Error(role + ' is ' + String(value) + '. A diff needs both sides: with no input at all, every comparison finds nothing and the report reads as agreement between two things that were never compared.');
|
|
120
|
+
}
|
|
121
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
122
|
+
throw new Error(role + ' is a ' + typeof value + (Array.isArray(value) ? ' (an array)' : '') + ', not a report. A diff needs two report objects: two values that are not reports flatten to two empty finding lists, and the delta then reads as agreement between things that were never compared.');
|
|
123
|
+
}
|
|
124
|
+
const record = value;
|
|
125
|
+
if (!REPORT_FIELDS.some((field) => field in record)) {
|
|
126
|
+
const keys = Object.keys(record);
|
|
127
|
+
throw new Error(role + ' is an object with no report fields' + (keys.length === 0 ? ' at all' : ' (' + keys.slice(0, 6).join(', ') + ')') + ', not a report. Two such values flatten to two empty finding lists, and the delta then reads as agreement between things that were never compared.');
|
|
128
|
+
}
|
|
129
|
+
return record;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Why a report-shaped value still must not be compared, or `undefined` if it can be.
|
|
133
|
+
*
|
|
134
|
+
* Two shapes reach here legitimately and neither is a report about a model:
|
|
135
|
+
*
|
|
136
|
+
* - a REFUSAL (`ran: false`): the tool did not run, so there is nothing to compare against;
|
|
137
|
+
* - a report about an INVALID INPUT (`summary.checksRun === 0`): the engine ran, refused the input, and
|
|
138
|
+
* examined nothing. Its finding list is not empty because the model is clean, and comparing it against
|
|
139
|
+
* a real report would call every finding in that report resolved.
|
|
140
|
+
*
|
|
141
|
+
* `checksRun` is read only when the report carries it, because the families that report flat findings
|
|
142
|
+
* have no check roster and `compose` counts machines rather than checks.
|
|
143
|
+
*/
|
|
144
|
+
function incomparableReason(record, role) {
|
|
145
|
+
if (record.ran === false) {
|
|
146
|
+
return role + ' is a refusal: the fields describe why the tool did not run, so there is no report to compare';
|
|
147
|
+
}
|
|
148
|
+
const summary = record.summary;
|
|
149
|
+
if (summary !== null && typeof summary === 'object' && summary.checksRun === 0) {
|
|
150
|
+
return role + ' examined nothing (its summary says checksRun 0), so its empty finding list is not evidence of anything';
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
/** The family a report belongs to, when it says so. Two different schemas cannot be compared. */
|
|
155
|
+
function familyOf(record) {
|
|
156
|
+
return typeof record.schema === 'string' && record.schema !== '' ? record.schema : undefined;
|
|
157
|
+
}
|
|
158
|
+
export function diffReports(baseline, current) {
|
|
159
|
+
const beforeRecord = requireReport(baseline, 'baseline');
|
|
160
|
+
const afterRecord = requireReport(current, 'current');
|
|
161
|
+
for (const [record, role] of [[beforeRecord, 'baseline'], [afterRecord, 'current']]) {
|
|
162
|
+
const reason = incomparableReason(record, role);
|
|
163
|
+
if (reason !== undefined)
|
|
164
|
+
throw new Error(reason + '. A diff needs two runs that produced reports.');
|
|
165
|
+
}
|
|
166
|
+
const beforeFamily = familyOf(beforeRecord);
|
|
167
|
+
const afterFamily = familyOf(afterRecord);
|
|
168
|
+
if (beforeFamily !== undefined && afterFamily !== undefined && beforeFamily !== afterFamily) {
|
|
169
|
+
throw new Error('cannot compare a ' + beforeFamily + ' report with a ' + afterFamily + ' report: they do not share a finding vocabulary, so every finding on one side would read as resolved or added on the other. Compare two runs of the same tool.');
|
|
170
|
+
}
|
|
171
|
+
const before = flattenReportFindings(baseline);
|
|
172
|
+
const after = flattenReportFindings(current);
|
|
173
|
+
const beforeByIdentity = new Map();
|
|
174
|
+
for (const finding of before)
|
|
175
|
+
beforeByIdentity.set(findingIdentity(finding), finding);
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const added = [];
|
|
178
|
+
const changed = [];
|
|
179
|
+
for (const finding of after) {
|
|
180
|
+
const identity = findingIdentity(finding);
|
|
181
|
+
seen.add(identity);
|
|
182
|
+
const previous = beforeByIdentity.get(identity);
|
|
183
|
+
if (previous === undefined) {
|
|
184
|
+
added.push(finding);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (previous.severity !== finding.severity || previous.message !== finding.message) {
|
|
188
|
+
changed.push({
|
|
189
|
+
identity,
|
|
190
|
+
code: finding.code,
|
|
191
|
+
before: { severity: previous.severity, message: previous.message },
|
|
192
|
+
after: { severity: finding.severity, message: finding.message },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const removed = before.filter((finding) => !seen.has(findingIdentity(finding)));
|
|
197
|
+
const addedErrors = added.filter((finding) => finding.severity === 'error');
|
|
198
|
+
const addedWarnings = added.filter((finding) => finding.severity === 'warning');
|
|
199
|
+
// Severity movement lives in `changed`. Counted here, because the verdict is about whether the slice got worse,
|
|
200
|
+
// and a warning that became an error did - even though nothing was added.
|
|
201
|
+
const escalated = changed.filter((change) => change.before.severity !== 'error' && change.after.severity === 'error');
|
|
202
|
+
const deescalated = changed.filter((change) => change.before.severity === 'error' && change.after.severity !== 'error');
|
|
203
|
+
const delta = diffOf(added.length, removed.length, addedErrors.length, escalated.length, deescalated.length, addedErrors[0]?.code ?? escalated[0]?.code);
|
|
204
|
+
const nextSteps = [];
|
|
205
|
+
if (addedErrors.length > 0) {
|
|
206
|
+
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.');
|
|
207
|
+
}
|
|
208
|
+
else if (escalated.length > 0) {
|
|
209
|
+
nextSteps.push('A finding moved from warning to error (' + String(escalated.length) + '): the slice added nothing but still made the report worse, so the baseline is less strict than the run.');
|
|
210
|
+
}
|
|
211
|
+
else if (added.length > 0) {
|
|
212
|
+
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.');
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
nextSteps.push('No new findings: re-record the baseline with this report if the slice is accepted, so the next comparison starts from it.');
|
|
216
|
+
}
|
|
217
|
+
if (changed.length > 0)
|
|
218
|
+
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.');
|
|
219
|
+
if (removed.length > 0)
|
|
220
|
+
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).');
|
|
221
|
+
if (verdictOfReport(current) === 'fail') {
|
|
222
|
+
// The sentence has to match the delta it sits beside: "the delta being clean" is false when the delta failed,
|
|
223
|
+
// which is exactly the case an escalation now produces.
|
|
224
|
+
const deltaClean = delta.verdict === 'pass' || delta.verdict === 'pass_with_findings';
|
|
225
|
+
nextSteps.push(deltaClean
|
|
226
|
+
? 'The current report still fails on its own terms; the delta being clean does not make the run clean.'
|
|
227
|
+
: 'The current report still fails on its own terms, and the delta already says the slice made it worse: settle both before accepting it.');
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
ok: true,
|
|
231
|
+
ran: true,
|
|
232
|
+
...delta,
|
|
233
|
+
schema: REPORT_SCHEMAS.baseline,
|
|
234
|
+
currentVerdict: verdictOfReport(current),
|
|
235
|
+
added,
|
|
236
|
+
removed,
|
|
237
|
+
changed,
|
|
238
|
+
summary: {
|
|
239
|
+
baseline: before.length,
|
|
240
|
+
current: after.length,
|
|
241
|
+
added: added.length,
|
|
242
|
+
removed: removed.length,
|
|
243
|
+
changed: changed.length,
|
|
244
|
+
addedErrors: addedErrors.length,
|
|
245
|
+
addedWarnings: addedWarnings.length,
|
|
246
|
+
},
|
|
247
|
+
nextSteps,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { runCompositionVerification } from './engine.js';
|
|
3
|
+
import { OptionShapeError, optionRefusal, requireArrayOption } from './option-shape.js';
|
|
4
|
+
import { renderReportText } from './report-view.js';
|
|
5
|
+
export const COMPOSE_TOOL_NAME = 'model_compose_verify';
|
|
6
|
+
/**
|
|
7
|
+
* DSH tool wrapping multi-machine composition verification:
|
|
8
|
+
* product-space reachability with rendezvous handshakes (C1 deadlock, C2 sync), and the shared
|
|
9
|
+
* locations the machines both touch (C3 declarations, C4 unordered write, C5 non-atomic update).
|
|
10
|
+
*/
|
|
11
|
+
export const modelComposeTool = defineTool({
|
|
12
|
+
name: COMPOSE_TOOL_NAME,
|
|
13
|
+
description: '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. Pass `shared` for locations two machines both touch and `accesses` to say which machine event reads or writes one: C3 reports a declaration that names something the composition does not have, C4 an unordered write to one location both machines write, and C5 a read-modify-write pair the model does not make atomic (the lost-update interleaving). Two accesses that name the same `protectedBy` lock are treated as exclusive; that the lock is held across the whole operation is a single-machine question this tool does not answer. 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. It decides what no single machine can show: a handshake that never fires, or a node where no machine may advance because of the others. One machine on its own is `model_verify`. Read `verdict` and `verdictReason`.',
|
|
14
|
+
parameters: {
|
|
15
|
+
machines: {
|
|
16
|
+
type: 'json',
|
|
17
|
+
required: true,
|
|
18
|
+
description: 'Array of LogicModelV1 state-machine models to compose (two or more).',
|
|
19
|
+
},
|
|
20
|
+
rendezvous: {
|
|
21
|
+
type: 'json',
|
|
22
|
+
description: 'Optional array of handshake event names shared by the machines.',
|
|
23
|
+
},
|
|
24
|
+
shared: {
|
|
25
|
+
type: 'json',
|
|
26
|
+
description: 'Optional array of shared locations: { id, initial? }. Without one, nothing is shared between the machines.',
|
|
27
|
+
},
|
|
28
|
+
accesses: {
|
|
29
|
+
type: 'json',
|
|
30
|
+
description: 'Optional array of { machine, event, location, kind: "read" | "write" | "read-write", protectedBy? }. A read-write is a read-modify-write: two of them that are not mutually exclusive lose an update.',
|
|
31
|
+
},
|
|
32
|
+
maxStates: {
|
|
33
|
+
type: 'integer',
|
|
34
|
+
description: 'Maximum composite states to explore. Default 10000.',
|
|
35
|
+
},
|
|
36
|
+
check: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Print one check in full when the digest truncated it. Pass the check id the CHECKS line lists (for example A9); the per-check budget no longer applies, and an id this report does not carry is answered with the ids that it does.',
|
|
39
|
+
},
|
|
40
|
+
reportView: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
enum: ['digest', 'full'],
|
|
43
|
+
description: 'digest (default) prints the readable body; full prints the structured report, which report_diff needs.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
output: {
|
|
47
|
+
schema: {
|
|
48
|
+
type: 'json',
|
|
49
|
+
description: 'Composition verification report with C1/C2 findings and, when shared locations are declared, C3/C4/C5.',
|
|
50
|
+
},
|
|
51
|
+
render(args, value) {
|
|
52
|
+
return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full', check: args.check }) }];
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
timeoutMs: 10_000,
|
|
56
|
+
isConcurrencySafe: () => true,
|
|
57
|
+
async execute(args) {
|
|
58
|
+
try {
|
|
59
|
+
// `machines` is declared `json`, so a string reached `machinesInput.forEach` as a raw TypeError. The
|
|
60
|
+
// array-ness is the tool's contract; check it here and refuse with a code that names the option.
|
|
61
|
+
requireArrayOption(args.machines, 'machines', 'COMPOSE_OPTION_INVALID');
|
|
62
|
+
if (args.machines !== undefined && args.machines.length === 0) {
|
|
63
|
+
throw new OptionShapeError('COMPOSE_OPTION_INVALID', 'machines is an empty array. A composition needs at least two machines to compose; with none there is nothing to decide.');
|
|
64
|
+
}
|
|
65
|
+
if (args.shared !== undefined)
|
|
66
|
+
requireArrayOption(args.shared, 'shared', 'COMPOSE_OPTION_INVALID');
|
|
67
|
+
if (args.accesses !== undefined)
|
|
68
|
+
requireArrayOption(args.accesses, 'accesses', 'COMPOSE_OPTION_INVALID');
|
|
69
|
+
return runCompositionVerification(args.machines, {
|
|
70
|
+
rendezvous: args.rendezvous,
|
|
71
|
+
shared: args.shared,
|
|
72
|
+
accesses: args.accesses,
|
|
73
|
+
maxStates: args.maxStates,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return optionRefusal(error, 'COMPOSE_OPTION_INVALID');
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { runConcurrencyScan } from './concurrency.js';
|
|
3
|
+
import { renderReportText } from './report-view.js';
|
|
4
|
+
export const INTERLEAVING_SCAN_TOOL_NAME = 'model_interleaving_scan';
|
|
5
|
+
/**
|
|
6
|
+
* Model-visible DSH tool that mines design documents/plans for concurrency-related
|
|
7
|
+
* claims and risk keywords. It does not prove concurrency safety; it flags terms
|
|
8
|
+
* such as "thread-safe", "lock-free", "race condition", "atomic", "mutex", etc.,
|
|
9
|
+
* so the model can either provide dedicated evidence or mark the claim unverified.
|
|
10
|
+
*/
|
|
11
|
+
export const modelConcurrencyScanTool = defineTool({
|
|
12
|
+
name: INTERLEAVING_SCAN_TOOL_NAME,
|
|
13
|
+
description: 'Scan a document or plan text for claims about how work is executed: threads, synchronization, async tasks, interrupts, shared state. Use ONLY after confirming the target actually has concurrency requirements or behavior; if the target is purely sequential, do not call this tool. Findings carry a family (threads, synchronization, async, interrupts, shared-state) and an absolute claim is one a plan makes about safety rather than a risk it mentions. Returns keywords like thread-safe, lock-free, wait-free, data race, race condition, atomic, synchronized, mutex, semaphore, spinlock, shared variable, shared memory, reentrant, interrupt-safe, ISR, IRQ, NMI, critical section, disable_irq/enable_irq; async ones such as async, await, promise, event loop, callback, microtask, coroutine, fire-and-forget, backpressure, task queue; and their Chinese counterparts (线程安全, 无数据竞争, 无锁, 互斥, 原子, 并发, 临界区, 异步, 回调, 事件循环, 微任务, 协程, 背压 and so on). Absolute claims (thread-safe, lock-free, no data race, interrupt-safe, ISR-safe, non-blocking, never blocks) are flagged as errors requiring dedicated verification, and each carries suggestions naming the analysis to run instead, chosen by family: TSan/Helgrind/CBMC/TLA+ for threads, RTOS-aware analysis for interrupts, and TLA+/UPPAAL plus a runtime check for async, where a blocking call on the event loop, a missing await and a lock held across an await are invisible to any model. Text with no claim to mine is refused with `CONCURRENCY_NO_CLAIM` rather than reported as clean: a scan of nothing is not a clean bill of health, and that includes text in a vocabulary this scan does not match. This plugin does not prove concurrency safety, and this tool does not check a machine: to decide a protocol it mined, model it and call `model_verify`, or compose two of them with `model_compose_verify`.',
|
|
14
|
+
parameters: {
|
|
15
|
+
text: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
required: true,
|
|
18
|
+
description: 'Document or plan text to scan for claims about threads, synchronization, async tasks, interrupts or shared state.',
|
|
19
|
+
},
|
|
20
|
+
reportView: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
enum: ['digest', 'full'],
|
|
23
|
+
description: 'digest (default) prints the readable body; full prints the structured report, which report_diff needs.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
output: {
|
|
27
|
+
schema: {
|
|
28
|
+
type: 'json',
|
|
29
|
+
description: 'Concurrency scan report with findings and summary.',
|
|
30
|
+
},
|
|
31
|
+
render(args, value) {
|
|
32
|
+
return [{ type: 'text', text: renderReportText(value, { full: args.reportView === 'full' }) }];
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
timeoutMs: 10_000,
|
|
36
|
+
isConcurrencySafe: () => true,
|
|
37
|
+
async execute(args) {
|
|
38
|
+
return runConcurrencyScan(args.text);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { REPORT_SCHEMAS, refusalVerdict, verdictOfFindings } from './engine.js';
|
|
2
|
+
/** How a finding names its family in prose. */
|
|
3
|
+
const FAMILY_WORD = {
|
|
4
|
+
threads: 'Concurrency',
|
|
5
|
+
synchronization: 'Synchronization',
|
|
6
|
+
async: 'Async',
|
|
7
|
+
interrupts: 'Interrupt',
|
|
8
|
+
'shared-state': 'Shared-state',
|
|
9
|
+
};
|
|
10
|
+
const KEYWORD_RULES = [
|
|
11
|
+
// Threads: the vocabulary the scan started with.
|
|
12
|
+
{ pattern: /\bthread\s*-?\s*safe\b/i, label: 'thread-safe', absolute: true, family: 'threads' },
|
|
13
|
+
{ pattern: /\block\s*-?\s*free\b/i, label: 'lock-free', absolute: true, family: 'threads' },
|
|
14
|
+
{ pattern: /\bwait\s*-?\s*free\b/i, label: 'wait-free', absolute: true, family: 'threads' },
|
|
15
|
+
{ pattern: /\bno\s+data\s+race\b/i, label: 'no data race', absolute: true, family: 'threads' },
|
|
16
|
+
{ pattern: /\brace\s*-?\s*free\b/i, label: 'race-free', absolute: true, family: 'threads' },
|
|
17
|
+
{ pattern: /\bdata\s+race\b/i, label: 'data race', absolute: false, family: 'threads' },
|
|
18
|
+
{ pattern: /\brace\s+condition\b/i, label: 'race condition', absolute: false, family: 'threads' },
|
|
19
|
+
{ pattern: /\bthread\s*-?\s*safety\b/i, label: 'thread safety', absolute: false, family: 'threads' },
|
|
20
|
+
{ pattern: /\bconcurrent\b/i, label: 'concurrent', absolute: false, family: 'threads' },
|
|
21
|
+
{ pattern: /\bparallel\b/i, label: 'parallel', absolute: false, family: 'threads' },
|
|
22
|
+
{ pattern: /\bmulti-?threaded\b/i, label: 'multi-threaded', absolute: false, family: 'threads' },
|
|
23
|
+
{ pattern: /\bmultithreaded\b/i, label: 'multithreaded', absolute: false, family: 'threads' },
|
|
24
|
+
// Synchronization primitives: a claim that names one is a claim about what it protects.
|
|
25
|
+
{ pattern: /\batomic\b/i, label: 'atomic', absolute: false, family: 'synchronization' },
|
|
26
|
+
{ pattern: /\bsynchronized\b/i, label: 'synchronized', absolute: false, family: 'synchronization' },
|
|
27
|
+
{ pattern: /\bmutex\b/i, label: 'mutex', absolute: false, family: 'synchronization' },
|
|
28
|
+
{ pattern: /\bsemaphore\b/i, label: 'semaphore', absolute: false, family: 'synchronization' },
|
|
29
|
+
{ pattern: /\bspinlock\b/i, label: 'spinlock', absolute: false, family: 'synchronization' },
|
|
30
|
+
{ pattern: /\breentrant\b/i, label: 'reentrant', absolute: false, family: 'synchronization' },
|
|
31
|
+
// Shared state: no primitive named, so the review asks what guards it.
|
|
32
|
+
{ pattern: /\bshared\s+variable\b/i, label: 'shared variable', absolute: false, family: 'shared-state' },
|
|
33
|
+
{ pattern: /\bshared\s+memory\b/i, label: 'shared memory', absolute: false, family: 'shared-state' },
|
|
34
|
+
{ pattern: /\bglobal\s+state\b/i, label: 'global state', absolute: false, family: 'shared-state' },
|
|
35
|
+
// Interrupts.
|
|
36
|
+
{ pattern: /\binterrupt\s*-?\s*safe\b/i, label: 'interrupt-safe', absolute: true, family: 'interrupts' },
|
|
37
|
+
{ pattern: /\bISR\s*-?\s*safe\b/i, label: 'ISR-safe', absolute: true, family: 'interrupts' },
|
|
38
|
+
{ pattern: /\binterrupt\s+safety\b/i, label: 'interrupt safety', absolute: false, family: 'interrupts' },
|
|
39
|
+
{ pattern: /\binterrupt\s+context\b/i, label: 'interrupt context', absolute: false, family: 'interrupts' },
|
|
40
|
+
{ pattern: /\bISR\b/i, label: 'ISR', absolute: false, family: 'interrupts' },
|
|
41
|
+
{ pattern: /\bIRQ\b/i, label: 'IRQ', absolute: false, family: 'interrupts' },
|
|
42
|
+
{ pattern: /\bNMI\b/i, label: 'NMI', absolute: false, family: 'interrupts' },
|
|
43
|
+
{ pattern: /\bcritical\s+section\b/i, label: 'critical section', absolute: false, family: 'interrupts' },
|
|
44
|
+
{ pattern: /\bdisable_irq\b/i, label: 'disable_irq', absolute: false, family: 'interrupts' },
|
|
45
|
+
{ pattern: /\benable_irq\b/i, label: 'enable_irq', absolute: false, family: 'interrupts' },
|
|
46
|
+
{ pattern: /\bspin_lock_irqsave\b/i, label: 'spin_lock_irqsave', absolute: false, family: 'interrupts' },
|
|
47
|
+
// Async: a second execution model, with its own failure modes and its own verifiers. These terms were
|
|
48
|
+
// absent, so an async design mined nothing and was refused as "no claim" - a blind spot that reads as a
|
|
49
|
+
// clean scan, the same defect the Chinese vocabulary had.
|
|
50
|
+
{ pattern: /\bnon-?\s*blocking\b/i, label: 'non-blocking', absolute: true, family: 'async' },
|
|
51
|
+
{ pattern: /\bnever\s+blocks?\b/i, label: 'never blocks', absolute: true, family: 'async' },
|
|
52
|
+
{ pattern: /\bno\s+blocking\b/i, label: 'no blocking', absolute: true, family: 'async' },
|
|
53
|
+
{ pattern: /\basync\b/i, label: 'async', absolute: false, family: 'async' },
|
|
54
|
+
{ pattern: /\bawait(?:s|ing)?\b/i, label: 'await', absolute: false, family: 'async' },
|
|
55
|
+
{ pattern: /\bpromises?\b/i, label: 'promise', absolute: false, family: 'async' },
|
|
56
|
+
{ pattern: /\bevent\s+loop\b/i, label: 'event loop', absolute: false, family: 'async' },
|
|
57
|
+
{ pattern: /\bcallbacks?\b/i, label: 'callback', absolute: false, family: 'async' },
|
|
58
|
+
{ pattern: /\bmicrotasks?\b/i, label: 'microtask', absolute: false, family: 'async' },
|
|
59
|
+
{ pattern: /\bcoroutines?\b/i, label: 'coroutine', absolute: false, family: 'async' },
|
|
60
|
+
{ pattern: /\bfire\s*-?\s*and\s*-?\s*forget\b/i, label: 'fire-and-forget', absolute: false, family: 'async' },
|
|
61
|
+
{ pattern: /\bbackpressure\b/i, label: 'backpressure', absolute: false, family: 'async' },
|
|
62
|
+
{ pattern: /\btask\s+queue\b/i, label: 'task queue', absolute: false, family: 'async' },
|
|
63
|
+
// Chinese terms. The vocabulary was English-only, so a Chinese document about the same subject mined
|
|
64
|
+
// nothing and landed in the no-claim path - a blind spot that read as a clean scan. A negated form is an
|
|
65
|
+
// absolute claim, and the lookbehind keeps the bare term from firing on the same line as well.
|
|
66
|
+
{ pattern: /线程安全/, label: '线程安全 (thread-safe)', absolute: true, family: 'threads' },
|
|
67
|
+
{ pattern: /无数据竞争/, label: '无数据竞争 (no data race)', absolute: true, family: 'threads' },
|
|
68
|
+
{ pattern: /(?<!无)数据竞争/, label: '数据竞争 (data race)', absolute: false, family: 'threads' },
|
|
69
|
+
{ pattern: /无竞态/, label: '无竞态 (race-free)', absolute: true, family: 'threads' },
|
|
70
|
+
{ pattern: /(?:无锁|免锁)/, label: '无锁 (lock-free)', absolute: true, family: 'threads' },
|
|
71
|
+
{ pattern: /(?:中断安全|ISR安全)/, label: '中断安全 (interrupt-safe)', absolute: true, family: 'interrupts' },
|
|
72
|
+
{ pattern: /竞态条件/, label: '竞态条件 (race condition)', absolute: false, family: 'threads' },
|
|
73
|
+
{ pattern: /并发/, label: '并发 (concurrent)', absolute: false, family: 'threads' },
|
|
74
|
+
{ pattern: /并行/, label: '并行 (parallel)', absolute: false, family: 'threads' },
|
|
75
|
+
{ pattern: /多线程/, label: '多线程 (multi-threaded)', absolute: false, family: 'threads' },
|
|
76
|
+
{ pattern: /原子/, label: '原子 (atomic)', absolute: false, family: 'synchronization' },
|
|
77
|
+
{ pattern: /互斥/, label: '互斥 (mutex)', absolute: false, family: 'synchronization' },
|
|
78
|
+
{ pattern: /信号量/, label: '信号量 (semaphore)', absolute: false, family: 'synchronization' },
|
|
79
|
+
{ pattern: /自旋锁/, label: '自旋锁 (spinlock)', absolute: false, family: 'synchronization' },
|
|
80
|
+
{ pattern: /共享变量/, label: '共享变量 (shared variable)', absolute: false, family: 'shared-state' },
|
|
81
|
+
{ pattern: /共享内存/, label: '共享内存 (shared memory)', absolute: false, family: 'shared-state' },
|
|
82
|
+
{ pattern: /全局状态/, label: '全局状态 (global state)', absolute: false, family: 'shared-state' },
|
|
83
|
+
{ pattern: /可重入/, label: '可重入 (reentrant)', absolute: false, family: 'synchronization' },
|
|
84
|
+
{ pattern: /临界区/, label: '临界区 (critical section)', absolute: false, family: 'interrupts' },
|
|
85
|
+
{ pattern: /关中断/, label: '关中断 (disable interrupt)', absolute: false, family: 'interrupts' },
|
|
86
|
+
{ pattern: /(?:非阻塞|不阻塞|无阻塞|零阻塞)/, label: '非阻塞 (non-blocking)', absolute: true, family: 'async' },
|
|
87
|
+
{ pattern: /异步/, label: '异步 (async)', absolute: false, family: 'async' },
|
|
88
|
+
{ pattern: /回调/, label: '回调 (callback)', absolute: false, family: 'async' },
|
|
89
|
+
{ pattern: /事件循环/, label: '事件循环 (event loop)', absolute: false, family: 'async' },
|
|
90
|
+
{ pattern: /微任务/, label: '微任务 (microtask)', absolute: false, family: 'async' },
|
|
91
|
+
{ pattern: /协程/, label: '协程 (coroutine)', absolute: false, family: 'async' },
|
|
92
|
+
{ pattern: /背压/, label: '背压 (backpressure)', absolute: false, family: 'async' },
|
|
93
|
+
];
|
|
94
|
+
/**
|
|
95
|
+
* The text to scan, or a refusal naming what arrived instead.
|
|
96
|
+
*
|
|
97
|
+
* `runConcurrencyScan(undefined)` threw a read of `split` on undefined. The scan proves nothing
|
|
98
|
+
* either way, so a caller who passed the wrong variable must be told which one was wrong rather
|
|
99
|
+
* than left with a crash.
|
|
100
|
+
*/
|
|
101
|
+
function requireScanText(value) {
|
|
102
|
+
if (typeof value !== 'string') {
|
|
103
|
+
const arrived = value === null ? 'null' : Array.isArray(value) ? 'an array' : typeof value;
|
|
104
|
+
throw new Error('text must be a string to scan, got ' + arrived + '. Pass the source or the notes to scan for concurrency claims.');
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
export function runConcurrencyScan(text) {
|
|
109
|
+
const source = requireScanText(text);
|
|
110
|
+
const lines = source.split(/\r?\n/);
|
|
111
|
+
const findings = [];
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const suggestionsFor = (family) => {
|
|
114
|
+
if (family === 'async') {
|
|
115
|
+
return [
|
|
116
|
+
'TLA+ (exhaustive interleaving), or model_compose_verify for a handshake between two tasks',
|
|
117
|
+
'model_verify with maxTicks + tickEvents (a timeout is a deadline), or model_export -> UPPAAL for clocks',
|
|
118
|
+
'A runtime or static check for the three things a model cannot see: a blocking call on the event loop, a missing await (fire-and-forget), a lock held across an await',
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
if (family === 'threads') {
|
|
122
|
+
return ['TSan / Helgrind (runtime data-race detection)', 'CBMC / static analysis (proof-oriented)', 'TLA+ (exhaustive interleaving model)'];
|
|
123
|
+
}
|
|
124
|
+
if (family === 'interrupts') {
|
|
125
|
+
return ['RTOS-aware analysis (interrupt latency, priority inversion)', 'CBMC (state-machine + ISR interleaving proof)', 'TLA+ (preemption/exclusion model)'];
|
|
126
|
+
}
|
|
127
|
+
return ['Dedicated concurrency verification (TSan, CBMC, TLA+, or RTOS-specific analysis)'];
|
|
128
|
+
};
|
|
129
|
+
lines.forEach((line, index) => {
|
|
130
|
+
const lineNumber = index + 1;
|
|
131
|
+
const lower = line.toLowerCase();
|
|
132
|
+
for (const rule of KEYWORD_RULES) {
|
|
133
|
+
if (!rule.pattern.test(line))
|
|
134
|
+
continue;
|
|
135
|
+
const key = rule.label + ':' + lineNumber;
|
|
136
|
+
if (seen.has(key))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(key);
|
|
139
|
+
const finding = {
|
|
140
|
+
code: rule.absolute ? 'CONCURRENCY_ABSOLUTE_CLAIM' : 'CONCURRENCY_KEYWORD',
|
|
141
|
+
severity: rule.absolute ? 'error' : 'warning',
|
|
142
|
+
message: rule.absolute
|
|
143
|
+
? FAMILY_WORD[rule.family] + ' safety claim "' + rule.label + '" detected; this plugin does not verify it — it needs dedicated verification.'
|
|
144
|
+
: FAMILY_WORD[rule.family] + '-related term "' + rule.label + '" detected; review whether the plan addresses this risk.',
|
|
145
|
+
line: lineNumber,
|
|
146
|
+
snippet: line.trim().slice(0, 200),
|
|
147
|
+
keyword: rule.label,
|
|
148
|
+
family: rule.family,
|
|
149
|
+
...(rule.absolute ? { suggestions: suggestionsFor(rule.family) } : {}),
|
|
150
|
+
};
|
|
151
|
+
findings.push(finding);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const families = new Set(findings.map((finding) => finding.family));
|
|
155
|
+
const errors = findings.filter((finding) => finding.severity === 'error').length;
|
|
156
|
+
const warnings = findings.filter((finding) => finding.severity === 'warning').length;
|
|
157
|
+
const absoluteClaims = findings.filter((finding) => finding.code === 'CONCURRENCY_ABSOLUTE_CLAIM').length;
|
|
158
|
+
const nextSteps = [];
|
|
159
|
+
if (absoluteClaims > 0) {
|
|
160
|
+
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.');
|
|
161
|
+
}
|
|
162
|
+
if (findings.length > absoluteClaims) {
|
|
163
|
+
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.');
|
|
164
|
+
}
|
|
165
|
+
if (families.has('async')) {
|
|
166
|
+
nextSteps.push('For an async design, three defects are invisible to this scan and to any model: a blocking call on the event loop, an await that never happens (fire-and-forget), and a lock held across an await. Name them in the model, or check them at runtime.');
|
|
167
|
+
}
|
|
168
|
+
nextSteps.push('This scan mines claims; it does not prove concurrency safety — route each one to the tool named in its suggestions.');
|
|
169
|
+
if (findings.length === 0) {
|
|
170
|
+
// Nothing was mined, so nothing was examined. Reporting `pass` here said "no concurrency problem"
|
|
171
|
+
// about a text this scan could not see: the same false green as a check that examined nothing, and the
|
|
172
|
+
// common case, because a plan that is not about concurrency at all lands here too.
|
|
173
|
+
const reason = 'the text contains no concurrency claim this scan can mine, so there is nothing to scan. It is either not about concurrency, or it is written in a vocabulary this scan does not match: it reads English terms such as mutex, atomic, thread-safe, async, await and event loop, and Chinese ones such as 互斥, 原子, 线程安全 and 事件循环';
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
ran: false,
|
|
177
|
+
...refusalVerdict(reason),
|
|
178
|
+
schema: REPORT_SCHEMAS.concurrency,
|
|
179
|
+
errorCode: 'CONCURRENCY_NO_CLAIM',
|
|
180
|
+
error: reason,
|
|
181
|
+
findings: [],
|
|
182
|
+
summary: { lines: lines.length, keywords: 0, absoluteClaims: 0, warnings: 0, errors: 0 },
|
|
183
|
+
hashes: {},
|
|
184
|
+
nextSteps: [
|
|
185
|
+
'If the text does discuss concurrency, name the claim explicitly — the resource, the guard, and what it protects — and scan it again.',
|
|
186
|
+
'This scan mines claims; it does not prove concurrency safety — route each one to the tool named in its suggestions.',
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
ran: true,
|
|
193
|
+
...verdictOfFindings(findings),
|
|
194
|
+
schema: REPORT_SCHEMAS.concurrency,
|
|
195
|
+
findings,
|
|
196
|
+
summary: {
|
|
197
|
+
lines: lines.length,
|
|
198
|
+
keywords: findings.length,
|
|
199
|
+
absoluteClaims,
|
|
200
|
+
warnings,
|
|
201
|
+
errors,
|
|
202
|
+
},
|
|
203
|
+
hashes: {},
|
|
204
|
+
nextSteps,
|
|
205
|
+
};
|
|
206
|
+
}
|