@fairfox/polly 0.1.3 → 0.1.5
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 +560 -184
- package/{cli/polly.ts → dist/cli/polly.js} +100 -206
- package/dist/cli/polly.js.map +10 -0
- package/dist/scripts/build-extension.js +137 -0
- package/dist/scripts/build-extension.js.map +10 -0
- package/dist/vendor/verify/src/cli.js +2089 -0
- package/dist/vendor/verify/src/cli.js.map +16 -0
- package/dist/vendor/visualize/src/cli.js +2204 -0
- package/dist/vendor/visualize/src/cli.js.map +19 -0
- package/package.json +12 -12
- package/vendor/analysis/src/extract/adr.ts +0 -212
- package/vendor/analysis/src/extract/architecture.ts +0 -160
- package/vendor/analysis/src/extract/contexts.ts +0 -298
- package/vendor/analysis/src/extract/flows.ts +0 -309
- package/vendor/analysis/src/extract/handlers.ts +0 -321
- package/vendor/analysis/src/extract/index.ts +0 -9
- package/vendor/analysis/src/extract/integrations.ts +0 -329
- package/vendor/analysis/src/extract/manifest.ts +0 -298
- package/vendor/analysis/src/extract/types.ts +0 -389
- package/vendor/analysis/src/index.ts +0 -7
- package/vendor/analysis/src/types/adr.ts +0 -53
- package/vendor/analysis/src/types/architecture.ts +0 -245
- package/vendor/analysis/src/types/core.ts +0 -210
- package/vendor/analysis/src/types/index.ts +0 -18
- package/vendor/verify/src/adapters/base.ts +0 -164
- package/vendor/verify/src/adapters/detection.ts +0 -281
- package/vendor/verify/src/adapters/event-bus/index.ts +0 -480
- package/vendor/verify/src/adapters/web-extension/index.ts +0 -508
- package/vendor/verify/src/adapters/websocket/index.ts +0 -486
- package/vendor/verify/src/cli.ts +0 -430
- package/vendor/verify/src/codegen/config.ts +0 -354
- package/vendor/verify/src/codegen/tla.ts +0 -719
- package/vendor/verify/src/config/parser.ts +0 -303
- package/vendor/verify/src/config/types.ts +0 -113
- package/vendor/verify/src/core/model.ts +0 -267
- package/vendor/verify/src/core/primitives.ts +0 -106
- package/vendor/verify/src/extract/handlers.ts +0 -2
- package/vendor/verify/src/extract/types.ts +0 -2
- package/vendor/verify/src/index.ts +0 -150
- package/vendor/verify/src/primitives/index.ts +0 -102
- package/vendor/verify/src/runner/docker.ts +0 -283
- package/vendor/verify/src/types.ts +0 -51
- package/vendor/visualize/src/cli.ts +0 -365
- package/vendor/visualize/src/codegen/structurizr.ts +0 -770
- package/vendor/visualize/src/index.ts +0 -13
- package/vendor/visualize/src/runner/export.ts +0 -235
- package/vendor/visualize/src/viewer/server.ts +0 -485
- /package/dist/{background → src/background}/index.js +0 -0
- /package/dist/{background → src/background}/index.js.map +0 -0
- /package/dist/{background → src/background}/message-router.js +0 -0
- /package/dist/{background → src/background}/message-router.js.map +0 -0
- /package/dist/{index.js → src/index.js} +0 -0
- /package/dist/{index.js.map → src/index.js.map} +0 -0
- /package/dist/{shared → src/shared}/adapters/index.js +0 -0
- /package/dist/{shared → src/shared}/adapters/index.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/context-helpers.js +0 -0
- /package/dist/{shared → src/shared}/lib/context-helpers.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/errors.js +0 -0
- /package/dist/{shared → src/shared}/lib/errors.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/message-bus.js +0 -0
- /package/dist/{shared → src/shared}/lib/message-bus.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/state.js +0 -0
- /package/dist/{shared → src/shared}/lib/state.js.map +0 -0
- /package/dist/{shared → src/shared}/lib/test-helpers.js +0 -0
- /package/dist/{shared → src/shared}/lib/test-helpers.js.map +0 -0
- /package/dist/{shared → src/shared}/state/app-state.js +0 -0
- /package/dist/{shared → src/shared}/state/app-state.js.map +0 -0
- /package/dist/{shared → src/shared}/types/messages.js +0 -0
- /package/dist/{shared → src/shared}/types/messages.js.map +0 -0
|
@@ -0,0 +1,2089 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
+
for (let key of __getOwnPropNames(mod))
|
|
12
|
+
if (!__hasOwnProp.call(to, key))
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: () => mod[key],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, {
|
|
22
|
+
get: all[name],
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
set: (newValue) => all[name] = () => newValue
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
29
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
30
|
+
|
|
31
|
+
// vendor/verify/src/codegen/tla.ts
|
|
32
|
+
var exports_tla = {};
|
|
33
|
+
__export(exports_tla, {
|
|
34
|
+
generateTLA: () => generateTLA,
|
|
35
|
+
TLAGenerator: () => TLAGenerator
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
class TLAGenerator {
|
|
39
|
+
lines = [];
|
|
40
|
+
indent = 0;
|
|
41
|
+
generate(config, analysis) {
|
|
42
|
+
this.lines = [];
|
|
43
|
+
this.indent = 0;
|
|
44
|
+
const spec = this.generateSpec(config, analysis);
|
|
45
|
+
const cfg = this.generateConfig(config, analysis);
|
|
46
|
+
return { spec, cfg };
|
|
47
|
+
}
|
|
48
|
+
generateSpec(config, analysis) {
|
|
49
|
+
this.lines = [];
|
|
50
|
+
this.indent = 0;
|
|
51
|
+
this.addHeader();
|
|
52
|
+
this.addExtends();
|
|
53
|
+
this.addConstants(config, analysis);
|
|
54
|
+
this.addMessageTypes(config, analysis);
|
|
55
|
+
this.addStateType(config, analysis);
|
|
56
|
+
this.addVariables();
|
|
57
|
+
this.addInit(config, analysis);
|
|
58
|
+
this.addActions(config, analysis);
|
|
59
|
+
this.addRouteWithHandlers(config, analysis);
|
|
60
|
+
this.addNext(config, analysis);
|
|
61
|
+
this.addSpec();
|
|
62
|
+
this.addInvariants(config, analysis);
|
|
63
|
+
return this.lines.join(`
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
generateConfig(config, analysis) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push("SPECIFICATION UserSpec");
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("\\* Constants");
|
|
71
|
+
lines.push("CONSTANTS");
|
|
72
|
+
lines.push(" Contexts = {background, content, popup}");
|
|
73
|
+
lines.push(` MaxMessages = ${config.messages.maxInFlight || 3}`);
|
|
74
|
+
lines.push(` MaxTabId = ${config.messages.maxTabs || 1}`);
|
|
75
|
+
lines.push(" TimeoutLimit = 3");
|
|
76
|
+
for (const [field, fieldConfig] of Object.entries(config.state)) {
|
|
77
|
+
if (typeof fieldConfig === "object" && fieldConfig !== null) {
|
|
78
|
+
if ("maxLength" in fieldConfig && fieldConfig.maxLength !== null) {
|
|
79
|
+
const constName = this.fieldToConstName(field);
|
|
80
|
+
lines.push(` ${constName}_MaxLength = ${fieldConfig.maxLength}`);
|
|
81
|
+
}
|
|
82
|
+
if ("max" in fieldConfig && fieldConfig.max !== null) {
|
|
83
|
+
const constName = this.fieldToConstName(field);
|
|
84
|
+
lines.push(` ${constName}_Max = ${fieldConfig.max}`);
|
|
85
|
+
}
|
|
86
|
+
if ("maxSize" in fieldConfig && fieldConfig.maxSize !== null) {
|
|
87
|
+
const constName = this.fieldToConstName(field);
|
|
88
|
+
lines.push(` ${constName}_MaxSize = ${fieldConfig.maxSize}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push("\\* Invariants to check");
|
|
94
|
+
lines.push("INVARIANTS");
|
|
95
|
+
lines.push(" TypeOK");
|
|
96
|
+
lines.push(" NoRoutingLoops");
|
|
97
|
+
lines.push(" UserStateTypeInvariant");
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push("\\* State constraint");
|
|
100
|
+
lines.push("CONSTRAINT");
|
|
101
|
+
lines.push(" StateConstraint");
|
|
102
|
+
return lines.join(`
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
addHeader() {
|
|
106
|
+
this.line("------------------------- MODULE UserApp -------------------------");
|
|
107
|
+
this.line("(*");
|
|
108
|
+
this.line(" Auto-generated TLA+ specification for web extension");
|
|
109
|
+
this.line(" ");
|
|
110
|
+
this.line(" Generated from:");
|
|
111
|
+
this.line(" - TypeScript type definitions");
|
|
112
|
+
this.line(" - Verification configuration");
|
|
113
|
+
this.line(" ");
|
|
114
|
+
this.line(" This spec extends MessageRouter with:");
|
|
115
|
+
this.line(" - Application-specific state types");
|
|
116
|
+
this.line(" - Message type definitions");
|
|
117
|
+
this.line(" - State transition actions");
|
|
118
|
+
this.line("*)");
|
|
119
|
+
this.line("");
|
|
120
|
+
}
|
|
121
|
+
addExtends() {
|
|
122
|
+
this.line("EXTENDS MessageRouter");
|
|
123
|
+
this.line("");
|
|
124
|
+
}
|
|
125
|
+
addConstants(config, analysis) {
|
|
126
|
+
const hasCustomConstants = Object.entries(config.state).some(([field, fieldConfig]) => {
|
|
127
|
+
if (typeof fieldConfig !== "object" || fieldConfig === null)
|
|
128
|
+
return false;
|
|
129
|
+
return "maxLength" in fieldConfig && fieldConfig.maxLength !== null || "max" in fieldConfig && fieldConfig.max !== null || "maxSize" in fieldConfig && fieldConfig.maxSize !== null;
|
|
130
|
+
});
|
|
131
|
+
if (!hasCustomConstants) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.line("\\* Application-specific constants");
|
|
135
|
+
this.line("CONSTANTS");
|
|
136
|
+
this.indent++;
|
|
137
|
+
let first = true;
|
|
138
|
+
for (const [field, fieldConfig] of Object.entries(config.state)) {
|
|
139
|
+
if (typeof fieldConfig === "object" && fieldConfig !== null) {
|
|
140
|
+
const constName = this.fieldToConstName(field);
|
|
141
|
+
if ("maxLength" in fieldConfig && fieldConfig.maxLength !== null) {
|
|
142
|
+
this.line(`${first ? "" : ","}${constName}_MaxLength \\* Max length for ${field}`);
|
|
143
|
+
first = false;
|
|
144
|
+
}
|
|
145
|
+
if ("max" in fieldConfig && fieldConfig.max !== null) {
|
|
146
|
+
this.line(`${first ? "" : ","}${constName}_Max \\* Max value for ${field}`);
|
|
147
|
+
first = false;
|
|
148
|
+
}
|
|
149
|
+
if ("maxSize" in fieldConfig && fieldConfig.maxSize !== null) {
|
|
150
|
+
this.line(`${first ? "" : ","}${constName}_MaxSize \\* Max size for ${field}`);
|
|
151
|
+
first = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
this.indent--;
|
|
156
|
+
this.line("");
|
|
157
|
+
}
|
|
158
|
+
addStateType(config, analysis) {
|
|
159
|
+
this.line("\\* Generic value type for sequences and maps");
|
|
160
|
+
this.line("\\* Bounded to allow model checking");
|
|
161
|
+
this.line('Value == {"v1", "v2", "v3"}');
|
|
162
|
+
this.line("");
|
|
163
|
+
this.line("\\* Generic key type for maps");
|
|
164
|
+
this.line("\\* Bounded to allow model checking");
|
|
165
|
+
this.line('Keys == {"k1", "k2", "k3"}');
|
|
166
|
+
this.line("");
|
|
167
|
+
this.line("\\* Application state type definition");
|
|
168
|
+
this.line("State == [");
|
|
169
|
+
this.indent++;
|
|
170
|
+
const stateFields = [];
|
|
171
|
+
for (const [fieldPath, fieldConfig] of Object.entries(config.state)) {
|
|
172
|
+
if (typeof fieldConfig !== "object" || fieldConfig === null)
|
|
173
|
+
continue;
|
|
174
|
+
const fieldName = this.sanitizeFieldName(fieldPath);
|
|
175
|
+
const tlaType = this.fieldConfigToTLAType(fieldPath, fieldConfig, config);
|
|
176
|
+
stateFields.push(`${fieldName}: ${tlaType}`);
|
|
177
|
+
}
|
|
178
|
+
for (let i = 0;i < stateFields.length; i++) {
|
|
179
|
+
const field = stateFields[i];
|
|
180
|
+
const suffix = i < stateFields.length - 1 ? "," : "";
|
|
181
|
+
this.line(`${field}${suffix}`);
|
|
182
|
+
}
|
|
183
|
+
this.indent--;
|
|
184
|
+
this.line("]");
|
|
185
|
+
this.line("");
|
|
186
|
+
}
|
|
187
|
+
addMessageTypes(config, analysis) {
|
|
188
|
+
if (analysis.messageTypes.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.line("\\* Message types from application");
|
|
192
|
+
const messageTypeSet = analysis.messageTypes.map((t) => `"${t}"`).join(", ");
|
|
193
|
+
this.line(`UserMessageTypes == {${messageTypeSet}}`);
|
|
194
|
+
this.line("");
|
|
195
|
+
}
|
|
196
|
+
addVariables() {
|
|
197
|
+
this.line("\\* Application state per context");
|
|
198
|
+
this.line("VARIABLE contextStates");
|
|
199
|
+
this.line("");
|
|
200
|
+
this.line("\\* All variables (extending MessageRouter vars)");
|
|
201
|
+
this.line("allVars == <<ports, messages, pendingRequests, delivered, routingDepth, time, contextStates>>");
|
|
202
|
+
this.line("");
|
|
203
|
+
}
|
|
204
|
+
addInit(config, analysis) {
|
|
205
|
+
this.line("\\* Initial application state");
|
|
206
|
+
this.line("InitialState == [");
|
|
207
|
+
this.indent++;
|
|
208
|
+
const fields = [];
|
|
209
|
+
for (const [fieldPath, fieldConfig] of Object.entries(config.state)) {
|
|
210
|
+
if (typeof fieldConfig !== "object" || fieldConfig === null)
|
|
211
|
+
continue;
|
|
212
|
+
const fieldName = this.sanitizeFieldName(fieldPath);
|
|
213
|
+
const initialValue = this.getInitialValue(fieldConfig);
|
|
214
|
+
fields.push(`${fieldName} |-> ${initialValue}`);
|
|
215
|
+
}
|
|
216
|
+
for (let i = 0;i < fields.length; i++) {
|
|
217
|
+
const field = fields[i];
|
|
218
|
+
const suffix = i < fields.length - 1 ? "," : "";
|
|
219
|
+
this.line(`${field}${suffix}`);
|
|
220
|
+
}
|
|
221
|
+
this.indent--;
|
|
222
|
+
this.line("]");
|
|
223
|
+
this.line("");
|
|
224
|
+
this.line("\\* Initial state (extends MessageRouter)");
|
|
225
|
+
this.line("UserInit ==");
|
|
226
|
+
this.indent++;
|
|
227
|
+
this.line("/\\ Init \\* MessageRouter's init");
|
|
228
|
+
this.line("/\\ contextStates = [c \\in Contexts |-> InitialState]");
|
|
229
|
+
this.indent--;
|
|
230
|
+
this.line("");
|
|
231
|
+
}
|
|
232
|
+
addActions(config, analysis) {
|
|
233
|
+
this.line("\\* =============================================================================");
|
|
234
|
+
this.line("\\* Application-specific actions");
|
|
235
|
+
this.line("\\* =============================================================================");
|
|
236
|
+
this.line("");
|
|
237
|
+
if (analysis.handlers.length === 0) {
|
|
238
|
+
this.line("\\* No message handlers found in codebase");
|
|
239
|
+
this.line("\\* State remains unchanged for all messages");
|
|
240
|
+
this.line("StateTransition(ctx, msgType) ==");
|
|
241
|
+
this.indent++;
|
|
242
|
+
this.line("UNCHANGED contextStates");
|
|
243
|
+
this.indent--;
|
|
244
|
+
this.line("");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.line("\\* State transitions extracted from message handlers");
|
|
248
|
+
this.line("");
|
|
249
|
+
const handlersByType = new Map;
|
|
250
|
+
for (const handler of analysis.handlers) {
|
|
251
|
+
if (!handlersByType.has(handler.messageType)) {
|
|
252
|
+
handlersByType.set(handler.messageType, []);
|
|
253
|
+
}
|
|
254
|
+
handlersByType.get(handler.messageType).push(handler);
|
|
255
|
+
}
|
|
256
|
+
for (const [messageType, handlers] of handlersByType.entries()) {
|
|
257
|
+
this.generateHandlerAction(messageType, handlers, config);
|
|
258
|
+
}
|
|
259
|
+
this.line("\\* Main state transition action");
|
|
260
|
+
this.line("StateTransition(ctx, msgType) ==");
|
|
261
|
+
this.indent++;
|
|
262
|
+
const messageTypes = Array.from(handlersByType.keys());
|
|
263
|
+
for (let i = 0;i < messageTypes.length; i++) {
|
|
264
|
+
const msgType = messageTypes[i];
|
|
265
|
+
const actionName = this.messageTypeToActionName(msgType);
|
|
266
|
+
if (i === 0) {
|
|
267
|
+
this.line(`IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
|
|
268
|
+
} else if (i === messageTypes.length - 1) {
|
|
269
|
+
this.line(`ELSE IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
|
|
270
|
+
this.line("ELSE UNCHANGED contextStates \\* Unknown message type");
|
|
271
|
+
} else {
|
|
272
|
+
this.line(`ELSE IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
this.indent--;
|
|
276
|
+
this.line("");
|
|
277
|
+
}
|
|
278
|
+
generateHandlerAction(messageType, handlers, config) {
|
|
279
|
+
const actionName = this.messageTypeToActionName(messageType);
|
|
280
|
+
this.line(`\\* Handler for ${messageType}`);
|
|
281
|
+
this.line(`${actionName}(ctx) ==`);
|
|
282
|
+
this.indent++;
|
|
283
|
+
const allPreconditions = handlers.flatMap((h) => h.preconditions);
|
|
284
|
+
const allAssignments = handlers.flatMap((h) => h.assignments);
|
|
285
|
+
const allPostconditions = handlers.flatMap((h) => h.postconditions);
|
|
286
|
+
if (allPreconditions.length > 0) {
|
|
287
|
+
for (const precondition of allPreconditions) {
|
|
288
|
+
const tlaExpr = this.tsExpressionToTLA(precondition.expression);
|
|
289
|
+
const comment = precondition.message ? ` \\* ${precondition.message}` : "";
|
|
290
|
+
this.line(`/\\ ${tlaExpr}${comment}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const validAssignments = allAssignments.filter((a) => {
|
|
294
|
+
if (a.value === null) {
|
|
295
|
+
const fieldConfig = config.state[a.field];
|
|
296
|
+
if (fieldConfig && typeof fieldConfig === "object" && "values" in fieldConfig && fieldConfig.values) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
}).map((a) => {
|
|
303
|
+
if (a.value === null) {
|
|
304
|
+
const fieldConfig = config.state[a.field];
|
|
305
|
+
if (fieldConfig && typeof fieldConfig === "object" && "values" in fieldConfig && fieldConfig.values) {
|
|
306
|
+
const nullValue = fieldConfig.values[fieldConfig.values.length - 1];
|
|
307
|
+
return { ...a, value: nullValue };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return a;
|
|
311
|
+
});
|
|
312
|
+
if (validAssignments.length === 0) {
|
|
313
|
+
if (allPreconditions.length === 0) {
|
|
314
|
+
this.line("\\* No state changes in handler");
|
|
315
|
+
}
|
|
316
|
+
this.line("/\\ UNCHANGED contextStates");
|
|
317
|
+
} else {
|
|
318
|
+
this.line("/\\ contextStates' = [contextStates EXCEPT");
|
|
319
|
+
this.indent++;
|
|
320
|
+
for (let i = 0;i < validAssignments.length; i++) {
|
|
321
|
+
const assignment = validAssignments[i];
|
|
322
|
+
const fieldName = this.sanitizeFieldName(assignment.field);
|
|
323
|
+
const value = this.assignmentValueToTLA(assignment.value);
|
|
324
|
+
const suffix = i < validAssignments.length - 1 ? "," : "";
|
|
325
|
+
this.line(`![ctx].${fieldName} = ${value}${suffix}`);
|
|
326
|
+
}
|
|
327
|
+
this.indent--;
|
|
328
|
+
this.line("]");
|
|
329
|
+
}
|
|
330
|
+
if (allPostconditions.length > 0) {
|
|
331
|
+
for (const postcondition of allPostconditions) {
|
|
332
|
+
const tlaExpr = this.tsExpressionToTLA(postcondition.expression, true);
|
|
333
|
+
const comment = postcondition.message ? ` \\* ${postcondition.message}` : "";
|
|
334
|
+
this.line(`/\\ ${tlaExpr}${comment}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
this.indent--;
|
|
338
|
+
this.line("");
|
|
339
|
+
}
|
|
340
|
+
tsExpressionToTLA(expr, isPrimed = false) {
|
|
341
|
+
let tla = expr;
|
|
342
|
+
const statePrefix = isPrimed ? "contextStates'[ctx]" : "contextStates[ctx]";
|
|
343
|
+
tla = tla.replace(/state\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (match, path2) => {
|
|
344
|
+
return `${statePrefix}.${this.sanitizeFieldName(path2)}`;
|
|
345
|
+
});
|
|
346
|
+
tla = tla.replace(/payload\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (match, path2) => {
|
|
347
|
+
return `payload.${this.sanitizeFieldName(path2)}`;
|
|
348
|
+
});
|
|
349
|
+
tla = tla.replace(/===/g, "=");
|
|
350
|
+
tla = tla.replace(/!==/g, "#");
|
|
351
|
+
tla = tla.replace(/!=/g, "#");
|
|
352
|
+
tla = tla.replace(/==/g, "=");
|
|
353
|
+
tla = tla.replace(/&&/g, "/\\");
|
|
354
|
+
tla = tla.replace(/\|\|/g, "\\/");
|
|
355
|
+
tla = tla.replace(/!([^=])/g, "~$1");
|
|
356
|
+
tla = tla.replace(/!$/g, "~");
|
|
357
|
+
tla = tla.replace(/\btrue\b/g, "TRUE");
|
|
358
|
+
tla = tla.replace(/\bfalse\b/g, "FALSE");
|
|
359
|
+
tla = tla.replace(/\bnull\b/g, "NULL");
|
|
360
|
+
tla = tla.replace(/</g, "<");
|
|
361
|
+
tla = tla.replace(/>/g, ">");
|
|
362
|
+
tla = tla.replace(/<=/g, "<=");
|
|
363
|
+
tla = tla.replace(/>=/g, ">=");
|
|
364
|
+
return tla;
|
|
365
|
+
}
|
|
366
|
+
messageTypeToActionName(messageType) {
|
|
367
|
+
return "Handle" + messageType.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
|
|
368
|
+
}
|
|
369
|
+
assignmentValueToTLA(value) {
|
|
370
|
+
if (typeof value === "boolean") {
|
|
371
|
+
return value ? "TRUE" : "FALSE";
|
|
372
|
+
}
|
|
373
|
+
if (typeof value === "number") {
|
|
374
|
+
return String(value);
|
|
375
|
+
}
|
|
376
|
+
if (value === null) {
|
|
377
|
+
return "NULL";
|
|
378
|
+
}
|
|
379
|
+
if (typeof value === "string") {
|
|
380
|
+
return `"${value}"`;
|
|
381
|
+
}
|
|
382
|
+
return "NULL";
|
|
383
|
+
}
|
|
384
|
+
addRouteWithHandlers(config, analysis) {
|
|
385
|
+
this.line("\\* =============================================================================");
|
|
386
|
+
this.line("\\* Message Routing with State Transitions");
|
|
387
|
+
this.line("\\* =============================================================================");
|
|
388
|
+
this.line("");
|
|
389
|
+
if (analysis.handlers.length === 0) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
this.line("\\* Route a message and invoke its handler");
|
|
393
|
+
this.line("UserRouteMessage(msgIndex) ==");
|
|
394
|
+
this.indent++;
|
|
395
|
+
this.line("/\\ msgIndex \\in 1..Len(messages)");
|
|
396
|
+
this.line("/\\ LET msg == messages[msgIndex]");
|
|
397
|
+
this.line(' IN /\\ msg.status = "pending"');
|
|
398
|
+
this.line(" /\\ routingDepth' = routingDepth + 1");
|
|
399
|
+
this.line(" /\\ routingDepth < 5");
|
|
400
|
+
this.line(" /\\ \\E target \\in msg.targets :");
|
|
401
|
+
this.line(' /\\ IF target \\in Contexts /\\ ports[target] = "connected"');
|
|
402
|
+
this.line(" THEN \\* Successful delivery - route AND invoke handler");
|
|
403
|
+
this.line(` /\\ messages' = [messages EXCEPT ![msgIndex].status = "delivered"]`);
|
|
404
|
+
this.line(" /\\ delivered' = delivered \\union {msg.id}");
|
|
405
|
+
this.line(" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->");
|
|
406
|
+
this.line(" pendingRequests[id]]");
|
|
407
|
+
this.line(" /\\ time' = time + 1");
|
|
408
|
+
this.line(" /\\ StateTransition(target, msg.msgType)");
|
|
409
|
+
this.line(" ELSE \\* Port not connected - message fails");
|
|
410
|
+
this.line(` /\\ messages' = [messages EXCEPT ![msgIndex].status = "failed"]`);
|
|
411
|
+
this.line(" /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->");
|
|
412
|
+
this.line(" pendingRequests[id]]");
|
|
413
|
+
this.line(" /\\ time' = time + 1");
|
|
414
|
+
this.line(" /\\ UNCHANGED <<delivered, contextStates>>");
|
|
415
|
+
this.line(" /\\ UNCHANGED ports");
|
|
416
|
+
this.indent--;
|
|
417
|
+
this.line("");
|
|
418
|
+
}
|
|
419
|
+
addNext(config, analysis) {
|
|
420
|
+
this.line("\\* Next state relation (extends MessageRouter)");
|
|
421
|
+
this.line("UserNext ==");
|
|
422
|
+
this.indent++;
|
|
423
|
+
if (analysis.handlers.length > 0) {
|
|
424
|
+
this.line("\\/ \\E c \\in Contexts : ConnectPort(c) /\\ UNCHANGED contextStates");
|
|
425
|
+
this.line("\\/ \\E c \\in Contexts : DisconnectPort(c) /\\ UNCHANGED contextStates");
|
|
426
|
+
this.line("\\/ \\E src \\in Contexts : \\E targetSet \\in (SUBSET Contexts \\ {{}}) : \\E tab \\in 0..MaxTabId : \\E msgType \\in UserMessageTypes :");
|
|
427
|
+
this.indent++;
|
|
428
|
+
this.line("SendMessage(src, targetSet, tab, msgType) /\\ UNCHANGED contextStates");
|
|
429
|
+
this.indent--;
|
|
430
|
+
this.line("\\/ \\E i \\in 1..Len(messages) : UserRouteMessage(i)");
|
|
431
|
+
this.line("\\/ CompleteRouting /\\ UNCHANGED contextStates");
|
|
432
|
+
this.line("\\/ \\E i \\in 1..Len(messages) : TimeoutMessage(i) /\\ UNCHANGED contextStates");
|
|
433
|
+
} else {
|
|
434
|
+
this.line("\\/ Next /\\ UNCHANGED contextStates");
|
|
435
|
+
}
|
|
436
|
+
this.indent--;
|
|
437
|
+
this.line("");
|
|
438
|
+
}
|
|
439
|
+
addSpec() {
|
|
440
|
+
this.line("\\* Specification");
|
|
441
|
+
this.line("UserSpec == UserInit /\\ [][UserNext]_allVars /\\ WF_allVars(UserNext)");
|
|
442
|
+
this.line("");
|
|
443
|
+
}
|
|
444
|
+
addInvariants(config, analysis) {
|
|
445
|
+
this.line("\\* =============================================================================");
|
|
446
|
+
this.line("\\* Application Invariants");
|
|
447
|
+
this.line("\\* =============================================================================");
|
|
448
|
+
this.line("");
|
|
449
|
+
this.line("\\* TypeOK and NoRoutingLoops are inherited from MessageRouter");
|
|
450
|
+
this.line("");
|
|
451
|
+
this.line("\\* Application state type invariant");
|
|
452
|
+
this.line("UserStateTypeInvariant ==");
|
|
453
|
+
this.indent++;
|
|
454
|
+
this.line("\\A ctx \\in Contexts :");
|
|
455
|
+
this.indent++;
|
|
456
|
+
this.line("contextStates[ctx] \\in State");
|
|
457
|
+
this.indent--;
|
|
458
|
+
this.indent--;
|
|
459
|
+
this.line("");
|
|
460
|
+
this.line("\\* State constraint to bound state space");
|
|
461
|
+
this.line("StateConstraint ==");
|
|
462
|
+
this.indent++;
|
|
463
|
+
this.line("Len(messages) <= MaxMessages");
|
|
464
|
+
this.indent--;
|
|
465
|
+
this.line("");
|
|
466
|
+
this.line("=============================================================================");
|
|
467
|
+
}
|
|
468
|
+
fieldConfigToTLAType(fieldPath, fieldConfig, config) {
|
|
469
|
+
if ("type" in fieldConfig) {
|
|
470
|
+
if (fieldConfig.type === "boolean") {
|
|
471
|
+
return "BOOLEAN";
|
|
472
|
+
}
|
|
473
|
+
if (fieldConfig.type === "enum" && fieldConfig.values) {
|
|
474
|
+
const values = fieldConfig.values.map((v) => `"${v}"`).join(", ");
|
|
475
|
+
return `{${values}}`;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if ("maxLength" in fieldConfig) {
|
|
479
|
+
const constName = this.fieldToConstName(fieldPath);
|
|
480
|
+
return `Seq(Value)`;
|
|
481
|
+
}
|
|
482
|
+
if ("min" in fieldConfig && "max" in fieldConfig) {
|
|
483
|
+
const constName = this.fieldToConstName(fieldPath);
|
|
484
|
+
const min = fieldConfig.min || 0;
|
|
485
|
+
const max = fieldConfig.max || 100;
|
|
486
|
+
return `${min}..${max}`;
|
|
487
|
+
}
|
|
488
|
+
if ("values" in fieldConfig) {
|
|
489
|
+
if (fieldConfig.values && Array.isArray(fieldConfig.values)) {
|
|
490
|
+
const values = fieldConfig.values.map((v) => `"${v}"`).join(", ");
|
|
491
|
+
return `{${values}}`;
|
|
492
|
+
}
|
|
493
|
+
if (fieldConfig.abstract) {
|
|
494
|
+
return "STRING";
|
|
495
|
+
}
|
|
496
|
+
return "STRING";
|
|
497
|
+
}
|
|
498
|
+
if ("maxSize" in fieldConfig) {
|
|
499
|
+
return "[Keys -> Value]";
|
|
500
|
+
}
|
|
501
|
+
return "Value";
|
|
502
|
+
}
|
|
503
|
+
getInitialValue(fieldConfig) {
|
|
504
|
+
if ("type" in fieldConfig) {
|
|
505
|
+
if (fieldConfig.type === "boolean") {
|
|
506
|
+
return "FALSE";
|
|
507
|
+
}
|
|
508
|
+
if (fieldConfig.type === "enum" && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
509
|
+
return `"${fieldConfig.values[0]}"`;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if ("maxLength" in fieldConfig) {
|
|
513
|
+
return "<<>>";
|
|
514
|
+
}
|
|
515
|
+
if ("min" in fieldConfig) {
|
|
516
|
+
return String(fieldConfig.min || 0);
|
|
517
|
+
}
|
|
518
|
+
if ("values" in fieldConfig && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
519
|
+
return `"${fieldConfig.values[0]}"`;
|
|
520
|
+
}
|
|
521
|
+
if ("maxSize" in fieldConfig) {
|
|
522
|
+
return '[k \\in Keys |-> "v1"]';
|
|
523
|
+
}
|
|
524
|
+
return "0";
|
|
525
|
+
}
|
|
526
|
+
fieldToConstName(fieldPath) {
|
|
527
|
+
return fieldPath.replace(/\./g, "_").toUpperCase();
|
|
528
|
+
}
|
|
529
|
+
sanitizeFieldName(fieldPath) {
|
|
530
|
+
return fieldPath.replace(/\./g, "_");
|
|
531
|
+
}
|
|
532
|
+
line(content) {
|
|
533
|
+
if (content === "") {
|
|
534
|
+
this.lines.push("");
|
|
535
|
+
} else {
|
|
536
|
+
const indentation = " ".repeat(this.indent);
|
|
537
|
+
this.lines.push(indentation + content);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function generateTLA(config, analysis) {
|
|
542
|
+
const generator = new TLAGenerator;
|
|
543
|
+
return generator.generate(config, analysis);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// vendor/verify/src/runner/docker.ts
|
|
547
|
+
var exports_docker = {};
|
|
548
|
+
__export(exports_docker, {
|
|
549
|
+
DockerRunner: () => DockerRunner
|
|
550
|
+
});
|
|
551
|
+
import { spawn } from "node:child_process";
|
|
552
|
+
import * as fs2 from "node:fs";
|
|
553
|
+
import * as path2 from "node:path";
|
|
554
|
+
|
|
555
|
+
class DockerRunner {
|
|
556
|
+
containerName = "web-ext-tla-verify";
|
|
557
|
+
async isDockerAvailable() {
|
|
558
|
+
try {
|
|
559
|
+
const result = await this.runCommand("docker", ["--version"]);
|
|
560
|
+
return result.exitCode === 0;
|
|
561
|
+
} catch {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async hasImage() {
|
|
566
|
+
try {
|
|
567
|
+
const result = await this.runCommand("docker", ["images", "-q", "talex5/tla"]);
|
|
568
|
+
return result.stdout.trim().length > 0;
|
|
569
|
+
} catch {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async pullImage(onProgress) {
|
|
574
|
+
await this.runCommandStreaming("docker", ["pull", "talex5/tla:latest"], onProgress);
|
|
575
|
+
}
|
|
576
|
+
async runTLC(specPath, options) {
|
|
577
|
+
if (!fs2.existsSync(specPath)) {
|
|
578
|
+
throw new Error(`Spec file not found: ${specPath}`);
|
|
579
|
+
}
|
|
580
|
+
const specDir = path2.dirname(specPath);
|
|
581
|
+
const specName = path2.basename(specPath, ".tla");
|
|
582
|
+
const cfgPath = path2.join(specDir, `${specName}.cfg`);
|
|
583
|
+
if (!fs2.existsSync(cfgPath)) {
|
|
584
|
+
throw new Error(`Config file not found: ${cfgPath}`);
|
|
585
|
+
}
|
|
586
|
+
const args = [
|
|
587
|
+
"run",
|
|
588
|
+
"--rm",
|
|
589
|
+
"-v",
|
|
590
|
+
`${specDir}:/specs`,
|
|
591
|
+
"talex5/tla",
|
|
592
|
+
"sh",
|
|
593
|
+
"-c",
|
|
594
|
+
`cd /specs && tlc -workers ${options?.workers || 1} ${specName}.tla`
|
|
595
|
+
];
|
|
596
|
+
const result = await this.runCommand("docker", args, {
|
|
597
|
+
timeout: options?.timeout || 60000
|
|
598
|
+
});
|
|
599
|
+
return this.parseTLCOutput(result);
|
|
600
|
+
}
|
|
601
|
+
parseTLCOutput(result) {
|
|
602
|
+
const output = result.stdout + result.stderr;
|
|
603
|
+
const violationMatch = output.match(/Error: Invariant (.*?) is violated/);
|
|
604
|
+
if (violationMatch) {
|
|
605
|
+
return {
|
|
606
|
+
success: false,
|
|
607
|
+
violation: {
|
|
608
|
+
type: "invariant",
|
|
609
|
+
name: violationMatch[1],
|
|
610
|
+
trace: this.extractTrace(output)
|
|
611
|
+
},
|
|
612
|
+
output
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
if (result.exitCode !== 0 || output.includes("Error:")) {
|
|
616
|
+
return {
|
|
617
|
+
success: false,
|
|
618
|
+
error: this.extractError(output),
|
|
619
|
+
output
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const statesMatch = output.match(/(\d+) states generated/);
|
|
623
|
+
const distinctMatch = output.match(/(\d+) distinct states/);
|
|
624
|
+
return {
|
|
625
|
+
success: true,
|
|
626
|
+
stats: {
|
|
627
|
+
statesGenerated: statesMatch ? Number.parseInt(statesMatch[1]) : 0,
|
|
628
|
+
distinctStates: distinctMatch ? Number.parseInt(distinctMatch[1]) : 0
|
|
629
|
+
},
|
|
630
|
+
output
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
extractTrace(output) {
|
|
634
|
+
const lines = output.split(`
|
|
635
|
+
`);
|
|
636
|
+
const trace = [];
|
|
637
|
+
let inTrace = false;
|
|
638
|
+
for (const line of lines) {
|
|
639
|
+
if (line.includes("State ") && line.includes(":")) {
|
|
640
|
+
inTrace = true;
|
|
641
|
+
trace.push(line);
|
|
642
|
+
} else if (inTrace) {
|
|
643
|
+
if (line.trim() === "" || line.startsWith("Error:")) {
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
trace.push(line);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return trace;
|
|
650
|
+
}
|
|
651
|
+
extractError(output) {
|
|
652
|
+
const errorMatch = output.match(/Error: (.*?)(?:\n|$)/);
|
|
653
|
+
if (errorMatch) {
|
|
654
|
+
return errorMatch[1];
|
|
655
|
+
}
|
|
656
|
+
if (output.includes("Parse Error")) {
|
|
657
|
+
return "TLA+ syntax error in specification";
|
|
658
|
+
}
|
|
659
|
+
if (output.includes("Semantic Error")) {
|
|
660
|
+
return "Semantic error in specification";
|
|
661
|
+
}
|
|
662
|
+
return "Unknown error occurred during model checking";
|
|
663
|
+
}
|
|
664
|
+
runCommand(command, args, options) {
|
|
665
|
+
return new Promise((resolve2, reject) => {
|
|
666
|
+
const proc = spawn(command, args);
|
|
667
|
+
let stdout = "";
|
|
668
|
+
let stderr = "";
|
|
669
|
+
proc.stdout.on("data", (data) => {
|
|
670
|
+
stdout += data.toString();
|
|
671
|
+
});
|
|
672
|
+
proc.stderr.on("data", (data) => {
|
|
673
|
+
stderr += data.toString();
|
|
674
|
+
});
|
|
675
|
+
const timeout = options?.timeout ? setTimeout(() => {
|
|
676
|
+
proc.kill();
|
|
677
|
+
reject(new Error(`Command timed out after ${options.timeout}ms`));
|
|
678
|
+
}, options.timeout) : null;
|
|
679
|
+
proc.on("close", (exitCode) => {
|
|
680
|
+
if (timeout)
|
|
681
|
+
clearTimeout(timeout);
|
|
682
|
+
resolve2({
|
|
683
|
+
exitCode: exitCode || 0,
|
|
684
|
+
stdout,
|
|
685
|
+
stderr
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
proc.on("error", (error) => {
|
|
689
|
+
if (timeout)
|
|
690
|
+
clearTimeout(timeout);
|
|
691
|
+
reject(error);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
runCommandStreaming(command, args, onOutput) {
|
|
696
|
+
return new Promise((resolve2, reject) => {
|
|
697
|
+
const proc = spawn(command, args);
|
|
698
|
+
proc.stdout.on("data", (data) => {
|
|
699
|
+
if (onOutput) {
|
|
700
|
+
const lines = data.toString().split(`
|
|
701
|
+
`);
|
|
702
|
+
for (const line of lines) {
|
|
703
|
+
if (line.trim()) {
|
|
704
|
+
onOutput(line.trim());
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
proc.stderr.on("data", (data) => {
|
|
710
|
+
if (onOutput) {
|
|
711
|
+
const lines = data.toString().split(`
|
|
712
|
+
`);
|
|
713
|
+
for (const line of lines) {
|
|
714
|
+
if (line.trim()) {
|
|
715
|
+
onOutput(line.trim());
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
proc.on("close", (exitCode) => {
|
|
721
|
+
if (exitCode === 0) {
|
|
722
|
+
resolve2();
|
|
723
|
+
} else {
|
|
724
|
+
reject(new Error(`Command failed with exit code ${exitCode}`));
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
proc.on("error", reject);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
var init_docker = () => {};
|
|
732
|
+
|
|
733
|
+
// vendor/verify/src/cli.ts
|
|
734
|
+
import * as fs3 from "node:fs";
|
|
735
|
+
import * as path3 from "node:path";
|
|
736
|
+
|
|
737
|
+
// vendor/analysis/src/extract/types.ts
|
|
738
|
+
import { Project as Project2 } from "ts-morph";
|
|
739
|
+
|
|
740
|
+
// vendor/analysis/src/extract/handlers.ts
|
|
741
|
+
import { Project, SyntaxKind, Node } from "ts-morph";
|
|
742
|
+
|
|
743
|
+
class HandlerExtractor {
|
|
744
|
+
project;
|
|
745
|
+
constructor(tsConfigPath) {
|
|
746
|
+
this.project = new Project({
|
|
747
|
+
tsConfigFilePath: tsConfigPath
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
extractHandlers() {
|
|
751
|
+
const handlers = [];
|
|
752
|
+
const messageTypes = new Set;
|
|
753
|
+
const sourceFiles = this.project.getSourceFiles();
|
|
754
|
+
for (const sourceFile of sourceFiles) {
|
|
755
|
+
const fileHandlers = this.extractFromFile(sourceFile);
|
|
756
|
+
handlers.push(...fileHandlers);
|
|
757
|
+
for (const handler of fileHandlers) {
|
|
758
|
+
messageTypes.add(handler.messageType);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
handlers,
|
|
763
|
+
messageTypes
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
extractFromFile(sourceFile) {
|
|
767
|
+
const handlers = [];
|
|
768
|
+
const filePath = sourceFile.getFilePath();
|
|
769
|
+
const context = this.inferContext(filePath);
|
|
770
|
+
sourceFile.forEachDescendant((node) => {
|
|
771
|
+
if (Node.isCallExpression(node)) {
|
|
772
|
+
const expression = node.getExpression();
|
|
773
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
774
|
+
const methodName = expression.getName();
|
|
775
|
+
if (methodName === "on") {
|
|
776
|
+
const handler = this.extractHandler(node, context, filePath);
|
|
777
|
+
if (handler) {
|
|
778
|
+
handlers.push(handler);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
return handlers;
|
|
785
|
+
}
|
|
786
|
+
extractHandler(callExpr, context, filePath) {
|
|
787
|
+
const args = callExpr.getArguments();
|
|
788
|
+
if (args.length < 2) {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
const messageTypeArg = args[0];
|
|
792
|
+
let messageType = null;
|
|
793
|
+
if (Node.isStringLiteral(messageTypeArg)) {
|
|
794
|
+
messageType = messageTypeArg.getLiteralValue();
|
|
795
|
+
} else if (Node.isTemplateExpression(messageTypeArg)) {
|
|
796
|
+
messageType = messageTypeArg.getText().replace(/[`'"]/g, "");
|
|
797
|
+
}
|
|
798
|
+
if (!messageType) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
const handlerArg = args[1];
|
|
802
|
+
const assignments = [];
|
|
803
|
+
const preconditions = [];
|
|
804
|
+
const postconditions = [];
|
|
805
|
+
if (Node.isArrowFunction(handlerArg) || Node.isFunctionExpression(handlerArg)) {
|
|
806
|
+
this.extractAssignments(handlerArg, assignments);
|
|
807
|
+
this.extractVerificationConditions(handlerArg, preconditions, postconditions);
|
|
808
|
+
}
|
|
809
|
+
const line = callExpr.getStartLineNumber();
|
|
810
|
+
return {
|
|
811
|
+
messageType,
|
|
812
|
+
node: context,
|
|
813
|
+
assignments,
|
|
814
|
+
preconditions,
|
|
815
|
+
postconditions,
|
|
816
|
+
location: {
|
|
817
|
+
file: filePath,
|
|
818
|
+
line
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
extractAssignments(funcNode, assignments) {
|
|
823
|
+
funcNode.forEachDescendant((node) => {
|
|
824
|
+
if (Node.isBinaryExpression(node)) {
|
|
825
|
+
const operator = node.getOperatorToken().getText();
|
|
826
|
+
if (operator === "=") {
|
|
827
|
+
const left = node.getLeft();
|
|
828
|
+
const right = node.getRight();
|
|
829
|
+
if (Node.isPropertyAccessExpression(left)) {
|
|
830
|
+
const fieldPath = this.getPropertyPath(left);
|
|
831
|
+
if (fieldPath.startsWith("state.")) {
|
|
832
|
+
const field = fieldPath.substring(6);
|
|
833
|
+
const value = this.extractValue(right);
|
|
834
|
+
if (value !== undefined) {
|
|
835
|
+
assignments.push({
|
|
836
|
+
field,
|
|
837
|
+
value
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
extractVerificationConditions(funcNode, preconditions, postconditions) {
|
|
847
|
+
const body = funcNode.getBody();
|
|
848
|
+
const statements = Node.isBlock(body) ? body.getStatements() : [body];
|
|
849
|
+
statements.forEach((statement, index) => {
|
|
850
|
+
if (Node.isExpressionStatement(statement)) {
|
|
851
|
+
const expr = statement.getExpression();
|
|
852
|
+
if (Node.isCallExpression(expr)) {
|
|
853
|
+
const callee = expr.getExpression();
|
|
854
|
+
if (Node.isIdentifier(callee)) {
|
|
855
|
+
const functionName = callee.getText();
|
|
856
|
+
if (functionName === "requires") {
|
|
857
|
+
const condition = this.extractCondition(expr);
|
|
858
|
+
if (condition) {
|
|
859
|
+
preconditions.push(condition);
|
|
860
|
+
}
|
|
861
|
+
} else if (functionName === "ensures") {
|
|
862
|
+
const condition = this.extractCondition(expr);
|
|
863
|
+
if (condition) {
|
|
864
|
+
postconditions.push(condition);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
extractCondition(callExpr) {
|
|
873
|
+
const args = callExpr.getArguments();
|
|
874
|
+
if (args.length === 0) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
const conditionArg = args[0];
|
|
878
|
+
const expression = conditionArg.getText();
|
|
879
|
+
let message;
|
|
880
|
+
if (args.length >= 2 && Node.isStringLiteral(args[1])) {
|
|
881
|
+
message = args[1].getLiteralValue();
|
|
882
|
+
}
|
|
883
|
+
const line = callExpr.getStartLineNumber();
|
|
884
|
+
const column = callExpr.getStartLinePos();
|
|
885
|
+
return {
|
|
886
|
+
expression,
|
|
887
|
+
message,
|
|
888
|
+
location: {
|
|
889
|
+
line,
|
|
890
|
+
column
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
getPropertyPath(node) {
|
|
895
|
+
const parts = [];
|
|
896
|
+
let current = node;
|
|
897
|
+
while (Node.isPropertyAccessExpression(current)) {
|
|
898
|
+
parts.unshift(current.getName());
|
|
899
|
+
current = current.getExpression();
|
|
900
|
+
}
|
|
901
|
+
if (Node.isIdentifier(current)) {
|
|
902
|
+
parts.unshift(current.getText());
|
|
903
|
+
}
|
|
904
|
+
return parts.join(".");
|
|
905
|
+
}
|
|
906
|
+
extractValue(node) {
|
|
907
|
+
if (Node.isStringLiteral(node)) {
|
|
908
|
+
return node.getLiteralValue();
|
|
909
|
+
}
|
|
910
|
+
if (Node.isNumericLiteral(node)) {
|
|
911
|
+
return node.getLiteralValue();
|
|
912
|
+
}
|
|
913
|
+
if (node.getKind() === SyntaxKind.TrueKeyword) {
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
if (node.getKind() === SyntaxKind.FalseKeyword) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
if (node.getKind() === SyntaxKind.NullKeyword) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
inferContext(filePath) {
|
|
925
|
+
const path = filePath.toLowerCase();
|
|
926
|
+
if (path.includes("/background/") || path.includes("\\background\\")) {
|
|
927
|
+
return "background";
|
|
928
|
+
}
|
|
929
|
+
if (path.includes("/content/") || path.includes("\\content\\")) {
|
|
930
|
+
return "content";
|
|
931
|
+
}
|
|
932
|
+
if (path.includes("/popup/") || path.includes("\\popup\\")) {
|
|
933
|
+
return "popup";
|
|
934
|
+
}
|
|
935
|
+
if (path.includes("/devtools/") || path.includes("\\devtools\\")) {
|
|
936
|
+
return "devtools";
|
|
937
|
+
}
|
|
938
|
+
if (path.includes("/options/") || path.includes("\\options\\")) {
|
|
939
|
+
return "options";
|
|
940
|
+
}
|
|
941
|
+
if (path.includes("/offscreen/") || path.includes("\\offscreen\\")) {
|
|
942
|
+
return "offscreen";
|
|
943
|
+
}
|
|
944
|
+
return "unknown";
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// vendor/analysis/src/extract/types.ts
|
|
949
|
+
class TypeExtractor {
|
|
950
|
+
project;
|
|
951
|
+
constructor(tsConfigPath) {
|
|
952
|
+
this.project = new Project2({
|
|
953
|
+
tsConfigFilePath: tsConfigPath
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
async analyzeCodebase(stateFilePath) {
|
|
957
|
+
const stateType = stateFilePath ? this.extractStateType(stateFilePath) : this.findStateType();
|
|
958
|
+
const messageTypes = this.findMessageTypes();
|
|
959
|
+
const fields = stateType ? this.analyzeFields(stateType) : [];
|
|
960
|
+
const configFilePath = this.project.getCompilerOptions().configFilePath;
|
|
961
|
+
const tsConfigPath = typeof configFilePath === "string" ? configFilePath : "tsconfig.json";
|
|
962
|
+
const handlerExtractor = new HandlerExtractor(tsConfigPath);
|
|
963
|
+
const handlerAnalysis = handlerExtractor.extractHandlers();
|
|
964
|
+
return {
|
|
965
|
+
stateType,
|
|
966
|
+
messageTypes: Array.from(new Set([...messageTypes, ...handlerAnalysis.messageTypes])),
|
|
967
|
+
fields,
|
|
968
|
+
handlers: handlerAnalysis.handlers
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
extractStateType(filePath) {
|
|
972
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
973
|
+
if (!sourceFile) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
const typeAlias = sourceFile.getTypeAlias("AppState") || sourceFile.getTypeAlias("State") || sourceFile.getTypeAliases()[0];
|
|
977
|
+
if (!typeAlias) {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
const type = typeAlias.getType();
|
|
981
|
+
return this.convertType(type, typeAlias.getName());
|
|
982
|
+
}
|
|
983
|
+
findStateType() {
|
|
984
|
+
const stateFiles = this.project.getSourceFiles("**/state*.ts");
|
|
985
|
+
for (const file of stateFiles) {
|
|
986
|
+
const typeAlias = file.getTypeAlias("AppState") || file.getTypeAlias("State");
|
|
987
|
+
if (typeAlias) {
|
|
988
|
+
const type = typeAlias.getType();
|
|
989
|
+
return this.convertType(type, typeAlias.getName());
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
findMessageTypes() {
|
|
995
|
+
const messageTypes = [];
|
|
996
|
+
const messageFiles = this.project.getSourceFiles("**/message*.ts");
|
|
997
|
+
for (const file of messageFiles) {
|
|
998
|
+
for (const typeAlias of file.getTypeAliases()) {
|
|
999
|
+
const type = typeAlias.getType();
|
|
1000
|
+
if (type.isUnion()) {
|
|
1001
|
+
for (const unionType of type.getUnionTypes()) {
|
|
1002
|
+
if (unionType.isObject()) {
|
|
1003
|
+
const typeProperty = unionType.getProperty("type");
|
|
1004
|
+
if (typeProperty) {
|
|
1005
|
+
const typeType = typeProperty.getTypeAtLocation(file);
|
|
1006
|
+
if (typeType.isStringLiteral()) {
|
|
1007
|
+
messageTypes.push(typeType.getLiteralValue());
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return [...new Set(messageTypes)];
|
|
1016
|
+
}
|
|
1017
|
+
convertType(type, name) {
|
|
1018
|
+
const nullable = type.isNullable();
|
|
1019
|
+
if (type.isBoolean() || type.isBooleanLiteral()) {
|
|
1020
|
+
return { name, kind: "boolean", nullable };
|
|
1021
|
+
}
|
|
1022
|
+
if (type.isUnion()) {
|
|
1023
|
+
const unionTypes = type.getUnionTypes();
|
|
1024
|
+
const allStringLiterals = unionTypes.every((t) => t.isStringLiteral());
|
|
1025
|
+
if (allStringLiterals) {
|
|
1026
|
+
const enumValues = unionTypes.map((t) => t.getLiteralValue());
|
|
1027
|
+
return {
|
|
1028
|
+
name,
|
|
1029
|
+
kind: "enum",
|
|
1030
|
+
nullable,
|
|
1031
|
+
enumValues
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const nonNullTypes = unionTypes.filter((t) => !t.isNull() && !t.isUndefined());
|
|
1035
|
+
if (nonNullTypes.length === 1) {
|
|
1036
|
+
const baseType = this.convertType(nonNullTypes[0], name);
|
|
1037
|
+
return {
|
|
1038
|
+
...baseType,
|
|
1039
|
+
nullable: true
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
name,
|
|
1044
|
+
kind: "union",
|
|
1045
|
+
nullable,
|
|
1046
|
+
unionTypes: unionTypes.map((t, i) => this.convertType(t, `${name}_${i}`))
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
if (type.isString() || type.isStringLiteral()) {
|
|
1050
|
+
return { name, kind: "string", nullable };
|
|
1051
|
+
}
|
|
1052
|
+
if (type.isNumber() || type.isNumberLiteral()) {
|
|
1053
|
+
return { name, kind: "number", nullable };
|
|
1054
|
+
}
|
|
1055
|
+
if (type.isArray()) {
|
|
1056
|
+
const elementType = type.getArrayElementType();
|
|
1057
|
+
return {
|
|
1058
|
+
name,
|
|
1059
|
+
kind: "array",
|
|
1060
|
+
nullable,
|
|
1061
|
+
elementType: elementType ? this.convertType(elementType, `${name}_element`) : { name: "unknown", kind: "unknown", nullable: false }
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
const symbol = type.getSymbol();
|
|
1065
|
+
if (symbol) {
|
|
1066
|
+
const symbolName = symbol.getName();
|
|
1067
|
+
if (symbolName === "Map") {
|
|
1068
|
+
const typeArgs = type.getTypeArguments();
|
|
1069
|
+
return {
|
|
1070
|
+
name,
|
|
1071
|
+
kind: "map",
|
|
1072
|
+
nullable,
|
|
1073
|
+
valueType: typeArgs && typeArgs[1] ? this.convertType(typeArgs[1], `${name}_value`) : undefined
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
if (symbolName === "Set") {
|
|
1077
|
+
const typeArgs = type.getTypeArguments();
|
|
1078
|
+
return {
|
|
1079
|
+
name,
|
|
1080
|
+
kind: "set",
|
|
1081
|
+
nullable,
|
|
1082
|
+
elementType: typeArgs && typeArgs[0] ? this.convertType(typeArgs[0], `${name}_element`) : undefined
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (type.isObject()) {
|
|
1087
|
+
const properties = {};
|
|
1088
|
+
for (const prop of type.getProperties()) {
|
|
1089
|
+
const propName = prop.getName();
|
|
1090
|
+
const propType = prop.getTypeAtLocation(this.project.getSourceFiles()[0]);
|
|
1091
|
+
properties[propName] = this.convertType(propType, propName);
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
name,
|
|
1095
|
+
kind: "object",
|
|
1096
|
+
nullable,
|
|
1097
|
+
properties
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (type.isNull()) {
|
|
1101
|
+
return { name, kind: "null", nullable: true };
|
|
1102
|
+
}
|
|
1103
|
+
return { name, kind: "unknown", nullable };
|
|
1104
|
+
}
|
|
1105
|
+
analyzeFields(stateType, prefix = "") {
|
|
1106
|
+
const fields = [];
|
|
1107
|
+
if (stateType.kind === "object" && stateType.properties) {
|
|
1108
|
+
for (const [key, propType] of Object.entries(stateType.properties)) {
|
|
1109
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
1110
|
+
if (propType.kind === "object") {
|
|
1111
|
+
fields.push(...this.analyzeFields(propType, path));
|
|
1112
|
+
} else {
|
|
1113
|
+
const analysis = this.analyzeField(path, propType);
|
|
1114
|
+
fields.push(analysis);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return fields;
|
|
1119
|
+
}
|
|
1120
|
+
analyzeField(path, type) {
|
|
1121
|
+
const analysis = {
|
|
1122
|
+
path,
|
|
1123
|
+
type,
|
|
1124
|
+
confidence: "low",
|
|
1125
|
+
evidence: [],
|
|
1126
|
+
suggestions: [],
|
|
1127
|
+
bounds: {}
|
|
1128
|
+
};
|
|
1129
|
+
if (type.kind === "boolean") {
|
|
1130
|
+
analysis.confidence = "high";
|
|
1131
|
+
analysis.evidence.push("Boolean type - auto-configured");
|
|
1132
|
+
return analysis;
|
|
1133
|
+
}
|
|
1134
|
+
if (type.kind === "enum" && type.enumValues) {
|
|
1135
|
+
analysis.confidence = "high";
|
|
1136
|
+
analysis.evidence.push(`Enum with ${type.enumValues.length} values`);
|
|
1137
|
+
analysis.bounds.values = type.enumValues;
|
|
1138
|
+
return analysis;
|
|
1139
|
+
}
|
|
1140
|
+
if (type.kind === "array") {
|
|
1141
|
+
analysis.confidence = "low";
|
|
1142
|
+
analysis.suggestions.push("Choose maxLength: 5 (fast), 10 (balanced), or 20 (thorough)");
|
|
1143
|
+
analysis.bounds.maxLength = undefined;
|
|
1144
|
+
const foundBound = this.findArrayBound(path);
|
|
1145
|
+
if (foundBound) {
|
|
1146
|
+
analysis.confidence = "medium";
|
|
1147
|
+
analysis.evidence.push(`Found array check: ${foundBound.evidence}`);
|
|
1148
|
+
analysis.bounds.maxLength = foundBound.value;
|
|
1149
|
+
}
|
|
1150
|
+
return analysis;
|
|
1151
|
+
}
|
|
1152
|
+
if (type.kind === "number") {
|
|
1153
|
+
analysis.confidence = "low";
|
|
1154
|
+
analysis.suggestions.push("Provide min and max values based on your application logic");
|
|
1155
|
+
analysis.bounds.min = undefined;
|
|
1156
|
+
analysis.bounds.max = undefined;
|
|
1157
|
+
const foundBound = this.findNumberBound(path);
|
|
1158
|
+
if (foundBound) {
|
|
1159
|
+
analysis.confidence = "high";
|
|
1160
|
+
analysis.evidence.push(`Found comparison: ${foundBound.evidence}`);
|
|
1161
|
+
analysis.bounds = { ...analysis.bounds, ...foundBound.bounds };
|
|
1162
|
+
}
|
|
1163
|
+
return analysis;
|
|
1164
|
+
}
|
|
1165
|
+
if (type.kind === "string") {
|
|
1166
|
+
analysis.confidence = "low";
|
|
1167
|
+
analysis.suggestions.push('Provide 2-3 example values: ["value1", "value2", "value3"]', "Or use { abstract: true } for symbolic verification");
|
|
1168
|
+
analysis.bounds.values = undefined;
|
|
1169
|
+
return analysis;
|
|
1170
|
+
}
|
|
1171
|
+
if (type.kind === "map" || type.kind === "set") {
|
|
1172
|
+
analysis.confidence = "low";
|
|
1173
|
+
analysis.suggestions.push("Provide maxSize (recommended: 3-5)");
|
|
1174
|
+
analysis.bounds.maxSize = undefined;
|
|
1175
|
+
return analysis;
|
|
1176
|
+
}
|
|
1177
|
+
return analysis;
|
|
1178
|
+
}
|
|
1179
|
+
findArrayBound(path) {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
findNumberBound(path) {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async function analyzeCodebase(options) {
|
|
1187
|
+
const extractor = new TypeExtractor(options.tsConfigPath);
|
|
1188
|
+
return extractor.analyzeCodebase(options.stateFilePath);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// vendor/verify/src/codegen/config.ts
|
|
1192
|
+
class ConfigGenerator {
|
|
1193
|
+
lines = [];
|
|
1194
|
+
indent = 0;
|
|
1195
|
+
generate(analysis) {
|
|
1196
|
+
this.lines = [];
|
|
1197
|
+
this.indent = 0;
|
|
1198
|
+
this.addHeader();
|
|
1199
|
+
this.addImports();
|
|
1200
|
+
this.addExport();
|
|
1201
|
+
this.addStateConfig(analysis.fields);
|
|
1202
|
+
this.addMessagesConfig();
|
|
1203
|
+
this.addBehaviorConfig();
|
|
1204
|
+
this.closeExport();
|
|
1205
|
+
return this.lines.join(`
|
|
1206
|
+
`);
|
|
1207
|
+
}
|
|
1208
|
+
addHeader() {
|
|
1209
|
+
this.line("// ═══════════════════════════════════════════════════════════════");
|
|
1210
|
+
this.line("// Verification Configuration");
|
|
1211
|
+
this.line("// ═══════════════════════════════════════════════════════════════");
|
|
1212
|
+
this.line("//");
|
|
1213
|
+
this.line("// This file configures TLA+ verification for your extension.");
|
|
1214
|
+
this.line("// Some values are auto-configured, others need your input.");
|
|
1215
|
+
this.line("//");
|
|
1216
|
+
this.line("// Look for:");
|
|
1217
|
+
this.line("// • /* CONFIGURE */ - Replace with your value");
|
|
1218
|
+
this.line("// • /* REVIEW */ - Check the auto-generated value");
|
|
1219
|
+
this.line("// • null - Must be replaced with a concrete value");
|
|
1220
|
+
this.line("//");
|
|
1221
|
+
this.line("// Run 'bun verify' to check for incomplete configuration.");
|
|
1222
|
+
this.line("// Run 'bun verify --setup' for interactive help.");
|
|
1223
|
+
this.line("//");
|
|
1224
|
+
this.line("");
|
|
1225
|
+
}
|
|
1226
|
+
addImports() {
|
|
1227
|
+
this.line("import { defineVerification } from '@fairfox/polly/verify'");
|
|
1228
|
+
this.line("");
|
|
1229
|
+
}
|
|
1230
|
+
addExport() {
|
|
1231
|
+
this.line("export default defineVerification({");
|
|
1232
|
+
this.indent++;
|
|
1233
|
+
}
|
|
1234
|
+
closeExport() {
|
|
1235
|
+
this.indent--;
|
|
1236
|
+
this.line("})");
|
|
1237
|
+
}
|
|
1238
|
+
addStateConfig(fields) {
|
|
1239
|
+
this.line("state: {");
|
|
1240
|
+
this.indent++;
|
|
1241
|
+
for (let i = 0;i < fields.length; i++) {
|
|
1242
|
+
const field = fields[i];
|
|
1243
|
+
if (i > 0) {
|
|
1244
|
+
this.line("");
|
|
1245
|
+
}
|
|
1246
|
+
this.addFieldConfig(field);
|
|
1247
|
+
}
|
|
1248
|
+
this.indent--;
|
|
1249
|
+
this.line("},");
|
|
1250
|
+
this.line("");
|
|
1251
|
+
}
|
|
1252
|
+
addFieldConfig(field) {
|
|
1253
|
+
this.addFieldComment(field);
|
|
1254
|
+
const config = this.generateFieldConfig(field);
|
|
1255
|
+
this.line(`"${field.path}": ${config},`);
|
|
1256
|
+
}
|
|
1257
|
+
addFieldComment(field) {
|
|
1258
|
+
const separator = "─".repeat(60);
|
|
1259
|
+
this.line(`// ${separator}`);
|
|
1260
|
+
this.line(`// ${field.path}: ${this.formatTypeName(field.type)}`);
|
|
1261
|
+
this.line(`// ${separator}`);
|
|
1262
|
+
if (field.confidence === "high") {
|
|
1263
|
+
this.line("// ✓ Auto-configured from code analysis");
|
|
1264
|
+
if (field.evidence.length > 0) {
|
|
1265
|
+
for (const evidence of field.evidence) {
|
|
1266
|
+
this.line(`// ${evidence}`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
this.line("//");
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (field.confidence === "medium") {
|
|
1273
|
+
this.line("// ⚠️ Please review this auto-generated value");
|
|
1274
|
+
if (field.evidence.length > 0) {
|
|
1275
|
+
for (const evidence of field.evidence) {
|
|
1276
|
+
this.line(`// Found: ${evidence}`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
this.line("//");
|
|
1280
|
+
this.line("// REVIEW: Adjust if needed");
|
|
1281
|
+
this.line("//");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
this.line("// ⚠️ Manual configuration required");
|
|
1285
|
+
this.line("//");
|
|
1286
|
+
this.addTypeGuidance(field);
|
|
1287
|
+
if (field.suggestions.length > 0) {
|
|
1288
|
+
this.line("//");
|
|
1289
|
+
for (const suggestion of field.suggestions) {
|
|
1290
|
+
this.line(`// ${suggestion}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
this.line("//");
|
|
1294
|
+
this.line("// CONFIGURE: Fill in the value below");
|
|
1295
|
+
this.line("//");
|
|
1296
|
+
}
|
|
1297
|
+
addTypeGuidance(field) {
|
|
1298
|
+
switch (field.type.kind) {
|
|
1299
|
+
case "array":
|
|
1300
|
+
this.line("// This array has no bounds in your code. Choose a maximum");
|
|
1301
|
+
this.line("// length for verification. Tradeoffs:");
|
|
1302
|
+
this.line("// • Small (3-5): Fast, catches basic bugs");
|
|
1303
|
+
this.line("// • Medium (10-15): Balanced, catches most bugs");
|
|
1304
|
+
this.line("// • Large (20+): Thorough, much slower");
|
|
1305
|
+
break;
|
|
1306
|
+
case "string":
|
|
1307
|
+
this.line("// Strings need concrete values for precise verification.");
|
|
1308
|
+
this.line("// Provide 2-3 representative values from your app.");
|
|
1309
|
+
if (field.type.nullable) {
|
|
1310
|
+
this.line("//");
|
|
1311
|
+
this.line("// Note: This field is nullable (can be null)");
|
|
1312
|
+
}
|
|
1313
|
+
this.line("//");
|
|
1314
|
+
this.line("// Examples:");
|
|
1315
|
+
this.line('// ["user_abc123", "user_xyz789", "guest_000"]');
|
|
1316
|
+
this.line('// ["active", "inactive", "pending"]');
|
|
1317
|
+
this.line("//");
|
|
1318
|
+
this.line("// Alternative: Use abstract verification (less precise, faster)");
|
|
1319
|
+
this.line("// { abstract: true }");
|
|
1320
|
+
break;
|
|
1321
|
+
case "number":
|
|
1322
|
+
this.line("// Numbers need a range. Choose min and max values based on");
|
|
1323
|
+
this.line("// realistic bounds in your application.");
|
|
1324
|
+
if (field.type.nullable) {
|
|
1325
|
+
this.line("//");
|
|
1326
|
+
this.line("// Note: This field is nullable (can be null)");
|
|
1327
|
+
}
|
|
1328
|
+
this.line("//");
|
|
1329
|
+
this.line("// Examples:");
|
|
1330
|
+
this.line("// { min: 0, max: 100 } // Counter");
|
|
1331
|
+
this.line("// { min: 0, max: 999999 } // Timestamp");
|
|
1332
|
+
break;
|
|
1333
|
+
case "map":
|
|
1334
|
+
case "set":
|
|
1335
|
+
this.line(`// ${field.type.kind} needs a maximum size. How many entries`);
|
|
1336
|
+
this.line("// do you need to model to catch bugs?");
|
|
1337
|
+
this.line("//");
|
|
1338
|
+
this.line("// Recommended: 3-5 for most cases");
|
|
1339
|
+
break;
|
|
1340
|
+
case "object":
|
|
1341
|
+
this.line("// Complex nested object. Configure each field separately.");
|
|
1342
|
+
break;
|
|
1343
|
+
default:
|
|
1344
|
+
this.line(`// ${field.type.kind} type requires configuration.`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
generateFieldConfig(field) {
|
|
1348
|
+
switch (field.type.kind) {
|
|
1349
|
+
case "boolean":
|
|
1350
|
+
return "{ type: 'boolean' }";
|
|
1351
|
+
case "enum":
|
|
1352
|
+
if (field.type.enumValues) {
|
|
1353
|
+
const values = field.type.enumValues.map((v) => `"${v}"`).join(", ");
|
|
1354
|
+
return `{ type: "enum", values: [${values}] }`;
|
|
1355
|
+
}
|
|
1356
|
+
return "{ type: 'enum', values: /* CONFIGURE */ null }";
|
|
1357
|
+
case "array":
|
|
1358
|
+
if (field.bounds?.maxLength !== undefined && field.bounds.maxLength !== null) {
|
|
1359
|
+
if (field.confidence === "medium") {
|
|
1360
|
+
return `{ maxLength: /* REVIEW */ ${field.bounds.maxLength} }`;
|
|
1361
|
+
}
|
|
1362
|
+
return `{ maxLength: ${field.bounds.maxLength} }`;
|
|
1363
|
+
}
|
|
1364
|
+
return "{ maxLength: /* CONFIGURE */ null }";
|
|
1365
|
+
case "number":
|
|
1366
|
+
if (field.bounds?.min !== undefined && field.bounds?.max !== undefined) {
|
|
1367
|
+
const minStr = field.bounds.min !== null ? field.bounds.min : "/* CONFIGURE */";
|
|
1368
|
+
const maxStr = field.bounds.max !== null ? field.bounds.max : "/* CONFIGURE */";
|
|
1369
|
+
if (field.confidence === "high") {
|
|
1370
|
+
return `{ min: ${minStr}, max: ${maxStr} }`;
|
|
1371
|
+
}
|
|
1372
|
+
return `{ min: /* REVIEW */ ${minStr}, max: /* REVIEW */ ${maxStr} }`;
|
|
1373
|
+
}
|
|
1374
|
+
return "{ min: /* CONFIGURE */ null, max: /* CONFIGURE */ null }";
|
|
1375
|
+
case "string":
|
|
1376
|
+
return "{ values: /* CONFIGURE */ null }";
|
|
1377
|
+
case "map":
|
|
1378
|
+
case "set":
|
|
1379
|
+
return "{ maxSize: /* CONFIGURE */ null }";
|
|
1380
|
+
default:
|
|
1381
|
+
return "{ /* CONFIGURE */ }";
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
addMessagesConfig() {
|
|
1385
|
+
this.line("messages: {");
|
|
1386
|
+
this.indent++;
|
|
1387
|
+
this.line("// Maximum messages in flight simultaneously across all contexts.");
|
|
1388
|
+
this.line("// Higher = more realistic concurrency, but exponentially slower.");
|
|
1389
|
+
this.line("//");
|
|
1390
|
+
this.line("// Recommended values:");
|
|
1391
|
+
this.line("// • 2-3: Fast verification (< 10 seconds)");
|
|
1392
|
+
this.line("// • 4-6: Balanced (10-60 seconds)");
|
|
1393
|
+
this.line("// • 8+: Thorough but slow (minutes)");
|
|
1394
|
+
this.line("//");
|
|
1395
|
+
this.line("// WARNING: State space grows exponentially! Start small.");
|
|
1396
|
+
this.line("maxInFlight: 3,");
|
|
1397
|
+
this.line("");
|
|
1398
|
+
this.line("// Maximum tab IDs to model (content scripts are per-tab).");
|
|
1399
|
+
this.line("//");
|
|
1400
|
+
this.line("// Recommended:");
|
|
1401
|
+
this.line("// • 0-1: Most extensions (single tab or tab-agnostic)");
|
|
1402
|
+
this.line("// • 2-3: Multi-tab coordination");
|
|
1403
|
+
this.line("//");
|
|
1404
|
+
this.line("// Start with 0 or 1 for faster verification.");
|
|
1405
|
+
this.line("maxTabs: 1,");
|
|
1406
|
+
this.indent--;
|
|
1407
|
+
this.line("},");
|
|
1408
|
+
this.line("");
|
|
1409
|
+
}
|
|
1410
|
+
addBehaviorConfig() {
|
|
1411
|
+
this.line("// Verification behavior");
|
|
1412
|
+
this.line("// ─────────────────────");
|
|
1413
|
+
this.line("//");
|
|
1414
|
+
this.line("// onBuild: What to do during development builds");
|
|
1415
|
+
this.line("// • 'warn' - Show warnings but don't fail (recommended)");
|
|
1416
|
+
this.line("// • 'error' - Fail the build on violations");
|
|
1417
|
+
this.line("// • 'off' - Skip verification");
|
|
1418
|
+
this.line("//");
|
|
1419
|
+
this.line("onBuild: 'warn',");
|
|
1420
|
+
this.line("");
|
|
1421
|
+
this.line("// onRelease: What to do during production builds");
|
|
1422
|
+
this.line("// • 'error' - Fail the build on violations (recommended)");
|
|
1423
|
+
this.line("// • 'warn' - Show warnings but don't fail");
|
|
1424
|
+
this.line("// • 'off' - Skip verification");
|
|
1425
|
+
this.line("//");
|
|
1426
|
+
this.line("onRelease: 'error',");
|
|
1427
|
+
}
|
|
1428
|
+
formatTypeName(type) {
|
|
1429
|
+
let typeName;
|
|
1430
|
+
switch (type.kind) {
|
|
1431
|
+
case "boolean":
|
|
1432
|
+
typeName = "boolean";
|
|
1433
|
+
break;
|
|
1434
|
+
case "string":
|
|
1435
|
+
typeName = "string";
|
|
1436
|
+
break;
|
|
1437
|
+
case "number":
|
|
1438
|
+
typeName = "number";
|
|
1439
|
+
break;
|
|
1440
|
+
case "enum":
|
|
1441
|
+
if (type.enumValues) {
|
|
1442
|
+
typeName = type.enumValues.map((v) => `"${v}"`).join(" | ");
|
|
1443
|
+
} else {
|
|
1444
|
+
typeName = "enum";
|
|
1445
|
+
}
|
|
1446
|
+
break;
|
|
1447
|
+
case "array":
|
|
1448
|
+
if (type.elementType) {
|
|
1449
|
+
typeName = `${this.formatTypeName(type.elementType)}[]`;
|
|
1450
|
+
} else {
|
|
1451
|
+
typeName = "array";
|
|
1452
|
+
}
|
|
1453
|
+
break;
|
|
1454
|
+
case "object":
|
|
1455
|
+
typeName = "object";
|
|
1456
|
+
break;
|
|
1457
|
+
case "map":
|
|
1458
|
+
typeName = "Map";
|
|
1459
|
+
break;
|
|
1460
|
+
case "set":
|
|
1461
|
+
typeName = "Set";
|
|
1462
|
+
break;
|
|
1463
|
+
case "null":
|
|
1464
|
+
typeName = "null";
|
|
1465
|
+
break;
|
|
1466
|
+
default:
|
|
1467
|
+
typeName = "unknown";
|
|
1468
|
+
}
|
|
1469
|
+
if (type.nullable && type.kind !== "null") {
|
|
1470
|
+
typeName += " | null";
|
|
1471
|
+
}
|
|
1472
|
+
return typeName;
|
|
1473
|
+
}
|
|
1474
|
+
line(content) {
|
|
1475
|
+
if (content === "") {
|
|
1476
|
+
this.lines.push("");
|
|
1477
|
+
} else {
|
|
1478
|
+
const indentation = " ".repeat(this.indent);
|
|
1479
|
+
this.lines.push(indentation + content);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
function generateConfig(analysis) {
|
|
1484
|
+
const generator = new ConfigGenerator;
|
|
1485
|
+
return generator.generate(analysis);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// vendor/verify/src/config/parser.ts
|
|
1489
|
+
import * as fs from "node:fs";
|
|
1490
|
+
import * as path from "node:path";
|
|
1491
|
+
class ConfigValidator {
|
|
1492
|
+
issues = [];
|
|
1493
|
+
validate(configPath) {
|
|
1494
|
+
this.issues = [];
|
|
1495
|
+
if (!fs.existsSync(configPath)) {
|
|
1496
|
+
this.issues.push({
|
|
1497
|
+
type: "incomplete",
|
|
1498
|
+
severity: "error",
|
|
1499
|
+
message: "Configuration file does not exist",
|
|
1500
|
+
suggestion: "Run 'bun verify --setup' to generate configuration"
|
|
1501
|
+
});
|
|
1502
|
+
return {
|
|
1503
|
+
valid: false,
|
|
1504
|
+
issues: this.issues
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
const configSource = fs.readFileSync(configPath, "utf-8");
|
|
1508
|
+
this.checkConfigureMarkers(configSource);
|
|
1509
|
+
this.checkReviewMarkers(configSource);
|
|
1510
|
+
try {
|
|
1511
|
+
const config = this.loadConfig(configPath);
|
|
1512
|
+
this.validateConfig(config);
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
this.issues.push({
|
|
1515
|
+
type: "invalid_value",
|
|
1516
|
+
severity: "error",
|
|
1517
|
+
message: `Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`,
|
|
1518
|
+
suggestion: "Check for syntax errors in the configuration file"
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
const hasErrors = this.issues.some((i) => i.severity === "error");
|
|
1522
|
+
return {
|
|
1523
|
+
valid: !hasErrors,
|
|
1524
|
+
issues: this.issues
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
checkConfigureMarkers(source) {
|
|
1528
|
+
const configureRegex = /\/\*\s*CONFIGURE\s*\*\//g;
|
|
1529
|
+
const matches = [...source.matchAll(configureRegex)];
|
|
1530
|
+
if (matches.length > 0) {
|
|
1531
|
+
const lines = source.split(`
|
|
1532
|
+
`);
|
|
1533
|
+
const locations = [];
|
|
1534
|
+
for (const match of matches) {
|
|
1535
|
+
const position = match.index;
|
|
1536
|
+
const lineNumber = source.substring(0, position).split(`
|
|
1537
|
+
`).length;
|
|
1538
|
+
const line = lines[lineNumber - 1];
|
|
1539
|
+
const fieldMatch = line.match(/"([^"]+)":\s*{/);
|
|
1540
|
+
const fieldName = fieldMatch ? fieldMatch[1] : "unknown";
|
|
1541
|
+
locations.push({
|
|
1542
|
+
line: lineNumber,
|
|
1543
|
+
column: match.index - source.lastIndexOf(`
|
|
1544
|
+
`, position),
|
|
1545
|
+
context: fieldName
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
this.issues.push({
|
|
1549
|
+
type: "incomplete",
|
|
1550
|
+
severity: "error",
|
|
1551
|
+
message: `Found ${matches.length} incomplete configuration marker(s)`,
|
|
1552
|
+
suggestion: "Replace all /* CONFIGURE */ markers with actual values"
|
|
1553
|
+
});
|
|
1554
|
+
for (const loc of locations) {
|
|
1555
|
+
this.issues.push({
|
|
1556
|
+
type: "incomplete",
|
|
1557
|
+
severity: "error",
|
|
1558
|
+
field: loc.context,
|
|
1559
|
+
location: { line: loc.line, column: loc.column },
|
|
1560
|
+
message: `Incomplete configuration at line ${loc.line}`,
|
|
1561
|
+
suggestion: `Fill in value for "${loc.context}"`
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
checkReviewMarkers(source) {
|
|
1567
|
+
const reviewRegex = /\/\*\s*REVIEW\s*\*\//g;
|
|
1568
|
+
const matches = [...source.matchAll(reviewRegex)];
|
|
1569
|
+
if (matches.length > 0) {
|
|
1570
|
+
this.issues.push({
|
|
1571
|
+
type: "incomplete",
|
|
1572
|
+
severity: "warning",
|
|
1573
|
+
message: `Found ${matches.length} value(s) that should be reviewed`,
|
|
1574
|
+
suggestion: "Review auto-generated values marked with /* REVIEW */"
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
loadConfig(configPath) {
|
|
1579
|
+
delete __require.cache[__require.resolve(path.resolve(configPath))];
|
|
1580
|
+
const module = __require(path.resolve(configPath));
|
|
1581
|
+
return module.default || module;
|
|
1582
|
+
}
|
|
1583
|
+
validateConfig(config) {
|
|
1584
|
+
this.findNullPlaceholders(config.state, "state");
|
|
1585
|
+
this.findNullPlaceholders(config.messages, "messages");
|
|
1586
|
+
this.validateBounds(config);
|
|
1587
|
+
}
|
|
1588
|
+
findNullPlaceholders(obj, path2) {
|
|
1589
|
+
if (obj === null || obj === undefined) {
|
|
1590
|
+
this.issues.push({
|
|
1591
|
+
type: "null_placeholder",
|
|
1592
|
+
severity: "error",
|
|
1593
|
+
field: path2,
|
|
1594
|
+
message: `Configuration incomplete: ${path2}`,
|
|
1595
|
+
suggestion: "Replace null with a concrete value"
|
|
1596
|
+
});
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (typeof obj !== "object") {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1603
|
+
const fullPath = `${path2}.${key}`;
|
|
1604
|
+
if (value === null) {
|
|
1605
|
+
this.issues.push({
|
|
1606
|
+
type: "null_placeholder",
|
|
1607
|
+
severity: "error",
|
|
1608
|
+
field: fullPath,
|
|
1609
|
+
message: `Configuration incomplete: ${fullPath}`,
|
|
1610
|
+
suggestion: "Replace null with a concrete value"
|
|
1611
|
+
});
|
|
1612
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1613
|
+
this.findNullPlaceholders(value, fullPath);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
validateBounds(config) {
|
|
1618
|
+
if (config.messages.maxInFlight !== null) {
|
|
1619
|
+
if (config.messages.maxInFlight < 1) {
|
|
1620
|
+
this.issues.push({
|
|
1621
|
+
type: "invalid_value",
|
|
1622
|
+
severity: "error",
|
|
1623
|
+
field: "messages.maxInFlight",
|
|
1624
|
+
message: "maxInFlight must be at least 1",
|
|
1625
|
+
suggestion: "Use a value between 4-10 for most cases"
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
if (config.messages.maxInFlight > 20) {
|
|
1629
|
+
this.issues.push({
|
|
1630
|
+
type: "unrealistic_bound",
|
|
1631
|
+
severity: "warning",
|
|
1632
|
+
field: "messages.maxInFlight",
|
|
1633
|
+
message: "Very high maxInFlight (>20) will slow verification significantly",
|
|
1634
|
+
suggestion: "Use 4-10 for development, up to 20 for thorough verification"
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (config.messages.maxTabs !== null) {
|
|
1639
|
+
if (config.messages.maxTabs < 1) {
|
|
1640
|
+
this.issues.push({
|
|
1641
|
+
type: "invalid_value",
|
|
1642
|
+
severity: "error",
|
|
1643
|
+
field: "messages.maxTabs",
|
|
1644
|
+
message: "maxTabs must be at least 1",
|
|
1645
|
+
suggestion: "Use 2-3 for most cases"
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
if (config.messages.maxTabs > 10) {
|
|
1649
|
+
this.issues.push({
|
|
1650
|
+
type: "unrealistic_bound",
|
|
1651
|
+
severity: "warning",
|
|
1652
|
+
field: "messages.maxTabs",
|
|
1653
|
+
message: "Very high maxTabs (>10) will slow verification significantly",
|
|
1654
|
+
suggestion: "Use 2-3 for most cases"
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
for (const [fieldName, fieldConfig] of Object.entries(config.state)) {
|
|
1659
|
+
if (typeof fieldConfig !== "object" || fieldConfig === null) {
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
if ("maxLength" in fieldConfig) {
|
|
1663
|
+
const maxLength = fieldConfig.maxLength;
|
|
1664
|
+
if (maxLength !== null) {
|
|
1665
|
+
if (maxLength < 0) {
|
|
1666
|
+
this.issues.push({
|
|
1667
|
+
type: "invalid_value",
|
|
1668
|
+
severity: "error",
|
|
1669
|
+
field: `state.${fieldName}.maxLength`,
|
|
1670
|
+
message: "maxLength cannot be negative",
|
|
1671
|
+
suggestion: "Use a positive number"
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
if (maxLength > 50) {
|
|
1675
|
+
this.issues.push({
|
|
1676
|
+
type: "unrealistic_bound",
|
|
1677
|
+
severity: "warning",
|
|
1678
|
+
field: `state.${fieldName}.maxLength`,
|
|
1679
|
+
message: `Very large maxLength (${maxLength}) will slow verification`,
|
|
1680
|
+
suggestion: "Use 10-20 for most cases"
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if ("min" in fieldConfig && "max" in fieldConfig) {
|
|
1686
|
+
const min = fieldConfig.min;
|
|
1687
|
+
const max = fieldConfig.max;
|
|
1688
|
+
if (min !== null && max !== null && min > max) {
|
|
1689
|
+
this.issues.push({
|
|
1690
|
+
type: "invalid_value",
|
|
1691
|
+
severity: "error",
|
|
1692
|
+
field: `state.${fieldName}`,
|
|
1693
|
+
message: `Invalid range: min (${min}) > max (${max})`,
|
|
1694
|
+
suggestion: "Ensure min is less than or equal to max"
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
if (min !== null && max !== null && max - min > 1000) {
|
|
1698
|
+
this.issues.push({
|
|
1699
|
+
type: "unrealistic_bound",
|
|
1700
|
+
severity: "warning",
|
|
1701
|
+
field: `state.${fieldName}`,
|
|
1702
|
+
message: `Very large number range (${max - min}) will slow verification`,
|
|
1703
|
+
suggestion: "Use smaller ranges when possible"
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
if ("maxSize" in fieldConfig) {
|
|
1708
|
+
const maxSize = fieldConfig.maxSize;
|
|
1709
|
+
if (maxSize !== null) {
|
|
1710
|
+
if (maxSize < 0) {
|
|
1711
|
+
this.issues.push({
|
|
1712
|
+
type: "invalid_value",
|
|
1713
|
+
severity: "error",
|
|
1714
|
+
field: `state.${fieldName}.maxSize`,
|
|
1715
|
+
message: "maxSize cannot be negative",
|
|
1716
|
+
suggestion: "Use a positive number"
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
if (maxSize > 20) {
|
|
1720
|
+
this.issues.push({
|
|
1721
|
+
type: "unrealistic_bound",
|
|
1722
|
+
severity: "warning",
|
|
1723
|
+
field: `state.${fieldName}.maxSize`,
|
|
1724
|
+
message: `Very large maxSize (${maxSize}) will slow verification`,
|
|
1725
|
+
suggestion: "Use 3-5 for most cases"
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
function validateConfig(configPath) {
|
|
1734
|
+
const validator = new ConfigValidator;
|
|
1735
|
+
return validator.validate(configPath);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// vendor/verify/src/cli.ts
|
|
1739
|
+
var COLORS = {
|
|
1740
|
+
reset: "\x1B[0m",
|
|
1741
|
+
red: "\x1B[31m",
|
|
1742
|
+
green: "\x1B[32m",
|
|
1743
|
+
yellow: "\x1B[33m",
|
|
1744
|
+
blue: "\x1B[34m",
|
|
1745
|
+
gray: "\x1B[90m"
|
|
1746
|
+
};
|
|
1747
|
+
function color(text, colorCode) {
|
|
1748
|
+
return `${colorCode}${text}${COLORS.reset}`;
|
|
1749
|
+
}
|
|
1750
|
+
async function main() {
|
|
1751
|
+
const args = process.argv.slice(2);
|
|
1752
|
+
const command = args[0];
|
|
1753
|
+
switch (command) {
|
|
1754
|
+
case "--setup":
|
|
1755
|
+
case "setup":
|
|
1756
|
+
await setupCommand();
|
|
1757
|
+
break;
|
|
1758
|
+
case "--validate":
|
|
1759
|
+
case "validate":
|
|
1760
|
+
await validateCommand();
|
|
1761
|
+
break;
|
|
1762
|
+
case "--help":
|
|
1763
|
+
case "help":
|
|
1764
|
+
showHelp();
|
|
1765
|
+
break;
|
|
1766
|
+
default:
|
|
1767
|
+
await verifyCommand();
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
async function setupCommand() {
|
|
1771
|
+
console.log(color(`
|
|
1772
|
+
\uD83D\uDD0D Analyzing codebase...
|
|
1773
|
+
`, COLORS.blue));
|
|
1774
|
+
try {
|
|
1775
|
+
const tsConfigPath = findTsConfig();
|
|
1776
|
+
if (!tsConfigPath) {
|
|
1777
|
+
console.error(color("❌ Could not find tsconfig.json", COLORS.red));
|
|
1778
|
+
console.error(" Run this command from your project root");
|
|
1779
|
+
process.exit(1);
|
|
1780
|
+
}
|
|
1781
|
+
console.log(color(` Using: ${tsConfigPath}`, COLORS.gray));
|
|
1782
|
+
const analysis = await analyzeCodebase({
|
|
1783
|
+
tsConfigPath,
|
|
1784
|
+
stateFilePath: findStateFile()
|
|
1785
|
+
});
|
|
1786
|
+
if (!analysis.stateType) {
|
|
1787
|
+
console.log(color(`
|
|
1788
|
+
⚠️ Could not find state type definition`, COLORS.yellow));
|
|
1789
|
+
console.log(" Expected to find a type named 'AppState' or 'State'");
|
|
1790
|
+
console.log(" in a file matching **/state*.ts");
|
|
1791
|
+
console.log();
|
|
1792
|
+
console.log(" You can still generate a config template:");
|
|
1793
|
+
console.log(" It will be empty and you'll need to fill it in manually.");
|
|
1794
|
+
console.log();
|
|
1795
|
+
} else {
|
|
1796
|
+
console.log(color(`✓ Found state type with ${analysis.fields.length} field(s)`, COLORS.green));
|
|
1797
|
+
}
|
|
1798
|
+
console.log(color(`✓ Found ${analysis.messageTypes.length} message type(s)`, COLORS.green));
|
|
1799
|
+
if (analysis.fields.length > 0) {
|
|
1800
|
+
console.log(color(`
|
|
1801
|
+
\uD83D\uDCCA Configuration Summary:
|
|
1802
|
+
`, COLORS.blue));
|
|
1803
|
+
const table = [
|
|
1804
|
+
["Field", "Type", "Status"],
|
|
1805
|
+
["─".repeat(30), "─".repeat(20), "─".repeat(20)]
|
|
1806
|
+
];
|
|
1807
|
+
for (const field of analysis.fields) {
|
|
1808
|
+
const status = field.confidence === "high" ? color("✓ Auto-configured", COLORS.green) : field.confidence === "medium" ? color("⚠ Review needed", COLORS.yellow) : color("⚠ Manual config", COLORS.red);
|
|
1809
|
+
table.push([field.path, field.type.kind, status]);
|
|
1810
|
+
}
|
|
1811
|
+
for (const row of table) {
|
|
1812
|
+
console.log(` ${row[0].padEnd(32)} ${row[1].padEnd(22)} ${row[2]}`);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
const configContent = generateConfig(analysis);
|
|
1816
|
+
const configPath = path3.join(process.cwd(), "specs", "verification.config.ts");
|
|
1817
|
+
const configDir = path3.dirname(configPath);
|
|
1818
|
+
if (!fs3.existsSync(configDir)) {
|
|
1819
|
+
fs3.mkdirSync(configDir, { recursive: true });
|
|
1820
|
+
}
|
|
1821
|
+
fs3.writeFileSync(configPath, configContent, "utf-8");
|
|
1822
|
+
console.log(color(`
|
|
1823
|
+
✅ Configuration generated!
|
|
1824
|
+
`, COLORS.green));
|
|
1825
|
+
console.log(` File: ${color(configPath, COLORS.blue)}`);
|
|
1826
|
+
console.log();
|
|
1827
|
+
console.log(color("\uD83D\uDCDD Next steps:", COLORS.blue));
|
|
1828
|
+
console.log();
|
|
1829
|
+
console.log(" 1. Review the generated configuration file");
|
|
1830
|
+
console.log(" 2. Fill in values marked with /* CONFIGURE */");
|
|
1831
|
+
console.log(" 3. Run 'bun verify' to check your configuration");
|
|
1832
|
+
console.log();
|
|
1833
|
+
console.log(color("\uD83D\uDCA1 Tip:", COLORS.gray));
|
|
1834
|
+
console.log(color(" Look for comments explaining what each field needs.", COLORS.gray));
|
|
1835
|
+
console.log();
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
console.error(color(`
|
|
1838
|
+
❌ Setup failed:`, COLORS.red));
|
|
1839
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
1840
|
+
process.exit(1);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
async function validateCommand() {
|
|
1844
|
+
const configPath = path3.join(process.cwd(), "specs", "verification.config.ts");
|
|
1845
|
+
console.log(color(`
|
|
1846
|
+
\uD83D\uDD0D Validating configuration...
|
|
1847
|
+
`, COLORS.blue));
|
|
1848
|
+
const result = validateConfig(configPath);
|
|
1849
|
+
if (result.valid) {
|
|
1850
|
+
console.log(color(`✅ Configuration is complete and valid!
|
|
1851
|
+
`, COLORS.green));
|
|
1852
|
+
console.log(" You can now run 'bun verify' to start verification.");
|
|
1853
|
+
console.log();
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
1857
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
1858
|
+
if (errors.length > 0) {
|
|
1859
|
+
console.log(color(`❌ Found ${errors.length} error(s):
|
|
1860
|
+
`, COLORS.red));
|
|
1861
|
+
for (const error of errors) {
|
|
1862
|
+
console.log(color(` • ${error.message}`, COLORS.red));
|
|
1863
|
+
if (error.field) {
|
|
1864
|
+
console.log(color(` Field: ${error.field}`, COLORS.gray));
|
|
1865
|
+
}
|
|
1866
|
+
if (error.location) {
|
|
1867
|
+
console.log(color(` Location: line ${error.location.line}`, COLORS.gray));
|
|
1868
|
+
}
|
|
1869
|
+
console.log(color(` → ${error.suggestion}`, COLORS.yellow));
|
|
1870
|
+
console.log();
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (warnings.length > 0) {
|
|
1874
|
+
console.log(color(`⚠️ Found ${warnings.length} warning(s):
|
|
1875
|
+
`, COLORS.yellow));
|
|
1876
|
+
for (const warning of warnings) {
|
|
1877
|
+
console.log(color(` • ${warning.message}`, COLORS.yellow));
|
|
1878
|
+
if (warning.field) {
|
|
1879
|
+
console.log(color(` Field: ${warning.field}`, COLORS.gray));
|
|
1880
|
+
}
|
|
1881
|
+
console.log(color(` → ${warning.suggestion}`, COLORS.gray));
|
|
1882
|
+
console.log();
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
console.log(color(`Configuration incomplete. Please fix the errors above.
|
|
1886
|
+
`, COLORS.red));
|
|
1887
|
+
process.exit(1);
|
|
1888
|
+
}
|
|
1889
|
+
async function verifyCommand() {
|
|
1890
|
+
const configPath = path3.join(process.cwd(), "specs", "verification.config.ts");
|
|
1891
|
+
console.log(color(`
|
|
1892
|
+
\uD83D\uDD0D Running verification...
|
|
1893
|
+
`, COLORS.blue));
|
|
1894
|
+
const validation = validateConfig(configPath);
|
|
1895
|
+
if (!validation.valid) {
|
|
1896
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
1897
|
+
console.log(color(`❌ Configuration incomplete (${errors.length} error(s))
|
|
1898
|
+
`, COLORS.red));
|
|
1899
|
+
for (const error of errors.slice(0, 3)) {
|
|
1900
|
+
console.log(color(` • ${error.message}`, COLORS.red));
|
|
1901
|
+
if (error.field) {
|
|
1902
|
+
console.log(color(` Field: ${error.field}`, COLORS.gray));
|
|
1903
|
+
}
|
|
1904
|
+
console.log();
|
|
1905
|
+
}
|
|
1906
|
+
if (errors.length > 3) {
|
|
1907
|
+
console.log(color(` ... and ${errors.length - 3} more error(s)`, COLORS.gray));
|
|
1908
|
+
console.log();
|
|
1909
|
+
}
|
|
1910
|
+
console.log(" Run 'bun verify --validate' to see all issues");
|
|
1911
|
+
console.log(" Run 'bun verify --setup' to regenerate configuration");
|
|
1912
|
+
console.log();
|
|
1913
|
+
process.exit(1);
|
|
1914
|
+
}
|
|
1915
|
+
console.log(color("✓ Configuration valid", COLORS.green));
|
|
1916
|
+
console.log();
|
|
1917
|
+
try {
|
|
1918
|
+
await runFullVerification(configPath);
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
console.error(color(`
|
|
1921
|
+
❌ Verification failed:`, COLORS.red));
|
|
1922
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
1923
|
+
process.exit(1);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
async function runFullVerification(configPath) {
|
|
1927
|
+
const { generateTLA: generateTLA2 } = await Promise.resolve().then(() => exports_tla);
|
|
1928
|
+
const { DockerRunner: DockerRunner2 } = await Promise.resolve().then(() => (init_docker(), exports_docker));
|
|
1929
|
+
delete __require.cache[__require.resolve(path3.resolve(configPath))];
|
|
1930
|
+
const configModule = __require(path3.resolve(configPath));
|
|
1931
|
+
const config = configModule.default || configModule;
|
|
1932
|
+
console.log(color("\uD83D\uDCCA Analyzing codebase...", COLORS.blue));
|
|
1933
|
+
const tsConfigPath = findTsConfig();
|
|
1934
|
+
if (!tsConfigPath) {
|
|
1935
|
+
throw new Error("Could not find tsconfig.json");
|
|
1936
|
+
}
|
|
1937
|
+
const analysis = await analyzeCodebase({
|
|
1938
|
+
tsConfigPath,
|
|
1939
|
+
stateFilePath: findStateFile()
|
|
1940
|
+
});
|
|
1941
|
+
console.log(color("✓ Analysis complete", COLORS.green));
|
|
1942
|
+
console.log();
|
|
1943
|
+
console.log(color("\uD83D\uDCDD Generating TLA+ specification...", COLORS.blue));
|
|
1944
|
+
const { spec, cfg } = generateTLA2(config, analysis);
|
|
1945
|
+
const specDir = path3.join(process.cwd(), "specs", "tla", "generated");
|
|
1946
|
+
if (!fs3.existsSync(specDir)) {
|
|
1947
|
+
fs3.mkdirSync(specDir, { recursive: true });
|
|
1948
|
+
}
|
|
1949
|
+
const specPath = path3.join(specDir, "UserApp.tla");
|
|
1950
|
+
const cfgPath = path3.join(specDir, "UserApp.cfg");
|
|
1951
|
+
fs3.writeFileSync(specPath, spec);
|
|
1952
|
+
fs3.writeFileSync(cfgPath, cfg);
|
|
1953
|
+
const baseSpecPath = path3.join(process.cwd(), "specs", "tla", "MessageRouter.tla");
|
|
1954
|
+
if (fs3.existsSync(baseSpecPath)) {
|
|
1955
|
+
const destSpecPath = path3.join(specDir, "MessageRouter.tla");
|
|
1956
|
+
fs3.copyFileSync(baseSpecPath, destSpecPath);
|
|
1957
|
+
} else {
|
|
1958
|
+
console.log(color("⚠️ Warning: MessageRouter.tla not found, verification may fail", COLORS.yellow));
|
|
1959
|
+
}
|
|
1960
|
+
console.log(color("✓ Specification generated", COLORS.green));
|
|
1961
|
+
console.log(color(` ${specPath}`, COLORS.gray));
|
|
1962
|
+
console.log();
|
|
1963
|
+
console.log(color("\uD83D\uDC33 Checking Docker...", COLORS.blue));
|
|
1964
|
+
const docker = new DockerRunner2;
|
|
1965
|
+
if (!await docker.isDockerAvailable()) {
|
|
1966
|
+
throw new Error("Docker is not available. Please install Docker and try again.");
|
|
1967
|
+
}
|
|
1968
|
+
if (!await docker.hasImage()) {
|
|
1969
|
+
console.log(color(" Pulling TLA+ image (this may take a moment)...", COLORS.gray));
|
|
1970
|
+
await docker.pullImage((line) => {
|
|
1971
|
+
console.log(color(` ${line}`, COLORS.gray));
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
console.log(color("✓ Docker ready", COLORS.green));
|
|
1975
|
+
console.log();
|
|
1976
|
+
console.log(color("⚙️ Running TLC model checker...", COLORS.blue));
|
|
1977
|
+
console.log(color(" This may take a minute...", COLORS.gray));
|
|
1978
|
+
console.log();
|
|
1979
|
+
const result = await docker.runTLC(specPath, {
|
|
1980
|
+
workers: 2,
|
|
1981
|
+
timeout: 120000
|
|
1982
|
+
});
|
|
1983
|
+
if (result.success) {
|
|
1984
|
+
console.log(color(`✅ Verification passed!
|
|
1985
|
+
`, COLORS.green));
|
|
1986
|
+
console.log(color("Statistics:", COLORS.blue));
|
|
1987
|
+
console.log(color(` States explored: ${result.stats?.statesGenerated || 0}`, COLORS.gray));
|
|
1988
|
+
console.log(color(` Distinct states: ${result.stats?.distinctStates || 0}`, COLORS.gray));
|
|
1989
|
+
console.log();
|
|
1990
|
+
} else {
|
|
1991
|
+
console.log(color(`❌ Verification failed!
|
|
1992
|
+
`, COLORS.red));
|
|
1993
|
+
if (result.violation) {
|
|
1994
|
+
console.log(color(`Invariant violated: ${result.violation.name}
|
|
1995
|
+
`, COLORS.red));
|
|
1996
|
+
console.log(color("Trace to violation:", COLORS.yellow));
|
|
1997
|
+
for (const line of result.violation.trace.slice(0, 20)) {
|
|
1998
|
+
console.log(color(` ${line}`, COLORS.gray));
|
|
1999
|
+
}
|
|
2000
|
+
if (result.violation.trace.length > 20) {
|
|
2001
|
+
console.log(color(` ... (${result.violation.trace.length - 20} more lines)`, COLORS.gray));
|
|
2002
|
+
}
|
|
2003
|
+
} else if (result.error) {
|
|
2004
|
+
console.log(color(`Error: ${result.error}`, COLORS.red));
|
|
2005
|
+
}
|
|
2006
|
+
console.log();
|
|
2007
|
+
console.log(color("Full output saved to:", COLORS.gray));
|
|
2008
|
+
console.log(color(` ${path3.join(specDir, "tlc-output.log")}`, COLORS.gray));
|
|
2009
|
+
fs3.writeFileSync(path3.join(specDir, "tlc-output.log"), result.output);
|
|
2010
|
+
process.exit(1);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function showHelp() {
|
|
2014
|
+
console.log(`
|
|
2015
|
+
${color("bun verify", COLORS.blue)} - Formal verification for web extensions
|
|
2016
|
+
|
|
2017
|
+
${color("Commands:", COLORS.blue)}
|
|
2018
|
+
|
|
2019
|
+
${color("bun verify", COLORS.green)}
|
|
2020
|
+
Run verification (validates config, generates specs, runs TLC)
|
|
2021
|
+
|
|
2022
|
+
${color("bun verify --setup", COLORS.green)}
|
|
2023
|
+
Analyze codebase and generate configuration file
|
|
2024
|
+
|
|
2025
|
+
${color("bun verify --validate", COLORS.green)}
|
|
2026
|
+
Validate existing configuration without running verification
|
|
2027
|
+
|
|
2028
|
+
${color("bun verify --help", COLORS.green)}
|
|
2029
|
+
Show this help message
|
|
2030
|
+
|
|
2031
|
+
${color("Getting Started:", COLORS.blue)}
|
|
2032
|
+
|
|
2033
|
+
1. Run ${color("bun verify --setup", COLORS.green)} to generate configuration
|
|
2034
|
+
2. Review ${color("specs/verification.config.ts", COLORS.blue)} and fill in marked fields
|
|
2035
|
+
3. Run ${color("bun verify --validate", COLORS.green)} to check your configuration
|
|
2036
|
+
4. Run ${color("bun verify", COLORS.green)} to start verification
|
|
2037
|
+
|
|
2038
|
+
${color("Configuration Help:", COLORS.blue)}
|
|
2039
|
+
|
|
2040
|
+
The generated config file uses special markers:
|
|
2041
|
+
|
|
2042
|
+
${color("/* CONFIGURE */", COLORS.yellow)} - Replace with your value
|
|
2043
|
+
${color("/* REVIEW */", COLORS.yellow)} - Check auto-generated value
|
|
2044
|
+
${color("null", COLORS.yellow)} - Must be replaced with concrete value
|
|
2045
|
+
|
|
2046
|
+
${color("Learn More:", COLORS.blue)}
|
|
2047
|
+
|
|
2048
|
+
Documentation: https://github.com/fairfox/web-ext
|
|
2049
|
+
TLA+ Resources: https://learntla.com
|
|
2050
|
+
`);
|
|
2051
|
+
}
|
|
2052
|
+
function findTsConfig() {
|
|
2053
|
+
const locations = [
|
|
2054
|
+
path3.join(process.cwd(), "tsconfig.json"),
|
|
2055
|
+
path3.join(process.cwd(), "packages", "web-ext", "tsconfig.json")
|
|
2056
|
+
];
|
|
2057
|
+
for (const loc of locations) {
|
|
2058
|
+
if (fs3.existsSync(loc)) {
|
|
2059
|
+
return loc;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
function findStateFile() {
|
|
2065
|
+
const locations = [
|
|
2066
|
+
path3.join(process.cwd(), "types", "state.ts"),
|
|
2067
|
+
path3.join(process.cwd(), "src", "types", "state.ts"),
|
|
2068
|
+
path3.join(process.cwd(), "packages", "web-ext", "src", "shared", "state", "app-state.ts")
|
|
2069
|
+
];
|
|
2070
|
+
for (const loc of locations) {
|
|
2071
|
+
if (fs3.existsSync(loc)) {
|
|
2072
|
+
return loc;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
main().catch((error) => {
|
|
2078
|
+
console.error(color(`
|
|
2079
|
+
❌ Fatal error:`, COLORS.red));
|
|
2080
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
2081
|
+
if (error instanceof Error && error.stack) {
|
|
2082
|
+
console.error(color(`
|
|
2083
|
+
Stack trace:`, COLORS.gray));
|
|
2084
|
+
console.error(color(error.stack, COLORS.gray));
|
|
2085
|
+
}
|
|
2086
|
+
process.exit(1);
|
|
2087
|
+
});
|
|
2088
|
+
|
|
2089
|
+
//# debugId=10F00303D4610C9D64756E2164756E21
|