@fairfox/polly 0.40.0 → 0.47.0

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