@fairfox/polly 0.40.0 → 0.49.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.md +16 -14
- package/dist/src/background/index.d.ts +1 -0
- package/dist/src/background/index.js +65 -2
- package/dist/src/background/index.js.map +8 -7
- package/dist/src/background/message-router.js.map +4 -4
- package/dist/src/client/index.js.map +2 -2
- package/dist/src/elysia/index.js.map +1 -1
- package/dist/src/index.js.map +6 -6
- package/dist/src/mesh-node.js.map +3 -3
- package/dist/src/mesh.js +25 -4
- package/dist/src/mesh.js.map +12 -12
- package/dist/src/peer.js.map +4 -4
- package/dist/src/polly-ui/registry.d.ts +16 -0
- package/dist/src/polly-ui/registry.generated.d.ts +20 -0
- package/dist/src/shared/adapters/index.js.map +3 -3
- package/dist/src/shared/lib/context-helpers.js.map +4 -4
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +24 -1
- package/dist/src/shared/lib/message-bus.js.map +4 -4
- package/dist/src/shared/lib/resource.js.map +3 -3
- package/dist/src/shared/lib/state.js.map +3 -3
- package/dist/src/shared/state/app-state.js.map +3 -3
- package/dist/tools/quality/src/attest.d.ts +55 -0
- package/dist/tools/quality/src/cache.d.ts +34 -0
- package/dist/tools/quality/src/cli.js +1881 -2
- package/dist/tools/quality/src/cli.js.map +14 -4
- package/dist/tools/quality/src/config.d.ts +18 -0
- package/dist/tools/quality/src/host.d.ts +46 -0
- package/dist/tools/quality/src/index.d.ts +7 -0
- package/dist/tools/quality/src/index.js +1780 -1
- package/dist/tools/quality/src/index.js.map +14 -4
- package/dist/tools/quality/src/plugins/cliche-checks.d.ts +16 -0
- package/dist/tools/quality/src/plugins/core-checks.d.ts +20 -0
- package/dist/tools/quality/src/plugins/core.d.ts +18 -0
- package/dist/tools/quality/src/plugins/extra-checks.d.ts +14 -0
- package/dist/tools/quality/src/plugins/import-checks.d.ts +16 -0
- package/dist/tools/quality/src/plugins/polly-ui.d.ts +31 -0
- package/dist/tools/quality/src/types.d.ts +104 -0
- package/dist/tools/test/src/browser/index.js.map +1 -1
- package/dist/tools/test/src/browser/run.js.map +1 -1
- package/dist/tools/verify/src/cli.js +325 -227
- package/dist/tools/verify/src/cli.js.map +10 -9
- package/dist/tools/verify/src/config.js.map +2 -2
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +9 -2
|
@@ -17,6 +17,236 @@ var __export = (target, all) => {
|
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
18
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
19
|
|
|
20
|
+
// tools/verify/src/analysis/expression-validator.ts
|
|
21
|
+
function extractFieldRefs(expression) {
|
|
22
|
+
const refs = [];
|
|
23
|
+
for (const m of expression.matchAll(SIGNAL_VALUE_FIELD)) {
|
|
24
|
+
const prefix = m[1];
|
|
25
|
+
const suffix = m[2];
|
|
26
|
+
refs.push(`${prefix}.${suffix}`);
|
|
27
|
+
}
|
|
28
|
+
for (const m of expression.matchAll(SIGNAL_VALUE_BARE)) {
|
|
29
|
+
if (m[1])
|
|
30
|
+
refs.push(m[1]);
|
|
31
|
+
}
|
|
32
|
+
for (const m of expression.matchAll(STATE_DOT_FIELD)) {
|
|
33
|
+
if (m[1])
|
|
34
|
+
refs.push(m[1]);
|
|
35
|
+
}
|
|
36
|
+
return [...new Set(refs)];
|
|
37
|
+
}
|
|
38
|
+
function fieldInConfig(fieldRef, configKeys, stateConfig) {
|
|
39
|
+
if (configKeys.has(fieldRef))
|
|
40
|
+
return true;
|
|
41
|
+
const underscored = fieldRef.replace(/\./g, "_");
|
|
42
|
+
if (configKeys.has(underscored))
|
|
43
|
+
return true;
|
|
44
|
+
const dotted = fieldRef.replace(/_/g, ".");
|
|
45
|
+
if (configKeys.has(dotted))
|
|
46
|
+
return true;
|
|
47
|
+
if (stateConfig && fieldRef.endsWith(".length")) {
|
|
48
|
+
const parent = fieldRef.slice(0, -".length".length);
|
|
49
|
+
const parentEntry = resolveConfigEntry(parent, stateConfig);
|
|
50
|
+
if (parentEntry !== undefined && isArrayLike(parentEntry))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function resolveConfigEntry(fieldRef, stateConfig) {
|
|
56
|
+
if (fieldRef in stateConfig)
|
|
57
|
+
return stateConfig[fieldRef];
|
|
58
|
+
const underscored = fieldRef.replace(/\./g, "_");
|
|
59
|
+
if (underscored in stateConfig)
|
|
60
|
+
return stateConfig[underscored];
|
|
61
|
+
const dotted = fieldRef.replace(/_/g, ".");
|
|
62
|
+
if (dotted in stateConfig)
|
|
63
|
+
return stateConfig[dotted];
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
function isArrayLike(configEntry) {
|
|
67
|
+
if (configEntry && typeof configEntry === "object") {
|
|
68
|
+
const obj = configEntry;
|
|
69
|
+
if (obj["type"] === "array")
|
|
70
|
+
return true;
|
|
71
|
+
if ("maxLength" in obj)
|
|
72
|
+
return true;
|
|
73
|
+
if ("maxSize" in obj)
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function isNullable(configEntry) {
|
|
79
|
+
if (Array.isArray(configEntry)) {
|
|
80
|
+
return configEntry.includes(null);
|
|
81
|
+
}
|
|
82
|
+
if (configEntry && typeof configEntry === "object") {
|
|
83
|
+
const obj = configEntry;
|
|
84
|
+
if (obj["abstract"] === true)
|
|
85
|
+
return true;
|
|
86
|
+
if (Array.isArray(obj["values"])) {
|
|
87
|
+
return obj["values"].includes(null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
function checkUnmodeledFields(expression, configKeys, stateConfig, messageType, conditionType, location) {
|
|
93
|
+
const warnings = [];
|
|
94
|
+
const refs = extractFieldRefs(expression);
|
|
95
|
+
for (const ref of refs) {
|
|
96
|
+
if (!fieldInConfig(ref, configKeys, stateConfig)) {
|
|
97
|
+
warnings.push({
|
|
98
|
+
kind: "unmodeled_field",
|
|
99
|
+
message: `${conditionType}() in ${messageType} references unmodeled field '${ref}'`,
|
|
100
|
+
messageType,
|
|
101
|
+
conditionType,
|
|
102
|
+
expression,
|
|
103
|
+
location,
|
|
104
|
+
suggestion: `Add '${ref}' to state config or remove from ${conditionType}()`
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return warnings;
|
|
109
|
+
}
|
|
110
|
+
function checkUnsupportedMethods(expression, configKeys, stateConfig, messageType, conditionType, location) {
|
|
111
|
+
const match = expression.match(UNSUPPORTED_METHODS);
|
|
112
|
+
if (!match)
|
|
113
|
+
return [];
|
|
114
|
+
const refs = extractFieldRefs(expression);
|
|
115
|
+
const allUnmodeled = refs.length > 0 && refs.every((r) => !fieldInConfig(r, configKeys, stateConfig));
|
|
116
|
+
if (allUnmodeled)
|
|
117
|
+
return [];
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
kind: "unsupported_method",
|
|
121
|
+
message: `${conditionType}() in ${messageType} uses .${match[1]}() which cannot be translated to TLA+`,
|
|
122
|
+
messageType,
|
|
123
|
+
conditionType,
|
|
124
|
+
expression,
|
|
125
|
+
location,
|
|
126
|
+
suggestion: `Rewrite without .${match[1]}() or model the check as a boolean state field`
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
function checkOptionalChaining(expression, messageType, conditionType, location) {
|
|
131
|
+
if (!OPTIONAL_CHAIN.test(expression))
|
|
132
|
+
return [];
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
kind: "optional_chaining",
|
|
136
|
+
message: `${conditionType}() in ${messageType} uses optional chaining (?.)`,
|
|
137
|
+
messageType,
|
|
138
|
+
conditionType,
|
|
139
|
+
expression,
|
|
140
|
+
location,
|
|
141
|
+
suggestion: "Optional chaining translation is fragile — consider explicit null checks or restructuring"
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
function checkNullComparisons(expression, stateConfig, configKeys, messageType, conditionType, location) {
|
|
146
|
+
if (!NULL_COMPARISON.test(expression))
|
|
147
|
+
return [];
|
|
148
|
+
const warnings = [];
|
|
149
|
+
const refs = extractFieldRefs(expression);
|
|
150
|
+
for (const ref of refs) {
|
|
151
|
+
if (!fieldInConfig(ref, configKeys, stateConfig))
|
|
152
|
+
continue;
|
|
153
|
+
const entry = resolveConfigEntry(ref, stateConfig);
|
|
154
|
+
if (entry !== undefined && !isNullable(entry)) {
|
|
155
|
+
warnings.push({
|
|
156
|
+
kind: "null_comparison",
|
|
157
|
+
message: `${conditionType}() in ${messageType} compares '${ref}' to null but field is not nullable`,
|
|
158
|
+
messageType,
|
|
159
|
+
conditionType,
|
|
160
|
+
expression,
|
|
161
|
+
location,
|
|
162
|
+
suggestion: `Add null to '${ref}' values in state config, or remove the null check`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return warnings;
|
|
167
|
+
}
|
|
168
|
+
function checkWeakPostconditions(expression, handler, messageType, conditionType, location) {
|
|
169
|
+
if (conditionType !== "ensures")
|
|
170
|
+
return [];
|
|
171
|
+
const match = expression.match(WEAK_NEGATION);
|
|
172
|
+
if (!match)
|
|
173
|
+
return [];
|
|
174
|
+
const refs = extractFieldRefs(expression);
|
|
175
|
+
if (refs.length === 0)
|
|
176
|
+
return [];
|
|
177
|
+
for (const ref of refs) {
|
|
178
|
+
const underscoredRef = ref.replace(/\./g, "_");
|
|
179
|
+
const hasAssignment = handler.assignments.some((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
|
|
180
|
+
if (hasAssignment) {
|
|
181
|
+
const assignment = handler.assignments.find((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
|
|
182
|
+
const assignedValue = assignment ? JSON.stringify(assignment.value) : "<value>";
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
kind: "weak_postcondition",
|
|
186
|
+
message: `ensures() in ${messageType} uses !== for '${ref}' which has a concrete assignment`,
|
|
187
|
+
messageType,
|
|
188
|
+
conditionType,
|
|
189
|
+
expression,
|
|
190
|
+
location,
|
|
191
|
+
suggestion: `Consider ensures(${ref} === ${assignedValue}) for a stronger postcondition`
|
|
192
|
+
}
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
function validateExpressions(handlers, stateConfig) {
|
|
199
|
+
const warnings = [];
|
|
200
|
+
let validCount = 0;
|
|
201
|
+
const configKeys = new Set(Object.keys(stateConfig));
|
|
202
|
+
for (const handler of handlers) {
|
|
203
|
+
const conditions = [
|
|
204
|
+
...handler.preconditions.map((c) => ({
|
|
205
|
+
cond: c,
|
|
206
|
+
type: "requires"
|
|
207
|
+
})),
|
|
208
|
+
...handler.postconditions.map((c) => ({
|
|
209
|
+
cond: c,
|
|
210
|
+
type: "ensures"
|
|
211
|
+
}))
|
|
212
|
+
];
|
|
213
|
+
for (const { cond, type } of conditions) {
|
|
214
|
+
const refs = extractFieldRefs(cond.expression);
|
|
215
|
+
const isPayloadOnly = refs.length === 0 && PAYLOAD_REF.test(cond.expression);
|
|
216
|
+
const loc = {
|
|
217
|
+
file: handler.location.file,
|
|
218
|
+
line: cond.location.line,
|
|
219
|
+
column: cond.location.column
|
|
220
|
+
};
|
|
221
|
+
const condWarnings = [];
|
|
222
|
+
if (!isPayloadOnly) {
|
|
223
|
+
condWarnings.push(...checkUnmodeledFields(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
|
|
224
|
+
}
|
|
225
|
+
condWarnings.push(...checkUnsupportedMethods(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
|
|
226
|
+
condWarnings.push(...checkOptionalChaining(cond.expression, handler.messageType, type, loc));
|
|
227
|
+
condWarnings.push(...checkNullComparisons(cond.expression, stateConfig, configKeys, handler.messageType, type, loc));
|
|
228
|
+
condWarnings.push(...checkWeakPostconditions(cond.expression, handler, handler.messageType, type, loc));
|
|
229
|
+
if (condWarnings.length === 0) {
|
|
230
|
+
validCount++;
|
|
231
|
+
} else {
|
|
232
|
+
warnings.push(...condWarnings);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { warnings, validCount, warnCount: warnings.length };
|
|
237
|
+
}
|
|
238
|
+
var SIGNAL_VALUE_FIELD, SIGNAL_VALUE_BARE, STATE_DOT_FIELD, PAYLOAD_REF, UNSUPPORTED_METHODS, OPTIONAL_CHAIN, NULL_COMPARISON, WEAK_NEGATION;
|
|
239
|
+
var init_expression_validator = __esm(() => {
|
|
240
|
+
SIGNAL_VALUE_FIELD = /([a-zA-Z_]\w*)\.value\.([a-zA-Z_][\w.]*)/g;
|
|
241
|
+
SIGNAL_VALUE_BARE = /([a-zA-Z_]\w*)\.value\b(?!\.)/g;
|
|
242
|
+
STATE_DOT_FIELD = /state\.([a-zA-Z_][\w.]*)/g;
|
|
243
|
+
PAYLOAD_REF = /payload\.\w+/;
|
|
244
|
+
UNSUPPORTED_METHODS = /\.(some|every|find|filter)\s*\(/;
|
|
245
|
+
OPTIONAL_CHAIN = /\?\./;
|
|
246
|
+
NULL_COMPARISON = /(===?\s*null|!==?\s*null|null\s*===?|null\s*!==?)/;
|
|
247
|
+
WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
|
|
248
|
+
});
|
|
249
|
+
|
|
20
250
|
// tools/verify/src/analysis/non-interference.ts
|
|
21
251
|
var exports_non_interference = {};
|
|
22
252
|
__export(exports_non_interference, {
|
|
@@ -59,6 +289,80 @@ function checkNonInterference(subsystems, handlers) {
|
|
|
59
289
|
};
|
|
60
290
|
}
|
|
61
291
|
|
|
292
|
+
// tools/verify/src/analysis/precondition-locality.ts
|
|
293
|
+
var exports_precondition_locality = {};
|
|
294
|
+
__export(exports_precondition_locality, {
|
|
295
|
+
checkPreconditionLocality: () => checkPreconditionLocality
|
|
296
|
+
});
|
|
297
|
+
function findOwner(fieldRef, fieldOwner) {
|
|
298
|
+
const candidates = [fieldRef, fieldRef.replace(/\./g, "_"), fieldRef.replace(/_/g, ".")];
|
|
299
|
+
for (const c of candidates) {
|
|
300
|
+
const owner = fieldOwner.get(c);
|
|
301
|
+
if (owner)
|
|
302
|
+
return owner;
|
|
303
|
+
}
|
|
304
|
+
if (fieldRef.endsWith(".length")) {
|
|
305
|
+
return findOwner(fieldRef.slice(0, -".length".length), fieldOwner);
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
function buildFieldOwner(subsystems) {
|
|
310
|
+
const fieldOwner = new Map;
|
|
311
|
+
for (const [name, sub] of Object.entries(subsystems)) {
|
|
312
|
+
for (const field of sub.state) {
|
|
313
|
+
fieldOwner.set(field, name);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return fieldOwner;
|
|
317
|
+
}
|
|
318
|
+
function buildHandlerSubsystem(subsystems) {
|
|
319
|
+
const handlerSubsystem = new Map;
|
|
320
|
+
for (const [name, sub] of Object.entries(subsystems)) {
|
|
321
|
+
for (const h of sub.handlers) {
|
|
322
|
+
handlerSubsystem.set(h, name);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return handlerSubsystem;
|
|
326
|
+
}
|
|
327
|
+
function violationsForPrecondition(handler, subsystemName, precondition, fieldOwner) {
|
|
328
|
+
const out = [];
|
|
329
|
+
for (const ref of extractFieldRefs(precondition.expression)) {
|
|
330
|
+
const owner = findOwner(ref, fieldOwner);
|
|
331
|
+
if (!owner || owner === subsystemName)
|
|
332
|
+
continue;
|
|
333
|
+
out.push({
|
|
334
|
+
handler: handler.messageType,
|
|
335
|
+
subsystem: subsystemName,
|
|
336
|
+
readsFrom: ref,
|
|
337
|
+
ownedBy: owner,
|
|
338
|
+
expression: precondition.expression,
|
|
339
|
+
location: {
|
|
340
|
+
file: handler.location?.file ?? "",
|
|
341
|
+
line: precondition.location?.line ?? 0,
|
|
342
|
+
column: precondition.location?.column ?? 0
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
function checkPreconditionLocality(subsystems, handlers) {
|
|
349
|
+
const fieldOwner = buildFieldOwner(subsystems);
|
|
350
|
+
const handlerSubsystem = buildHandlerSubsystem(subsystems);
|
|
351
|
+
const violations = [];
|
|
352
|
+
for (const handler of handlers) {
|
|
353
|
+
const subsystemName = handlerSubsystem.get(handler.messageType);
|
|
354
|
+
if (!subsystemName)
|
|
355
|
+
continue;
|
|
356
|
+
for (const precondition of handler.preconditions ?? []) {
|
|
357
|
+
violations.push(...violationsForPrecondition(handler, subsystemName, precondition, fieldOwner));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { valid: violations.length === 0, violations };
|
|
361
|
+
}
|
|
362
|
+
var init_precondition_locality = __esm(() => {
|
|
363
|
+
init_expression_validator();
|
|
364
|
+
});
|
|
365
|
+
|
|
62
366
|
// tools/verify/src/codegen/invariants.ts
|
|
63
367
|
import { Node as Node3, Project as Project3 } from "ts-morph";
|
|
64
368
|
|
|
@@ -2657,234 +2961,10 @@ var __dirname = "/Users/AJT/projects/polly/tools/verify/src/runner";
|
|
|
2657
2961
|
var init_docker = () => {};
|
|
2658
2962
|
|
|
2659
2963
|
// tools/verify/src/cli.ts
|
|
2964
|
+
init_expression_validator();
|
|
2660
2965
|
import * as fs4 from "node:fs";
|
|
2661
2966
|
import * as path4 from "node:path";
|
|
2662
2967
|
|
|
2663
|
-
// tools/verify/src/analysis/expression-validator.ts
|
|
2664
|
-
var SIGNAL_VALUE_FIELD = /([a-zA-Z_]\w*)\.value\.([a-zA-Z_][\w.]*)/g;
|
|
2665
|
-
var SIGNAL_VALUE_BARE = /([a-zA-Z_]\w*)\.value\b(?!\.)/g;
|
|
2666
|
-
var STATE_DOT_FIELD = /state\.([a-zA-Z_][\w.]*)/g;
|
|
2667
|
-
var PAYLOAD_REF = /payload\.\w+/;
|
|
2668
|
-
function extractFieldRefs(expression) {
|
|
2669
|
-
const refs = [];
|
|
2670
|
-
for (const m of expression.matchAll(SIGNAL_VALUE_FIELD)) {
|
|
2671
|
-
const prefix = m[1];
|
|
2672
|
-
const suffix = m[2];
|
|
2673
|
-
refs.push(`${prefix}.${suffix}`);
|
|
2674
|
-
}
|
|
2675
|
-
for (const m of expression.matchAll(SIGNAL_VALUE_BARE)) {
|
|
2676
|
-
refs.push(m[1]);
|
|
2677
|
-
}
|
|
2678
|
-
for (const m of expression.matchAll(STATE_DOT_FIELD)) {
|
|
2679
|
-
refs.push(m[1]);
|
|
2680
|
-
}
|
|
2681
|
-
return [...new Set(refs)];
|
|
2682
|
-
}
|
|
2683
|
-
function fieldInConfig(fieldRef, configKeys, stateConfig) {
|
|
2684
|
-
if (configKeys.has(fieldRef))
|
|
2685
|
-
return true;
|
|
2686
|
-
const underscored = fieldRef.replace(/\./g, "_");
|
|
2687
|
-
if (configKeys.has(underscored))
|
|
2688
|
-
return true;
|
|
2689
|
-
const dotted = fieldRef.replace(/_/g, ".");
|
|
2690
|
-
if (configKeys.has(dotted))
|
|
2691
|
-
return true;
|
|
2692
|
-
if (stateConfig && fieldRef.endsWith(".length")) {
|
|
2693
|
-
const parent = fieldRef.slice(0, -".length".length);
|
|
2694
|
-
const parentEntry = resolveConfigEntry(parent, stateConfig);
|
|
2695
|
-
if (parentEntry !== undefined && isArrayLike(parentEntry))
|
|
2696
|
-
return true;
|
|
2697
|
-
}
|
|
2698
|
-
return false;
|
|
2699
|
-
}
|
|
2700
|
-
function resolveConfigEntry(fieldRef, stateConfig) {
|
|
2701
|
-
if (fieldRef in stateConfig)
|
|
2702
|
-
return stateConfig[fieldRef];
|
|
2703
|
-
const underscored = fieldRef.replace(/\./g, "_");
|
|
2704
|
-
if (underscored in stateConfig)
|
|
2705
|
-
return stateConfig[underscored];
|
|
2706
|
-
const dotted = fieldRef.replace(/_/g, ".");
|
|
2707
|
-
if (dotted in stateConfig)
|
|
2708
|
-
return stateConfig[dotted];
|
|
2709
|
-
return;
|
|
2710
|
-
}
|
|
2711
|
-
function isArrayLike(configEntry) {
|
|
2712
|
-
if (configEntry && typeof configEntry === "object") {
|
|
2713
|
-
const obj = configEntry;
|
|
2714
|
-
if (obj.type === "array")
|
|
2715
|
-
return true;
|
|
2716
|
-
if ("maxLength" in obj)
|
|
2717
|
-
return true;
|
|
2718
|
-
if ("maxSize" in obj)
|
|
2719
|
-
return true;
|
|
2720
|
-
}
|
|
2721
|
-
return false;
|
|
2722
|
-
}
|
|
2723
|
-
function isNullable(configEntry) {
|
|
2724
|
-
if (Array.isArray(configEntry)) {
|
|
2725
|
-
return configEntry.includes(null);
|
|
2726
|
-
}
|
|
2727
|
-
if (configEntry && typeof configEntry === "object") {
|
|
2728
|
-
const obj = configEntry;
|
|
2729
|
-
if (obj.abstract === true)
|
|
2730
|
-
return true;
|
|
2731
|
-
if (Array.isArray(obj.values)) {
|
|
2732
|
-
return obj.values.includes(null);
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
return false;
|
|
2736
|
-
}
|
|
2737
|
-
var UNSUPPORTED_METHODS = /\.(some|every|find|filter)\s*\(/;
|
|
2738
|
-
var OPTIONAL_CHAIN = /\?\./;
|
|
2739
|
-
var NULL_COMPARISON = /(===?\s*null|!==?\s*null|null\s*===?|null\s*!==?)/;
|
|
2740
|
-
function checkUnmodeledFields(expression, configKeys, stateConfig, messageType, conditionType, location) {
|
|
2741
|
-
const warnings = [];
|
|
2742
|
-
const refs = extractFieldRefs(expression);
|
|
2743
|
-
for (const ref of refs) {
|
|
2744
|
-
if (!fieldInConfig(ref, configKeys, stateConfig)) {
|
|
2745
|
-
warnings.push({
|
|
2746
|
-
kind: "unmodeled_field",
|
|
2747
|
-
message: `${conditionType}() in ${messageType} references unmodeled field '${ref}'`,
|
|
2748
|
-
messageType,
|
|
2749
|
-
conditionType,
|
|
2750
|
-
expression,
|
|
2751
|
-
location,
|
|
2752
|
-
suggestion: `Add '${ref}' to state config or remove from ${conditionType}()`
|
|
2753
|
-
});
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
return warnings;
|
|
2757
|
-
}
|
|
2758
|
-
function checkUnsupportedMethods(expression, configKeys, stateConfig, messageType, conditionType, location) {
|
|
2759
|
-
const match = expression.match(UNSUPPORTED_METHODS);
|
|
2760
|
-
if (!match)
|
|
2761
|
-
return [];
|
|
2762
|
-
const refs = extractFieldRefs(expression);
|
|
2763
|
-
const allUnmodeled = refs.length > 0 && refs.every((r) => !fieldInConfig(r, configKeys, stateConfig));
|
|
2764
|
-
if (allUnmodeled)
|
|
2765
|
-
return [];
|
|
2766
|
-
return [
|
|
2767
|
-
{
|
|
2768
|
-
kind: "unsupported_method",
|
|
2769
|
-
message: `${conditionType}() in ${messageType} uses .${match[1]}() which cannot be translated to TLA+`,
|
|
2770
|
-
messageType,
|
|
2771
|
-
conditionType,
|
|
2772
|
-
expression,
|
|
2773
|
-
location,
|
|
2774
|
-
suggestion: `Rewrite without .${match[1]}() or model the check as a boolean state field`
|
|
2775
|
-
}
|
|
2776
|
-
];
|
|
2777
|
-
}
|
|
2778
|
-
function checkOptionalChaining(expression, messageType, conditionType, location) {
|
|
2779
|
-
if (!OPTIONAL_CHAIN.test(expression))
|
|
2780
|
-
return [];
|
|
2781
|
-
return [
|
|
2782
|
-
{
|
|
2783
|
-
kind: "optional_chaining",
|
|
2784
|
-
message: `${conditionType}() in ${messageType} uses optional chaining (?.)`,
|
|
2785
|
-
messageType,
|
|
2786
|
-
conditionType,
|
|
2787
|
-
expression,
|
|
2788
|
-
location,
|
|
2789
|
-
suggestion: "Optional chaining translation is fragile — consider explicit null checks or restructuring"
|
|
2790
|
-
}
|
|
2791
|
-
];
|
|
2792
|
-
}
|
|
2793
|
-
function checkNullComparisons(expression, stateConfig, configKeys, messageType, conditionType, location) {
|
|
2794
|
-
if (!NULL_COMPARISON.test(expression))
|
|
2795
|
-
return [];
|
|
2796
|
-
const warnings = [];
|
|
2797
|
-
const refs = extractFieldRefs(expression);
|
|
2798
|
-
for (const ref of refs) {
|
|
2799
|
-
if (!fieldInConfig(ref, configKeys, stateConfig))
|
|
2800
|
-
continue;
|
|
2801
|
-
const entry = resolveConfigEntry(ref, stateConfig);
|
|
2802
|
-
if (entry !== undefined && !isNullable(entry)) {
|
|
2803
|
-
warnings.push({
|
|
2804
|
-
kind: "null_comparison",
|
|
2805
|
-
message: `${conditionType}() in ${messageType} compares '${ref}' to null but field is not nullable`,
|
|
2806
|
-
messageType,
|
|
2807
|
-
conditionType,
|
|
2808
|
-
expression,
|
|
2809
|
-
location,
|
|
2810
|
-
suggestion: `Add null to '${ref}' values in state config, or remove the null check`
|
|
2811
|
-
});
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
return warnings;
|
|
2815
|
-
}
|
|
2816
|
-
var WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
|
|
2817
|
-
function checkWeakPostconditions(expression, handler, messageType, conditionType, location) {
|
|
2818
|
-
if (conditionType !== "ensures")
|
|
2819
|
-
return [];
|
|
2820
|
-
const match = expression.match(WEAK_NEGATION);
|
|
2821
|
-
if (!match)
|
|
2822
|
-
return [];
|
|
2823
|
-
const refs = extractFieldRefs(expression);
|
|
2824
|
-
if (refs.length === 0)
|
|
2825
|
-
return [];
|
|
2826
|
-
for (const ref of refs) {
|
|
2827
|
-
const underscoredRef = ref.replace(/\./g, "_");
|
|
2828
|
-
const hasAssignment = handler.assignments.some((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
|
|
2829
|
-
if (hasAssignment) {
|
|
2830
|
-
const assignment = handler.assignments.find((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
|
|
2831
|
-
const assignedValue = assignment ? JSON.stringify(assignment.value) : "<value>";
|
|
2832
|
-
return [
|
|
2833
|
-
{
|
|
2834
|
-
kind: "weak_postcondition",
|
|
2835
|
-
message: `ensures() in ${messageType} uses !== for '${ref}' which has a concrete assignment`,
|
|
2836
|
-
messageType,
|
|
2837
|
-
conditionType,
|
|
2838
|
-
expression,
|
|
2839
|
-
location,
|
|
2840
|
-
suggestion: `Consider ensures(${ref} === ${assignedValue}) for a stronger postcondition`
|
|
2841
|
-
}
|
|
2842
|
-
];
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
return [];
|
|
2846
|
-
}
|
|
2847
|
-
function validateExpressions(handlers, stateConfig) {
|
|
2848
|
-
const warnings = [];
|
|
2849
|
-
let validCount = 0;
|
|
2850
|
-
const configKeys = new Set(Object.keys(stateConfig));
|
|
2851
|
-
for (const handler of handlers) {
|
|
2852
|
-
const conditions = [
|
|
2853
|
-
...handler.preconditions.map((c) => ({
|
|
2854
|
-
cond: c,
|
|
2855
|
-
type: "requires"
|
|
2856
|
-
})),
|
|
2857
|
-
...handler.postconditions.map((c) => ({
|
|
2858
|
-
cond: c,
|
|
2859
|
-
type: "ensures"
|
|
2860
|
-
}))
|
|
2861
|
-
];
|
|
2862
|
-
for (const { cond, type } of conditions) {
|
|
2863
|
-
const refs = extractFieldRefs(cond.expression);
|
|
2864
|
-
const isPayloadOnly = refs.length === 0 && PAYLOAD_REF.test(cond.expression);
|
|
2865
|
-
const loc = {
|
|
2866
|
-
file: handler.location.file,
|
|
2867
|
-
line: cond.location.line,
|
|
2868
|
-
column: cond.location.column
|
|
2869
|
-
};
|
|
2870
|
-
const condWarnings = [];
|
|
2871
|
-
if (!isPayloadOnly) {
|
|
2872
|
-
condWarnings.push(...checkUnmodeledFields(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
|
|
2873
|
-
}
|
|
2874
|
-
condWarnings.push(...checkUnsupportedMethods(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
|
|
2875
|
-
condWarnings.push(...checkOptionalChaining(cond.expression, handler.messageType, type, loc));
|
|
2876
|
-
condWarnings.push(...checkNullComparisons(cond.expression, stateConfig, configKeys, handler.messageType, type, loc));
|
|
2877
|
-
condWarnings.push(...checkWeakPostconditions(cond.expression, handler, handler.messageType, type, loc));
|
|
2878
|
-
if (condWarnings.length === 0) {
|
|
2879
|
-
validCount++;
|
|
2880
|
-
} else {
|
|
2881
|
-
warnings.push(...condWarnings);
|
|
2882
|
-
}
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
return { warnings, validCount, warnCount: warnings.length };
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
2968
|
// tools/verify/src/config/types.ts
|
|
2889
2969
|
function isAdapterConfig(config) {
|
|
2890
2970
|
return "adapter" in config;
|
|
@@ -7476,7 +7556,7 @@ async function runMonolithicVerification(config, analysis) {
|
|
|
7476
7556
|
async function runSubsystemVerification(config, analysis) {
|
|
7477
7557
|
const subsystems = config.subsystems;
|
|
7478
7558
|
const subsystemNames = Object.keys(subsystems);
|
|
7479
|
-
console.log(color(
|
|
7559
|
+
console.log(color(`Subsystem-scoped verification (${subsystemNames.length} subsystems)
|
|
7480
7560
|
`, COLORS.blue));
|
|
7481
7561
|
const { checkNonInterference: checkNonInterference2 } = await Promise.resolve().then(() => exports_non_interference);
|
|
7482
7562
|
const interference = checkNonInterference2(subsystems, analysis.handlers);
|
|
@@ -7493,6 +7573,24 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
7493
7573
|
console.log(color(" Compositional verification may not be sound. Consider restructuring subsystem boundaries.", COLORS.yellow));
|
|
7494
7574
|
console.log();
|
|
7495
7575
|
}
|
|
7576
|
+
const { checkPreconditionLocality: checkPreconditionLocality2 } = await Promise.resolve().then(() => (init_precondition_locality(), exports_precondition_locality));
|
|
7577
|
+
const locality = checkPreconditionLocality2(subsystems, analysis.handlers);
|
|
7578
|
+
if (locality.valid) {
|
|
7579
|
+
console.log(color("✓ Precondition locality: verified (no cross-subsystem requires() reads)", COLORS.green));
|
|
7580
|
+
console.log();
|
|
7581
|
+
} else {
|
|
7582
|
+
console.log(color(`⚠️ Precondition-locality violations detected:
|
|
7583
|
+
`, COLORS.yellow));
|
|
7584
|
+
for (const v of locality.violations) {
|
|
7585
|
+
console.log(color(` • Handler "${v.handler}" (${v.subsystem}) requires "${v.readsFrom}" owned by "${v.ownedBy}"`, COLORS.yellow));
|
|
7586
|
+
console.log(color(` ${v.expression}`, COLORS.gray));
|
|
7587
|
+
}
|
|
7588
|
+
console.log();
|
|
7589
|
+
console.log(color(" Compositional verification of these subsystems cannot satisfy the precondition;", COLORS.yellow));
|
|
7590
|
+
console.log(color(" the handler's ensures() postcondition will not be evaluated. Consider restructuring", COLORS.yellow));
|
|
7591
|
+
console.log(color(" subsystems to keep preconditions local.", COLORS.yellow));
|
|
7592
|
+
console.log();
|
|
7593
|
+
}
|
|
7496
7594
|
const assignedHandlers = new Set(Object.values(subsystems).flatMap((s) => s.handlers));
|
|
7497
7595
|
const unassigned = analysis.messageTypes.filter((mt) => !assignedHandlers.has(mt));
|
|
7498
7596
|
if (unassigned.length > 0) {
|
|
@@ -7819,4 +7917,4 @@ main().catch((error) => {
|
|
|
7819
7917
|
process.exit(1);
|
|
7820
7918
|
});
|
|
7821
7919
|
|
|
7822
|
-
//# debugId=
|
|
7920
|
+
//# debugId=E1FA1E90DBF1B2F664756E2164756E21
|