@fairfox/polly 0.1.1 → 0.1.2

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.
Files changed (39) hide show
  1. package/cli/polly.ts +9 -3
  2. package/package.json +2 -2
  3. package/vendor/analysis/src/extract/adr.ts +212 -0
  4. package/vendor/analysis/src/extract/architecture.ts +160 -0
  5. package/vendor/analysis/src/extract/contexts.ts +298 -0
  6. package/vendor/analysis/src/extract/flows.ts +309 -0
  7. package/vendor/analysis/src/extract/handlers.ts +321 -0
  8. package/vendor/analysis/src/extract/index.ts +9 -0
  9. package/vendor/analysis/src/extract/integrations.ts +329 -0
  10. package/vendor/analysis/src/extract/manifest.ts +298 -0
  11. package/vendor/analysis/src/extract/types.ts +389 -0
  12. package/vendor/analysis/src/index.ts +7 -0
  13. package/vendor/analysis/src/types/adr.ts +53 -0
  14. package/vendor/analysis/src/types/architecture.ts +245 -0
  15. package/vendor/analysis/src/types/core.ts +210 -0
  16. package/vendor/analysis/src/types/index.ts +18 -0
  17. package/vendor/verify/src/adapters/base.ts +164 -0
  18. package/vendor/verify/src/adapters/detection.ts +281 -0
  19. package/vendor/verify/src/adapters/event-bus/index.ts +480 -0
  20. package/vendor/verify/src/adapters/web-extension/index.ts +508 -0
  21. package/vendor/verify/src/adapters/websocket/index.ts +486 -0
  22. package/vendor/verify/src/cli.ts +430 -0
  23. package/vendor/verify/src/codegen/config.ts +354 -0
  24. package/vendor/verify/src/codegen/tla.ts +719 -0
  25. package/vendor/verify/src/config/parser.ts +303 -0
  26. package/vendor/verify/src/config/types.ts +113 -0
  27. package/vendor/verify/src/core/model.ts +267 -0
  28. package/vendor/verify/src/core/primitives.ts +106 -0
  29. package/vendor/verify/src/extract/handlers.ts +2 -0
  30. package/vendor/verify/src/extract/types.ts +2 -0
  31. package/vendor/verify/src/index.ts +150 -0
  32. package/vendor/verify/src/primitives/index.ts +102 -0
  33. package/vendor/verify/src/runner/docker.ts +283 -0
  34. package/vendor/verify/src/types.ts +51 -0
  35. package/vendor/visualize/src/cli.ts +365 -0
  36. package/vendor/visualize/src/codegen/structurizr.ts +770 -0
  37. package/vendor/visualize/src/index.ts +13 -0
  38. package/vendor/visualize/src/runner/export.ts +235 -0
  39. package/vendor/visualize/src/viewer/server.ts +485 -0
@@ -0,0 +1,719 @@
1
+ // TLA+ specification generator
2
+
3
+ import type { VerificationConfig, CodebaseAnalysis } from "../types";
4
+ import type { MessageHandler } from "../core/model";
5
+
6
+ export class TLAGenerator {
7
+ private lines: string[] = [];
8
+ private indent = 0;
9
+
10
+ generate(
11
+ config: VerificationConfig,
12
+ analysis: CodebaseAnalysis
13
+ ): {
14
+ spec: string;
15
+ cfg: string;
16
+ } {
17
+ this.lines = [];
18
+ this.indent = 0;
19
+
20
+ const spec = this.generateSpec(config, analysis);
21
+ const cfg = this.generateConfig(config, analysis);
22
+
23
+ return { spec, cfg };
24
+ }
25
+
26
+ private generateSpec(config: VerificationConfig, analysis: CodebaseAnalysis): string {
27
+ this.lines = [];
28
+ this.indent = 0;
29
+
30
+ this.addHeader();
31
+ this.addExtends();
32
+ this.addConstants(config, analysis);
33
+ this.addMessageTypes(config, analysis);
34
+ this.addStateType(config, analysis);
35
+ this.addVariables();
36
+ this.addInit(config, analysis);
37
+ this.addActions(config, analysis);
38
+ this.addRouteWithHandlers(config, analysis);
39
+ this.addNext(config, analysis);
40
+ this.addSpec();
41
+ this.addInvariants(config, analysis);
42
+
43
+ return this.lines.join("\n");
44
+ }
45
+
46
+ private generateConfig(config: VerificationConfig, analysis: CodebaseAnalysis): string {
47
+ const lines: string[] = [];
48
+
49
+ lines.push("SPECIFICATION UserSpec");
50
+ lines.push("");
51
+ lines.push("\\* Constants");
52
+ lines.push("CONSTANTS");
53
+
54
+ // Generate context set (reduced for faster verification)
55
+ lines.push(" Contexts = {background, content, popup}");
56
+
57
+ // Message bounds (defaults chosen for reasonable verification time)
58
+ lines.push(` MaxMessages = ${config.messages.maxInFlight || 3}`);
59
+ lines.push(` MaxTabId = ${config.messages.maxTabs || 1}`);
60
+ lines.push(" TimeoutLimit = 3");
61
+
62
+ // State bounds from config
63
+ for (const [field, fieldConfig] of Object.entries(config.state)) {
64
+ if (typeof fieldConfig === "object" && fieldConfig !== null) {
65
+ if ("maxLength" in fieldConfig && fieldConfig.maxLength !== null) {
66
+ const constName = this.fieldToConstName(field);
67
+ lines.push(` ${constName}_MaxLength = ${fieldConfig.maxLength}`);
68
+ }
69
+ if ("max" in fieldConfig && fieldConfig.max !== null) {
70
+ const constName = this.fieldToConstName(field);
71
+ lines.push(` ${constName}_Max = ${fieldConfig.max}`);
72
+ }
73
+ if ("maxSize" in fieldConfig && fieldConfig.maxSize !== null) {
74
+ const constName = this.fieldToConstName(field);
75
+ lines.push(` ${constName}_MaxSize = ${fieldConfig.maxSize}`);
76
+ }
77
+ }
78
+ }
79
+
80
+ lines.push("");
81
+ lines.push("\\* Invariants to check");
82
+ lines.push("INVARIANTS");
83
+ lines.push(" TypeOK");
84
+ lines.push(" NoRoutingLoops");
85
+ lines.push(" UserStateTypeInvariant");
86
+ lines.push("");
87
+ lines.push("\\* State constraint");
88
+ lines.push("CONSTRAINT");
89
+ lines.push(" StateConstraint");
90
+
91
+ return lines.join("\n");
92
+ }
93
+
94
+ private addHeader(): void {
95
+ this.line("------------------------- MODULE UserApp -------------------------");
96
+ this.line("(*");
97
+ this.line(" Auto-generated TLA+ specification for web extension");
98
+ this.line(" ");
99
+ this.line(" Generated from:");
100
+ this.line(" - TypeScript type definitions");
101
+ this.line(" - Verification configuration");
102
+ this.line(" ");
103
+ this.line(" This spec extends MessageRouter with:");
104
+ this.line(" - Application-specific state types");
105
+ this.line(" - Message type definitions");
106
+ this.line(" - State transition actions");
107
+ this.line("*)");
108
+ this.line("");
109
+ }
110
+
111
+ private addExtends(): void {
112
+ this.line("EXTENDS MessageRouter");
113
+ this.line("");
114
+ }
115
+
116
+ private addConstants(config: VerificationConfig, analysis: CodebaseAnalysis): void {
117
+ // MessageRouter already defines: Contexts, MaxMessages, MaxTabId, TimeoutLimit
118
+ // We only add application-specific constants
119
+
120
+ const hasCustomConstants = Object.entries(config.state).some(([field, fieldConfig]) => {
121
+ if (typeof fieldConfig !== "object" || fieldConfig === null) return false;
122
+ return (
123
+ ("maxLength" in fieldConfig && fieldConfig.maxLength !== null) ||
124
+ ("max" in fieldConfig && fieldConfig.max !== null) ||
125
+ ("maxSize" in fieldConfig && fieldConfig.maxSize !== null)
126
+ );
127
+ });
128
+
129
+ if (!hasCustomConstants) {
130
+ // No custom constants needed
131
+ return;
132
+ }
133
+
134
+ this.line("\\* Application-specific constants");
135
+ this.line("CONSTANTS");
136
+ this.indent++;
137
+
138
+ let first = true;
139
+ for (const [field, fieldConfig] of Object.entries(config.state)) {
140
+ if (typeof fieldConfig === "object" && fieldConfig !== null) {
141
+ const constName = this.fieldToConstName(field);
142
+
143
+ if ("maxLength" in fieldConfig && fieldConfig.maxLength !== null) {
144
+ this.line(`${first ? "" : ","}${constName}_MaxLength \\* Max length for ${field}`);
145
+ first = false;
146
+ }
147
+ if ("max" in fieldConfig && fieldConfig.max !== null) {
148
+ this.line(`${first ? "" : ","}${constName}_Max \\* Max value for ${field}`);
149
+ first = false;
150
+ }
151
+ if ("maxSize" in fieldConfig && fieldConfig.maxSize !== null) {
152
+ this.line(`${first ? "" : ","}${constName}_MaxSize \\* Max size for ${field}`);
153
+ first = false;
154
+ }
155
+ }
156
+ }
157
+
158
+ this.indent--;
159
+ this.line("");
160
+ }
161
+
162
+ private addStateType(config: VerificationConfig, analysis: CodebaseAnalysis): void {
163
+ // Define Value type for generic sequences and maps
164
+ // Use a finite set for model checking
165
+ this.line("\\* Generic value type for sequences and maps");
166
+ this.line("\\* Bounded to allow model checking");
167
+ this.line('Value == {"v1", "v2", "v3"}');
168
+ this.line("");
169
+
170
+ // Define Keys type for map domains
171
+ this.line("\\* Generic key type for maps");
172
+ this.line("\\* Bounded to allow model checking");
173
+ this.line('Keys == {"k1", "k2", "k3"}');
174
+ this.line("");
175
+
176
+ this.line("\\* Application state type definition");
177
+ this.line("State == [");
178
+ this.indent++;
179
+
180
+ const stateFields: string[] = [];
181
+
182
+ for (const [fieldPath, fieldConfig] of Object.entries(config.state)) {
183
+ if (typeof fieldConfig !== "object" || fieldConfig === null) continue;
184
+
185
+ const fieldName = this.sanitizeFieldName(fieldPath);
186
+ const tlaType = this.fieldConfigToTLAType(fieldPath, fieldConfig, config);
187
+
188
+ stateFields.push(`${fieldName}: ${tlaType}`);
189
+ }
190
+
191
+ for (let i = 0; i < stateFields.length; i++) {
192
+ const field = stateFields[i];
193
+ const suffix = i < stateFields.length - 1 ? "," : "";
194
+ this.line(`${field}${suffix}`);
195
+ }
196
+
197
+ this.indent--;
198
+ this.line("]");
199
+ this.line("");
200
+ }
201
+
202
+ private addMessageTypes(config: VerificationConfig, analysis: CodebaseAnalysis): void {
203
+ if (analysis.messageTypes.length === 0) {
204
+ // No message types found, skip
205
+ return;
206
+ }
207
+
208
+ this.line("\\* Message types from application");
209
+ const messageTypeSet = analysis.messageTypes.map((t) => `"${t}"`).join(", ");
210
+ this.line(`UserMessageTypes == {${messageTypeSet}}`);
211
+ this.line("");
212
+ }
213
+
214
+ private addVariables(): void {
215
+ // MessageRouter already defines: ports, messages, pendingRequests, delivered, routingDepth, time
216
+ // We add: contextStates for application state
217
+
218
+ this.line("\\* Application state per context");
219
+ this.line("VARIABLE contextStates");
220
+ this.line("");
221
+ this.line("\\* All variables (extending MessageRouter vars)");
222
+ this.line(
223
+ "allVars == <<ports, messages, pendingRequests, delivered, routingDepth, time, contextStates>>"
224
+ );
225
+ this.line("");
226
+ }
227
+
228
+ private addInit(config: VerificationConfig, analysis: CodebaseAnalysis): void {
229
+ // Generate InitialState first
230
+ this.line("\\* Initial application state");
231
+ this.line("InitialState == [");
232
+ this.indent++;
233
+
234
+ const fields: string[] = [];
235
+ for (const [fieldPath, fieldConfig] of Object.entries(config.state)) {
236
+ if (typeof fieldConfig !== "object" || fieldConfig === null) continue;
237
+
238
+ const fieldName = this.sanitizeFieldName(fieldPath);
239
+ const initialValue = this.getInitialValue(fieldConfig);
240
+ fields.push(`${fieldName} |-> ${initialValue}`);
241
+ }
242
+
243
+ for (let i = 0; i < fields.length; i++) {
244
+ const field = fields[i];
245
+ const suffix = i < fields.length - 1 ? "," : "";
246
+ this.line(`${field}${suffix}`);
247
+ }
248
+
249
+ this.indent--;
250
+ this.line("]");
251
+ this.line("");
252
+
253
+ // Init extends MessageRouter's Init
254
+ this.line("\\* Initial state (extends MessageRouter)");
255
+ this.line("UserInit ==");
256
+ this.indent++;
257
+ this.line("/\\ Init \\* MessageRouter's init");
258
+ this.line("/\\ contextStates = [c \\in Contexts |-> InitialState]");
259
+ this.indent--;
260
+ this.line("");
261
+ }
262
+
263
+ private addActions(config: VerificationConfig, analysis: CodebaseAnalysis): void {
264
+ this.line("\\* =============================================================================");
265
+ this.line("\\* Application-specific actions");
266
+ this.line("\\* =============================================================================");
267
+ this.line("");
268
+
269
+ if (analysis.handlers.length === 0) {
270
+ // No handlers found, keep the stub
271
+ this.line("\\* No message handlers found in codebase");
272
+ this.line("\\* State remains unchanged for all messages");
273
+ this.line("StateTransition(ctx, msgType) ==");
274
+ this.indent++;
275
+ this.line("UNCHANGED contextStates");
276
+ this.indent--;
277
+ this.line("");
278
+ return;
279
+ }
280
+
281
+ // Generate state transition actions for each handler
282
+ this.line("\\* State transitions extracted from message handlers");
283
+ this.line("");
284
+
285
+ // Group handlers by message type
286
+ const handlersByType = new Map<string, typeof analysis.handlers>();
287
+ for (const handler of analysis.handlers) {
288
+ if (!handlersByType.has(handler.messageType)) {
289
+ handlersByType.set(handler.messageType, []);
290
+ }
291
+ handlersByType.get(handler.messageType)!.push(handler);
292
+ }
293
+
294
+ // Generate an action for each message type
295
+ for (const [messageType, handlers] of handlersByType.entries()) {
296
+ this.generateHandlerAction(messageType, handlers, config);
297
+ }
298
+
299
+ // Generate the main StateTransition action that dispatches to specific handlers
300
+ this.line("\\* Main state transition action");
301
+ this.line("StateTransition(ctx, msgType) ==");
302
+ this.indent++;
303
+
304
+ const messageTypes = Array.from(handlersByType.keys());
305
+ for (let i = 0; i < messageTypes.length; i++) {
306
+ const msgType = messageTypes[i];
307
+ const actionName = this.messageTypeToActionName(msgType);
308
+
309
+ if (i === 0) {
310
+ this.line(`IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
311
+ } else if (i === messageTypes.length - 1) {
312
+ this.line(`ELSE IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
313
+ this.line("ELSE UNCHANGED contextStates \\* Unknown message type");
314
+ } else {
315
+ this.line(`ELSE IF msgType = "${msgType}" THEN ${actionName}(ctx)`);
316
+ }
317
+ }
318
+
319
+ this.indent--;
320
+ this.line("");
321
+ }
322
+
323
+ private generateHandlerAction(
324
+ messageType: string,
325
+ handlers: MessageHandler[],
326
+ config: VerificationConfig
327
+ ): void {
328
+ const actionName = this.messageTypeToActionName(messageType);
329
+
330
+ this.line(`\\* Handler for ${messageType}`);
331
+ this.line(`${actionName}(ctx) ==`);
332
+ this.indent++;
333
+
334
+ // Collect all preconditions from all handlers
335
+ const allPreconditions = handlers.flatMap((h) => h.preconditions);
336
+
337
+ // Collect all assignments from all handlers for this message type
338
+ const allAssignments = handlers.flatMap((h) => h.assignments);
339
+
340
+ // Collect all postconditions from all handlers
341
+ const allPostconditions = handlers.flatMap((h) => h.postconditions);
342
+
343
+ // Emit preconditions first
344
+ if (allPreconditions.length > 0) {
345
+ for (const precondition of allPreconditions) {
346
+ const tlaExpr = this.tsExpressionToTLA(precondition.expression);
347
+ const comment = precondition.message ? ` \\* ${precondition.message}` : "";
348
+ this.line(`/\\ ${tlaExpr}${comment}`);
349
+ }
350
+ }
351
+
352
+ // Filter out null assignments and map them to valid values
353
+ const validAssignments = allAssignments
354
+ .filter((a) => {
355
+ if (a.value === null) {
356
+ // For null values, check if we can map to a valid value based on field config
357
+ const fieldConfig = config.state[a.field];
358
+ if (
359
+ fieldConfig &&
360
+ typeof fieldConfig === "object" &&
361
+ "values" in fieldConfig &&
362
+ fieldConfig.values
363
+ ) {
364
+ // Use the last value as the "null" value (often "guest", "none", etc.)
365
+ return true;
366
+ }
367
+ // Skip null assignments if we can't map them
368
+ return false;
369
+ }
370
+ return true;
371
+ })
372
+ .map((a) => {
373
+ if (a.value === null) {
374
+ const fieldConfig = config.state[a.field];
375
+ if (
376
+ fieldConfig &&
377
+ typeof fieldConfig === "object" &&
378
+ "values" in fieldConfig &&
379
+ fieldConfig.values
380
+ ) {
381
+ // Use the last value as the "null" value
382
+ const nullValue = fieldConfig.values[fieldConfig.values.length - 1];
383
+ return { ...a, value: nullValue };
384
+ }
385
+ }
386
+ return a;
387
+ });
388
+
389
+ if (validAssignments.length === 0) {
390
+ // Handler exists but makes no state changes
391
+ if (allPreconditions.length === 0) {
392
+ this.line("\\* No state changes in handler");
393
+ }
394
+ this.line("/\\ UNCHANGED contextStates");
395
+ } else {
396
+ // Generate state updates
397
+ this.line("/\\ contextStates' = [contextStates EXCEPT");
398
+ this.indent++;
399
+
400
+ for (let i = 0; i < validAssignments.length; i++) {
401
+ const assignment = validAssignments[i];
402
+ const fieldName = this.sanitizeFieldName(assignment.field);
403
+ const value = this.assignmentValueToTLA(assignment.value);
404
+ const suffix = i < validAssignments.length - 1 ? "," : "";
405
+
406
+ this.line(`![ctx].${fieldName} = ${value}${suffix}`);
407
+ }
408
+
409
+ this.indent--;
410
+ this.line("]");
411
+ }
412
+
413
+ // Emit postconditions last
414
+ if (allPostconditions.length > 0) {
415
+ for (const postcondition of allPostconditions) {
416
+ const tlaExpr = this.tsExpressionToTLA(postcondition.expression, true);
417
+ const comment = postcondition.message ? ` \\* ${postcondition.message}` : "";
418
+ this.line(`/\\ ${tlaExpr}${comment}`);
419
+ }
420
+ }
421
+
422
+ this.indent--;
423
+ this.line("");
424
+ }
425
+
426
+ /**
427
+ * Translate TypeScript expression to TLA+ syntax
428
+ * @param expr - TypeScript expression from requires() or ensures()
429
+ * @param isPrimed - If true, use contextStates' (for postconditions)
430
+ */
431
+ private tsExpressionToTLA(expr: string, isPrimed: boolean = false): string {
432
+ let tla = expr;
433
+
434
+ // Replace state references with contextStates[ctx] or contextStates'[ctx]
435
+ const statePrefix = isPrimed ? "contextStates'[ctx]" : "contextStates[ctx]";
436
+
437
+ // Replace state.field.subfield with contextStates[ctx].field_subfield
438
+ tla = tla.replace(/state\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (match, path) => {
439
+ return `${statePrefix}.${this.sanitizeFieldName(path)}`;
440
+ });
441
+
442
+ // Replace payload.field with payload.field (no change needed, but sanitize)
443
+ tla = tla.replace(/payload\.([a-zA-Z_][a-zA-Z0-9_.]*)/g, (match, path) => {
444
+ return `payload.${this.sanitizeFieldName(path)}`;
445
+ });
446
+
447
+ // Replace comparison operators
448
+ tla = tla.replace(/===/g, "=");
449
+ tla = tla.replace(/!==/g, "#");
450
+ tla = tla.replace(/!=/g, "#");
451
+ tla = tla.replace(/==/g, "=");
452
+
453
+ // Replace logical operators
454
+ tla = tla.replace(/&&/g, "/\\");
455
+ tla = tla.replace(/\|\|/g, "\\/");
456
+
457
+ // Replace negation operator (careful with !== already handled)
458
+ // Match ! that's not part of !== or !=
459
+ tla = tla.replace(/!([^=])/g, "~$1");
460
+ tla = tla.replace(/!$/g, "~"); // Handle ! at end of string
461
+
462
+ // Replace boolean literals
463
+ tla = tla.replace(/\btrue\b/g, "TRUE");
464
+ tla = tla.replace(/\bfalse\b/g, "FALSE");
465
+
466
+ // Replace null
467
+ tla = tla.replace(/\bnull\b/g, "NULL");
468
+
469
+ // Replace less than / greater than comparisons
470
+ tla = tla.replace(/</g, "<");
471
+ tla = tla.replace(/>/g, ">");
472
+ tla = tla.replace(/<=/g, "<=");
473
+ tla = tla.replace(/>=/g, ">=");
474
+
475
+ return tla;
476
+ }
477
+
478
+ private messageTypeToActionName(messageType: string): string {
479
+ // Convert USER_LOGIN -> HandleUserLogin
480
+ return (
481
+ "Handle" +
482
+ messageType
483
+ .split("_")
484
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
485
+ .join("")
486
+ );
487
+ }
488
+
489
+ private assignmentValueToTLA(value: string | boolean | number | null): string {
490
+ if (typeof value === "boolean") {
491
+ return value ? "TRUE" : "FALSE";
492
+ }
493
+ if (typeof value === "number") {
494
+ return String(value);
495
+ }
496
+ if (value === null) {
497
+ return "NULL"; // Will need to handle this based on type
498
+ }
499
+ if (typeof value === "string") {
500
+ return `"${value}"`;
501
+ }
502
+ return "NULL";
503
+ }
504
+
505
+ private addRouteWithHandlers(config: VerificationConfig, analysis: CodebaseAnalysis): void {
506
+ this.line("\\* =============================================================================");
507
+ this.line("\\* Message Routing with State Transitions");
508
+ this.line("\\* =============================================================================");
509
+ this.line("");
510
+
511
+ if (analysis.handlers.length === 0) {
512
+ // No handlers, just use base routing
513
+ return;
514
+ }
515
+
516
+ this.line("\\* Route a message and invoke its handler");
517
+ this.line("UserRouteMessage(msgIndex) ==");
518
+ this.indent++;
519
+ this.line("/\\ msgIndex \\in 1..Len(messages)");
520
+ this.line("/\\ LET msg == messages[msgIndex]");
521
+ this.line(' IN /\\ msg.status = "pending"');
522
+ this.line(" /\\ routingDepth' = routingDepth + 1");
523
+ this.line(" /\\ routingDepth < 5");
524
+ this.line(" /\\ \\E target \\in msg.targets :");
525
+ this.line(' /\\ IF target \\in Contexts /\\ ports[target] = "connected"');
526
+ this.line(" THEN \\* Successful delivery - route AND invoke handler");
527
+ this.line(
528
+ ' /\\ messages\' = [messages EXCEPT ![msgIndex].status = "delivered"]'
529
+ );
530
+ this.line(" /\\ delivered' = delivered \\union {msg.id}");
531
+ this.line(
532
+ " /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->"
533
+ );
534
+ this.line(" pendingRequests[id]]");
535
+ this.line(" /\\ time' = time + 1");
536
+ this.line(" /\\ StateTransition(target, msg.msgType)");
537
+ this.line(" ELSE \\* Port not connected - message fails");
538
+ this.line(
539
+ ' /\\ messages\' = [messages EXCEPT ![msgIndex].status = "failed"]'
540
+ );
541
+ this.line(
542
+ " /\\ pendingRequests' = [id \\in DOMAIN pendingRequests \\ {msg.id} |->"
543
+ );
544
+ this.line(" pendingRequests[id]]");
545
+ this.line(" /\\ time' = time + 1");
546
+ this.line(" /\\ UNCHANGED <<delivered, contextStates>>");
547
+ this.line(" /\\ UNCHANGED ports");
548
+ this.indent--;
549
+ this.line("");
550
+ }
551
+
552
+ private addNext(config: VerificationConfig, analysis: CodebaseAnalysis): void {
553
+ this.line("\\* Next state relation (extends MessageRouter)");
554
+ this.line("UserNext ==");
555
+ this.indent++;
556
+
557
+ if (analysis.handlers.length > 0) {
558
+ // Use integrated routing + handlers
559
+ this.line("\\/ \\E c \\in Contexts : ConnectPort(c) /\\ UNCHANGED contextStates");
560
+ this.line("\\/ \\E c \\in Contexts : DisconnectPort(c) /\\ UNCHANGED contextStates");
561
+ this.line(
562
+ "\\/ \\E src \\in Contexts : \\E targetSet \\in (SUBSET Contexts \\ {{}}) : \\E tab \\in 0..MaxTabId : \\E msgType \\in UserMessageTypes :"
563
+ );
564
+ this.indent++;
565
+ this.line("SendMessage(src, targetSet, tab, msgType) /\\ UNCHANGED contextStates");
566
+ this.indent--;
567
+ this.line("\\/ \\E i \\in 1..Len(messages) : UserRouteMessage(i)");
568
+ this.line("\\/ CompleteRouting /\\ UNCHANGED contextStates");
569
+ this.line("\\/ \\E i \\in 1..Len(messages) : TimeoutMessage(i) /\\ UNCHANGED contextStates");
570
+ } else {
571
+ // No handlers, all actions preserve contextStates
572
+ this.line("\\/ Next /\\ UNCHANGED contextStates");
573
+ }
574
+
575
+ this.indent--;
576
+ this.line("");
577
+ }
578
+
579
+ private addSpec(): void {
580
+ this.line("\\* Specification");
581
+ this.line("UserSpec == UserInit /\\ [][UserNext]_allVars /\\ WF_allVars(UserNext)");
582
+ this.line("");
583
+ }
584
+
585
+ private addInvariants(config: VerificationConfig, analysis: CodebaseAnalysis): void {
586
+ this.line("\\* =============================================================================");
587
+ this.line("\\* Application Invariants");
588
+ this.line("\\* =============================================================================");
589
+ this.line("");
590
+
591
+ this.line("\\* TypeOK and NoRoutingLoops are inherited from MessageRouter");
592
+ this.line("");
593
+
594
+ this.line("\\* Application state type invariant");
595
+ this.line("UserStateTypeInvariant ==");
596
+ this.indent++;
597
+ this.line("\\A ctx \\in Contexts :");
598
+ this.indent++;
599
+ this.line("contextStates[ctx] \\in State");
600
+ this.indent--;
601
+ this.indent--;
602
+ this.line("");
603
+
604
+ this.line("\\* State constraint to bound state space");
605
+ this.line("StateConstraint ==");
606
+ this.indent++;
607
+ this.line("Len(messages) <= MaxMessages");
608
+ this.indent--;
609
+ this.line("");
610
+
611
+ this.line("=============================================================================");
612
+ }
613
+
614
+ private fieldConfigToTLAType(
615
+ fieldPath: string,
616
+ fieldConfig: any,
617
+ config: VerificationConfig
618
+ ): string {
619
+ if ("type" in fieldConfig) {
620
+ if (fieldConfig.type === "boolean") {
621
+ return "BOOLEAN";
622
+ }
623
+ if (fieldConfig.type === "enum" && fieldConfig.values) {
624
+ const values = fieldConfig.values.map((v: string) => `"${v}"`).join(", ");
625
+ return `{${values}}`;
626
+ }
627
+ }
628
+
629
+ if ("maxLength" in fieldConfig) {
630
+ // Array type - represented as sequence with bounded length
631
+ const constName = this.fieldToConstName(fieldPath);
632
+ return `Seq(Value)`; // Simplified - would need element type
633
+ }
634
+
635
+ if ("min" in fieldConfig && "max" in fieldConfig) {
636
+ // Number type
637
+ const constName = this.fieldToConstName(fieldPath);
638
+ const min = fieldConfig.min || 0;
639
+ const max = fieldConfig.max || 100;
640
+ return `${min}..${max}`;
641
+ }
642
+
643
+ if ("values" in fieldConfig) {
644
+ if (fieldConfig.values && Array.isArray(fieldConfig.values)) {
645
+ // String with concrete values
646
+ const values = fieldConfig.values.map((v: string) => `"${v}"`).join(", ");
647
+ return `{${values}}`;
648
+ }
649
+ if (fieldConfig.abstract) {
650
+ // Abstract string
651
+ return "STRING";
652
+ }
653
+ // Needs configuration
654
+ return "STRING";
655
+ }
656
+
657
+ if ("maxSize" in fieldConfig) {
658
+ // Map with bounded key set
659
+ return "[Keys -> Value]";
660
+ }
661
+
662
+ return "Value"; // Generic fallback
663
+ }
664
+
665
+ private getInitialValue(fieldConfig: any): string {
666
+ if ("type" in fieldConfig) {
667
+ if (fieldConfig.type === "boolean") {
668
+ return "FALSE";
669
+ }
670
+ if (fieldConfig.type === "enum" && fieldConfig.values && fieldConfig.values.length > 0) {
671
+ return `"${fieldConfig.values[0]}"`;
672
+ }
673
+ }
674
+
675
+ if ("maxLength" in fieldConfig) {
676
+ return "<<>>"; // Empty sequence
677
+ }
678
+
679
+ if ("min" in fieldConfig) {
680
+ return String(fieldConfig.min || 0);
681
+ }
682
+
683
+ if ("values" in fieldConfig && fieldConfig.values && fieldConfig.values.length > 0) {
684
+ return `"${fieldConfig.values[0]}"`;
685
+ }
686
+
687
+ if ("maxSize" in fieldConfig) {
688
+ // Map with all keys mapped to default value
689
+ return '[k \\in Keys |-> "v1"]';
690
+ }
691
+
692
+ return "0"; // Default fallback
693
+ }
694
+
695
+ private fieldToConstName(fieldPath: string): string {
696
+ return fieldPath.replace(/\./g, "_").toUpperCase();
697
+ }
698
+
699
+ private sanitizeFieldName(fieldPath: string): string {
700
+ return fieldPath.replace(/\./g, "_");
701
+ }
702
+
703
+ private line(content: string): void {
704
+ if (content === "") {
705
+ this.lines.push("");
706
+ } else {
707
+ const indentation = " ".repeat(this.indent);
708
+ this.lines.push(indentation + content);
709
+ }
710
+ }
711
+ }
712
+
713
+ export function generateTLA(
714
+ config: VerificationConfig,
715
+ analysis: CodebaseAnalysis
716
+ ): { spec: string; cfg: string } {
717
+ const generator = new TLAGenerator();
718
+ return generator.generate(config, analysis);
719
+ }