@fairfox/polly 0.84.0 → 0.85.1
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/dist/tools/verify/src/cli.js +415 -92
- package/dist/tools/verify/src/cli.js.map +8 -8
- package/dist/tools/verify/src/config.d.ts +7 -0
- package/dist/tools/verify/src/config.js.map +2 -2
- package/dist/tools/visualize/src/cli.js +164 -1
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +1 -1
|
@@ -258,12 +258,35 @@ __export(exports_model_coverage, {
|
|
|
258
258
|
function norm(field) {
|
|
259
259
|
return field.replace(/_/g, ".");
|
|
260
260
|
}
|
|
261
|
-
function
|
|
261
|
+
function foldOffSurface(offSurface, rawByKey) {
|
|
262
|
+
const offSurfaceByField = new Map;
|
|
263
|
+
const offSurfaceMutations = [];
|
|
264
|
+
for (const m of offSurface) {
|
|
265
|
+
const key = norm(m.field);
|
|
266
|
+
const raw = rawByKey.get(key);
|
|
267
|
+
if (raw === undefined)
|
|
268
|
+
continue;
|
|
269
|
+
offSurfaceMutations.push({
|
|
270
|
+
field: raw,
|
|
271
|
+
signalVariable: m.signalVariable,
|
|
272
|
+
function: m.functionName,
|
|
273
|
+
file: m.filePath,
|
|
274
|
+
line: m.line,
|
|
275
|
+
declared: true
|
|
276
|
+
});
|
|
277
|
+
const list = offSurfaceByField.get(key) ?? [];
|
|
278
|
+
list.push({ function: m.functionName, file: m.filePath, line: m.line });
|
|
279
|
+
offSurfaceByField.set(key, list);
|
|
280
|
+
}
|
|
281
|
+
return { offSurfaceByField, offSurfaceMutations };
|
|
282
|
+
}
|
|
283
|
+
function computeModelCoverage(stateFields, handlers, offSurface = []) {
|
|
262
284
|
const writersByField = new Map;
|
|
263
285
|
for (const field of stateFields) {
|
|
264
286
|
writersByField.set(norm(field), new Set);
|
|
265
287
|
}
|
|
266
288
|
const declaredOrder = stateFields.map((f) => ({ raw: f, key: norm(f) }));
|
|
289
|
+
const rawByKey = new Map(declaredOrder.map((d) => [d.key, d.raw]));
|
|
267
290
|
for (const handler of handlers) {
|
|
268
291
|
for (const assignment of handler.assignments) {
|
|
269
292
|
const key = norm(assignment.field);
|
|
@@ -272,10 +295,15 @@ function computeModelCoverage(stateFields, handlers) {
|
|
|
272
295
|
writers.add(handler.messageType);
|
|
273
296
|
}
|
|
274
297
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
298
|
+
const { offSurfaceByField, offSurfaceMutations } = foldOffSurface(offSurface, rawByKey);
|
|
299
|
+
const fieldCoverage = declaredOrder.map(({ raw, key }) => {
|
|
300
|
+
const offSurfaceWriters = offSurfaceByField.get(key);
|
|
301
|
+
return {
|
|
302
|
+
field: raw,
|
|
303
|
+
writers: Array.from(writersByField.get(key) ?? []).sort(),
|
|
304
|
+
...offSurfaceWriters && offSurfaceWriters.length > 0 ? { offSurfaceWriters } : {}
|
|
305
|
+
};
|
|
306
|
+
});
|
|
279
307
|
const unwrittenFields = fieldCoverage.filter((f) => f.writers.length === 0).map((f) => f.field);
|
|
280
308
|
const declaredKeys = new Set(declaredOrder.map((d) => d.key));
|
|
281
309
|
const unconstrainedMutators = [];
|
|
@@ -291,18 +319,24 @@ function computeModelCoverage(stateFields, handlers) {
|
|
|
291
319
|
});
|
|
292
320
|
}
|
|
293
321
|
}
|
|
322
|
+
const hasDeclaredOffSurface = offSurfaceMutations.some((m) => m.declared);
|
|
294
323
|
return {
|
|
295
324
|
fieldCoverage,
|
|
296
325
|
unwrittenFields,
|
|
297
326
|
unconstrainedMutators,
|
|
298
|
-
|
|
327
|
+
offSurfaceMutations,
|
|
328
|
+
hasStrictViolation: unwrittenFields.length > 0 || hasDeclaredOffSurface
|
|
299
329
|
};
|
|
300
330
|
}
|
|
301
331
|
function strictCoverageReasons(report, meshFindingCount) {
|
|
302
332
|
const reasons = [];
|
|
303
|
-
if (report.
|
|
333
|
+
if (report.unwrittenFields.length > 0) {
|
|
304
334
|
reasons.push(`${report.unwrittenFields.length} declared state field(s) written by no modelled handler`);
|
|
305
335
|
}
|
|
336
|
+
const declaredOffSurface = (report.offSurfaceMutations ?? []).filter((m) => m.declared);
|
|
337
|
+
if (declaredOffSurface.length > 0) {
|
|
338
|
+
reasons.push(`${declaredOffSurface.length} declared state field write(s) outside any modelled transition (polly#163)`);
|
|
339
|
+
}
|
|
306
340
|
if (meshFindingCount > 0) {
|
|
307
341
|
reasons.push(`${meshFindingCount} unverified $meshState/$peerState predicate(s)`);
|
|
308
342
|
}
|
|
@@ -3357,15 +3391,20 @@ var init_witness = __esm(() => {
|
|
|
3357
3391
|
var exports_witness2 = {};
|
|
3358
3392
|
__export(exports_witness2, {
|
|
3359
3393
|
witnessVerdict: () => witnessVerdict,
|
|
3394
|
+
witnessSpecLocation: () => witnessSpecLocation,
|
|
3360
3395
|
witnessPolarity: () => witnessPolarity,
|
|
3361
3396
|
routeWitness: () => routeWitness,
|
|
3397
|
+
parseModuleName: () => parseModuleName,
|
|
3362
3398
|
buildWitnessModule: () => buildWitnessModule,
|
|
3399
|
+
buildWitnessInvariantBare: () => buildWitnessInvariantBare,
|
|
3363
3400
|
buildWitnessInvariant: () => buildWitnessInvariant,
|
|
3364
3401
|
buildWitnessCfg: () => buildWitnessCfg,
|
|
3365
3402
|
bddPredicateToTLA: () => bddPredicateToTLA,
|
|
3403
|
+
bareFieldRenderer: () => bareFieldRenderer,
|
|
3366
3404
|
WitnessTranslationError: () => WitnessTranslationError,
|
|
3367
3405
|
WITNESS_INVARIANT: () => WITNESS_INVARIANT
|
|
3368
3406
|
});
|
|
3407
|
+
import * as path3 from "node:path";
|
|
3369
3408
|
function witnessPolarity(tags) {
|
|
3370
3409
|
return tags.includes("forbidden") ? "forbidden" : "positive";
|
|
3371
3410
|
}
|
|
@@ -3383,10 +3422,13 @@ function witnessVerdict(polarity, reachable) {
|
|
|
3383
3422
|
message: "the model proves this outcome impossible (the scenario lies)"
|
|
3384
3423
|
};
|
|
3385
3424
|
}
|
|
3386
|
-
function flattenField(
|
|
3387
|
-
return
|
|
3425
|
+
function flattenField(path4) {
|
|
3426
|
+
return path4.split(".").join("_");
|
|
3388
3427
|
}
|
|
3389
|
-
function
|
|
3428
|
+
function bareFieldRenderer(fields = {}) {
|
|
3429
|
+
return (fieldPath) => fields[fieldPath] ?? flattenField(fieldPath);
|
|
3430
|
+
}
|
|
3431
|
+
function translateOperand(raw, render) {
|
|
3390
3432
|
const op = raw.trim();
|
|
3391
3433
|
if (op === "true")
|
|
3392
3434
|
return "TRUE";
|
|
@@ -3402,11 +3444,11 @@ function translateOperand(raw) {
|
|
|
3402
3444
|
throw new WitnessTranslationError(`unsupported operand "${raw}" in witness predicate`);
|
|
3403
3445
|
}
|
|
3404
3446
|
if (op.endsWith(".length")) {
|
|
3405
|
-
return `Len(
|
|
3447
|
+
return `Len(${render(op.slice(0, -".length".length))})`;
|
|
3406
3448
|
}
|
|
3407
|
-
return
|
|
3449
|
+
return render(op);
|
|
3408
3450
|
}
|
|
3409
|
-
function translateConjunct(conjunct) {
|
|
3451
|
+
function translateConjunct(conjunct, render) {
|
|
3410
3452
|
const text = conjunct.trim();
|
|
3411
3453
|
for (const [js, tla] of COMPARATORS) {
|
|
3412
3454
|
const at = text.indexOf(js);
|
|
@@ -3414,31 +3456,59 @@ function translateConjunct(conjunct) {
|
|
|
3414
3456
|
continue;
|
|
3415
3457
|
const lhs = text.slice(0, at);
|
|
3416
3458
|
const rhs = text.slice(at + js.length);
|
|
3417
|
-
return `${translateOperand(lhs)} ${tla} ${translateOperand(rhs)}`;
|
|
3459
|
+
return `${translateOperand(lhs, render)} ${tla} ${translateOperand(rhs, render)}`;
|
|
3418
3460
|
}
|
|
3419
3461
|
throw new WitnessTranslationError(`no comparison operator in witness conjunct "${conjunct}"`);
|
|
3420
3462
|
}
|
|
3421
|
-
function bddPredicateToTLA(predicate) {
|
|
3463
|
+
function bddPredicateToTLA(predicate, render = contextFieldRenderer) {
|
|
3422
3464
|
const conjuncts = predicate.split("&&").map((c) => c.trim()).filter(Boolean);
|
|
3423
3465
|
if (conjuncts.length === 0) {
|
|
3424
3466
|
throw new WitnessTranslationError("empty witness predicate");
|
|
3425
3467
|
}
|
|
3426
|
-
return conjuncts.map(translateConjunct).join(" /\\ ");
|
|
3468
|
+
return conjuncts.map((c) => translateConjunct(c, render)).join(" /\\ ");
|
|
3427
3469
|
}
|
|
3428
3470
|
function buildWitnessInvariant(tlaPredicate) {
|
|
3429
3471
|
return `${WITNESS_INVARIANT} == ~(\\E ctx \\in Contexts : ${tlaPredicate})`;
|
|
3430
3472
|
}
|
|
3431
|
-
function
|
|
3473
|
+
function buildWitnessInvariantBare(tlaPredicate) {
|
|
3474
|
+
return `${WITNESS_INVARIANT} == ~(${tlaPredicate})`;
|
|
3475
|
+
}
|
|
3476
|
+
function buildWitnessModule(moduleName, baseModule, tlaPredicate, options = {}) {
|
|
3477
|
+
const invariant = options.bare ? buildWitnessInvariantBare(tlaPredicate) : buildWitnessInvariant(tlaPredicate);
|
|
3432
3478
|
return [
|
|
3433
3479
|
`---- MODULE ${moduleName} ----`,
|
|
3434
|
-
`EXTENDS ${
|
|
3480
|
+
`EXTENDS ${baseModule}`,
|
|
3435
3481
|
"",
|
|
3436
|
-
|
|
3482
|
+
invariant,
|
|
3437
3483
|
"====",
|
|
3438
3484
|
""
|
|
3439
3485
|
].join(`
|
|
3440
3486
|
`);
|
|
3441
3487
|
}
|
|
3488
|
+
function parseModuleName(tlaText) {
|
|
3489
|
+
const m = /^-+\s*MODULE\s+([A-Za-z_]\w*)/m.exec(tlaText);
|
|
3490
|
+
return m?.[1] ?? null;
|
|
3491
|
+
}
|
|
3492
|
+
function witnessSpecLocation(cwd, subsystem, custom, customModule) {
|
|
3493
|
+
if (custom) {
|
|
3494
|
+
const tlaPath = path3.resolve(cwd, custom.tla);
|
|
3495
|
+
return {
|
|
3496
|
+
dir: path3.dirname(tlaPath),
|
|
3497
|
+
tlaPath,
|
|
3498
|
+
cfgPath: path3.resolve(cwd, custom.cfg),
|
|
3499
|
+
module: customModule,
|
|
3500
|
+
custom: true
|
|
3501
|
+
};
|
|
3502
|
+
}
|
|
3503
|
+
const dir = path3.join(cwd, "specs", "tla", "generated", subsystem);
|
|
3504
|
+
return {
|
|
3505
|
+
dir,
|
|
3506
|
+
tlaPath: path3.join(dir, `UserApp_${subsystem}.tla`),
|
|
3507
|
+
cfgPath: path3.join(dir, `UserApp_${subsystem}.cfg`),
|
|
3508
|
+
module: `UserApp_${subsystem}`,
|
|
3509
|
+
custom: false
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3442
3512
|
function sectionBody(cfg, header) {
|
|
3443
3513
|
const body = [];
|
|
3444
3514
|
let inSection = false;
|
|
@@ -3457,6 +3527,28 @@ function sectionBody(cfg, header) {
|
|
|
3457
3527
|
}
|
|
3458
3528
|
return body;
|
|
3459
3529
|
}
|
|
3530
|
+
function constantBody(cfg) {
|
|
3531
|
+
const body = [];
|
|
3532
|
+
let inSection = false;
|
|
3533
|
+
for (const line of cfg.split(`
|
|
3534
|
+
`)) {
|
|
3535
|
+
const headerMatch = /^([A-Z_]+)\b(.*)$/.exec(line);
|
|
3536
|
+
if (headerMatch) {
|
|
3537
|
+
const isConstant = headerMatch[1] === "CONSTANT" || headerMatch[1] === "CONSTANTS";
|
|
3538
|
+
inSection = isConstant;
|
|
3539
|
+
const inline = (headerMatch[2] ?? "").trim();
|
|
3540
|
+
if (isConstant && inline !== "")
|
|
3541
|
+
body.push(` ${inline}`);
|
|
3542
|
+
continue;
|
|
3543
|
+
}
|
|
3544
|
+
if (!inSection)
|
|
3545
|
+
continue;
|
|
3546
|
+
if (line.trim() === "" || line.trim().startsWith("\\*"))
|
|
3547
|
+
continue;
|
|
3548
|
+
body.push(line);
|
|
3549
|
+
}
|
|
3550
|
+
return body;
|
|
3551
|
+
}
|
|
3460
3552
|
function headerLine(cfg, header) {
|
|
3461
3553
|
for (const line of cfg.split(`
|
|
3462
3554
|
`)) {
|
|
@@ -3467,11 +3559,22 @@ function headerLine(cfg, header) {
|
|
|
3467
3559
|
return null;
|
|
3468
3560
|
}
|
|
3469
3561
|
function buildWitnessCfg(baseCfg) {
|
|
3470
|
-
const spec = headerLine(baseCfg, "SPECIFICATION")
|
|
3471
|
-
const
|
|
3562
|
+
const spec = headerLine(baseCfg, "SPECIFICATION");
|
|
3563
|
+
const init = headerLine(baseCfg, "INIT");
|
|
3564
|
+
const next = headerLine(baseCfg, "NEXT");
|
|
3565
|
+
const behaviour = spec ? [spec] : init && next ? [init, next] : ["SPECIFICATION UserSpec"];
|
|
3566
|
+
const constants = constantBody(baseCfg);
|
|
3472
3567
|
const constraint = sectionBody(baseCfg, "CONSTRAINT");
|
|
3473
3568
|
const symmetry = sectionBody(baseCfg, "SYMMETRY");
|
|
3474
|
-
const out = [
|
|
3569
|
+
const out = [
|
|
3570
|
+
...behaviour,
|
|
3571
|
+
"",
|
|
3572
|
+
"CONSTANTS",
|
|
3573
|
+
...constants,
|
|
3574
|
+
"",
|
|
3575
|
+
"INVARIANTS",
|
|
3576
|
+
` ${WITNESS_INVARIANT}`
|
|
3577
|
+
];
|
|
3475
3578
|
if (constraint.length > 0)
|
|
3476
3579
|
out.push("", "CONSTRAINT", ...constraint);
|
|
3477
3580
|
if (symmetry.length > 0)
|
|
@@ -3490,7 +3593,7 @@ function routeWitness(fields, subsystems) {
|
|
|
3490
3593
|
const only = owners.length === 1 ? owners[0] : undefined;
|
|
3491
3594
|
return only ? only[0] : null;
|
|
3492
3595
|
}
|
|
3493
|
-
var WITNESS_INVARIANT = "WitnessReachable", WitnessTranslationError, COMPARATORS
|
|
3596
|
+
var WITNESS_INVARIANT = "WitnessReachable", WitnessTranslationError, COMPARATORS, contextFieldRenderer = (fieldPath) => `contextStates[ctx].${flattenField(fieldPath)}`;
|
|
3494
3597
|
var init_witness2 = __esm(() => {
|
|
3495
3598
|
WitnessTranslationError = class WitnessTranslationError extends Error {
|
|
3496
3599
|
};
|
|
@@ -3513,11 +3616,11 @@ __export(exports_docker, {
|
|
|
3513
3616
|
});
|
|
3514
3617
|
import { spawn } from "node:child_process";
|
|
3515
3618
|
import * as fs3 from "node:fs";
|
|
3516
|
-
import * as
|
|
3619
|
+
import * as path4 from "node:path";
|
|
3517
3620
|
|
|
3518
3621
|
class DockerRunner {
|
|
3519
3622
|
IMAGE_NAME = "polly-tla:latest";
|
|
3520
|
-
DOCKERFILE_PATH =
|
|
3623
|
+
DOCKERFILE_PATH = path4.join(__dirname, "../../Dockerfile");
|
|
3521
3624
|
async isDockerAvailable() {
|
|
3522
3625
|
try {
|
|
3523
3626
|
const result = await this.runCommand("docker", ["info"], {
|
|
@@ -3545,7 +3648,7 @@ class DockerRunner {
|
|
|
3545
3648
|
}
|
|
3546
3649
|
}
|
|
3547
3650
|
async buildImage(onProgress) {
|
|
3548
|
-
const dockerfileDir =
|
|
3651
|
+
const dockerfileDir = path4.dirname(this.DOCKERFILE_PATH);
|
|
3549
3652
|
await this.runCommandStreaming("docker", ["build", "-f", this.DOCKERFILE_PATH, "-t", this.IMAGE_NAME, dockerfileDir], onProgress, 300000);
|
|
3550
3653
|
}
|
|
3551
3654
|
async pullImage(onProgress) {
|
|
@@ -3555,13 +3658,13 @@ class DockerRunner {
|
|
|
3555
3658
|
if (!fs3.existsSync(specPath)) {
|
|
3556
3659
|
throw new Error(`Spec file not found: ${specPath}`);
|
|
3557
3660
|
}
|
|
3558
|
-
const specDir =
|
|
3559
|
-
const specName =
|
|
3560
|
-
const cfgPath =
|
|
3661
|
+
const specDir = path4.dirname(specPath);
|
|
3662
|
+
const specName = path4.basename(specPath, ".tla");
|
|
3663
|
+
const cfgPath = path4.join(specDir, `${specName}.cfg`);
|
|
3561
3664
|
if (!fs3.existsSync(cfgPath)) {
|
|
3562
3665
|
throw new Error(`Config file not found: ${cfgPath}`);
|
|
3563
3666
|
}
|
|
3564
|
-
const statesDir =
|
|
3667
|
+
const statesDir = path4.join(specDir, "states");
|
|
3565
3668
|
if (fs3.existsSync(statesDir)) {
|
|
3566
3669
|
fs3.rmSync(statesDir, { recursive: true, force: true });
|
|
3567
3670
|
}
|
|
@@ -3654,7 +3757,7 @@ class DockerRunner {
|
|
|
3654
3757
|
return "Unknown error occurred during model checking";
|
|
3655
3758
|
}
|
|
3656
3759
|
runCommand(command, args, options) {
|
|
3657
|
-
return new Promise((
|
|
3760
|
+
return new Promise((resolve3, reject) => {
|
|
3658
3761
|
const proc = spawn(command, args);
|
|
3659
3762
|
let stdout = "";
|
|
3660
3763
|
let stderr = "";
|
|
@@ -3672,7 +3775,7 @@ class DockerRunner {
|
|
|
3672
3775
|
proc.on("close", (exitCode) => {
|
|
3673
3776
|
if (timeout)
|
|
3674
3777
|
clearTimeout(timeout);
|
|
3675
|
-
|
|
3778
|
+
resolve3({
|
|
3676
3779
|
exitCode: exitCode || 0,
|
|
3677
3780
|
stdout,
|
|
3678
3781
|
stderr
|
|
@@ -3686,7 +3789,7 @@ class DockerRunner {
|
|
|
3686
3789
|
});
|
|
3687
3790
|
}
|
|
3688
3791
|
runCommandStreaming(command, args, onOutput, timeout) {
|
|
3689
|
-
return new Promise((
|
|
3792
|
+
return new Promise((resolve3, reject) => {
|
|
3690
3793
|
const proc = spawn(command, args);
|
|
3691
3794
|
const timeoutHandle = timeout && timeout > 0 ? setTimeout(() => {
|
|
3692
3795
|
proc.kill();
|
|
@@ -3718,7 +3821,7 @@ class DockerRunner {
|
|
|
3718
3821
|
if (timeoutHandle)
|
|
3719
3822
|
clearTimeout(timeoutHandle);
|
|
3720
3823
|
if (exitCode === 0) {
|
|
3721
|
-
|
|
3824
|
+
resolve3();
|
|
3722
3825
|
} else {
|
|
3723
3826
|
reject(new Error(`Command failed with exit code ${exitCode}`));
|
|
3724
3827
|
}
|
|
@@ -3737,7 +3840,7 @@ var init_docker = () => {};
|
|
|
3737
3840
|
// tools/verify/src/cli.ts
|
|
3738
3841
|
init_expression_validator();
|
|
3739
3842
|
import * as fs4 from "node:fs";
|
|
3740
|
-
import * as
|
|
3843
|
+
import * as path5 from "node:path";
|
|
3741
3844
|
|
|
3742
3845
|
// tools/verify/src/analysis/mesh-signal-warnings.ts
|
|
3743
3846
|
function computeMeshOrPeerSignalFindings(analysis, declaredMeshDocs) {
|
|
@@ -5464,6 +5567,7 @@ class HandlerExtractor {
|
|
|
5464
5567
|
warnings;
|
|
5465
5568
|
currentFunctionParams = [];
|
|
5466
5569
|
contextOverrides;
|
|
5570
|
+
onSurfaceSpans = [];
|
|
5467
5571
|
constructor(tsConfigPath, contextOverrides) {
|
|
5468
5572
|
this.project = new Project({
|
|
5469
5573
|
tsConfigFilePath: tsConfigPath
|
|
@@ -5523,6 +5627,7 @@ class HandlerExtractor {
|
|
|
5523
5627
|
const meshOrPeerSignals = [];
|
|
5524
5628
|
const resources = [];
|
|
5525
5629
|
this.warnings = [];
|
|
5630
|
+
this.onSurfaceSpans = [];
|
|
5526
5631
|
const allSourceFiles = this.project.getSourceFiles();
|
|
5527
5632
|
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
5528
5633
|
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
@@ -5557,6 +5662,30 @@ class HandlerExtractor {
|
|
|
5557
5662
|
continue;
|
|
5558
5663
|
meshOrPeerSignals.push(...this.extractMeshOrPeerSignalsFromFile(sourceFile));
|
|
5559
5664
|
}
|
|
5665
|
+
const stateSignalNames = new Set;
|
|
5666
|
+
for (const filePath of this.analyzedFiles) {
|
|
5667
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
5668
|
+
if (!sourceFile)
|
|
5669
|
+
continue;
|
|
5670
|
+
for (const name of this.extractStateSignalVariableNames(sourceFile)) {
|
|
5671
|
+
stateSignalNames.add(name);
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
for (const v of verifiedStates)
|
|
5675
|
+
stateSignalNames.add(v.variableName);
|
|
5676
|
+
for (const m of meshOrPeerSignals)
|
|
5677
|
+
stateSignalNames.add(m.variableName);
|
|
5678
|
+
const offSurfaceMutations = [];
|
|
5679
|
+
if (stateSignalNames.size > 0) {
|
|
5680
|
+
for (const filePath of this.analyzedFiles) {
|
|
5681
|
+
if (!this.isOffSurfaceScannable(filePath))
|
|
5682
|
+
continue;
|
|
5683
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
5684
|
+
if (!sourceFile)
|
|
5685
|
+
continue;
|
|
5686
|
+
offSurfaceMutations.push(...this.findOffSurfaceMutations(sourceFile, stateSignalNames));
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5560
5689
|
this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
|
|
5561
5690
|
this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
|
|
5562
5691
|
return {
|
|
@@ -5567,6 +5696,7 @@ class HandlerExtractor {
|
|
|
5567
5696
|
verifiedStates,
|
|
5568
5697
|
meshOrPeerSignals,
|
|
5569
5698
|
resources,
|
|
5699
|
+
offSurfaceMutations,
|
|
5570
5700
|
warnings: this.warnings
|
|
5571
5701
|
};
|
|
5572
5702
|
}
|
|
@@ -5977,6 +6107,7 @@ class HandlerExtractor {
|
|
|
5977
6107
|
return false;
|
|
5978
6108
|
}
|
|
5979
6109
|
extractAssignments(funcNode, assignments) {
|
|
6110
|
+
this.recordOnSurfaceSpan(funcNode);
|
|
5980
6111
|
funcNode.forEachDescendant((node) => {
|
|
5981
6112
|
if (Node2.isBinaryExpression(node)) {
|
|
5982
6113
|
this.extractBinaryExpressionAssignment(node, assignments);
|
|
@@ -7563,6 +7694,7 @@ class HandlerExtractor {
|
|
|
7563
7694
|
}
|
|
7564
7695
|
findStateMutationsInFunction(func, stateVarNames) {
|
|
7565
7696
|
const mutations = [];
|
|
7697
|
+
this.recordOnSurfaceSpan(func);
|
|
7566
7698
|
func.forEachDescendant((node) => {
|
|
7567
7699
|
if (!Node2.isBinaryExpression(node))
|
|
7568
7700
|
return;
|
|
@@ -7592,6 +7724,140 @@ class HandlerExtractor {
|
|
|
7592
7724
|
});
|
|
7593
7725
|
return mutations;
|
|
7594
7726
|
}
|
|
7727
|
+
isOffSurfaceScannable(filePath) {
|
|
7728
|
+
return !/(?:\.(?:test|spec|stories)\.[cm]?[jt]sx?$)|(?:\/(?:__tests__|tests|test|features|e2e|stories|__mocks__)\/)/i.test(filePath);
|
|
7729
|
+
}
|
|
7730
|
+
static STATE_SIGNAL_FACTORIES = new Set([
|
|
7731
|
+
"$state",
|
|
7732
|
+
"$sharedState",
|
|
7733
|
+
"$syncedState",
|
|
7734
|
+
"$persistedState",
|
|
7735
|
+
"$meshState",
|
|
7736
|
+
"$peerState"
|
|
7737
|
+
]);
|
|
7738
|
+
extractStateSignalVariableNames(sourceFile) {
|
|
7739
|
+
const names = [];
|
|
7740
|
+
sourceFile.forEachDescendant((node) => {
|
|
7741
|
+
if (!Node2.isCallExpression(node))
|
|
7742
|
+
return;
|
|
7743
|
+
const expr = node.getExpression();
|
|
7744
|
+
if (!Node2.isIdentifier(expr))
|
|
7745
|
+
return;
|
|
7746
|
+
if (!HandlerExtractor.STATE_SIGNAL_FACTORIES.has(expr.getText()))
|
|
7747
|
+
return;
|
|
7748
|
+
const varName = this.getVariableNameFromParent(node);
|
|
7749
|
+
if (varName)
|
|
7750
|
+
names.push(varName);
|
|
7751
|
+
});
|
|
7752
|
+
return names;
|
|
7753
|
+
}
|
|
7754
|
+
recordOnSurfaceSpan(node) {
|
|
7755
|
+
this.onSurfaceSpans.push({
|
|
7756
|
+
file: node.getSourceFile().getFilePath(),
|
|
7757
|
+
start: node.getStart(),
|
|
7758
|
+
end: node.getEnd()
|
|
7759
|
+
});
|
|
7760
|
+
}
|
|
7761
|
+
isWithinOnSurfaceSpan(file, pos) {
|
|
7762
|
+
for (const span of this.onSurfaceSpans) {
|
|
7763
|
+
if (span.file === file && pos >= span.start && pos <= span.end)
|
|
7764
|
+
return true;
|
|
7765
|
+
}
|
|
7766
|
+
return false;
|
|
7767
|
+
}
|
|
7768
|
+
findOffSurfaceMutations(sourceFile, stateVarNames) {
|
|
7769
|
+
const out = [];
|
|
7770
|
+
const filePath = sourceFile.getFilePath();
|
|
7771
|
+
sourceFile.forEachDescendant((node) => {
|
|
7772
|
+
if (Node2.isBinaryExpression(node)) {
|
|
7773
|
+
out.push(...this.offSurfaceWritesAt(node, stateVarNames, filePath));
|
|
7774
|
+
}
|
|
7775
|
+
});
|
|
7776
|
+
return out;
|
|
7777
|
+
}
|
|
7778
|
+
offSurfaceWritesAt(node, stateVarNames, filePath) {
|
|
7779
|
+
if (node.getOperatorToken().getText() !== "=")
|
|
7780
|
+
return [];
|
|
7781
|
+
const left = node.getLeft();
|
|
7782
|
+
if (!Node2.isPropertyAccessExpression(left))
|
|
7783
|
+
return [];
|
|
7784
|
+
const match = this.matchVerifiedStateWrite(this.getPropertyPath(left), stateVarNames);
|
|
7785
|
+
if (!match)
|
|
7786
|
+
return [];
|
|
7787
|
+
if (this.isWithinOnSurfaceSpan(filePath, left.getStart()))
|
|
7788
|
+
return [];
|
|
7789
|
+
const base = {
|
|
7790
|
+
signalVariable: match.signal,
|
|
7791
|
+
functionName: this.enclosingFunctionName(node),
|
|
7792
|
+
filePath,
|
|
7793
|
+
line: node.getStartLineNumber()
|
|
7794
|
+
};
|
|
7795
|
+
if (match.field !== undefined) {
|
|
7796
|
+
return [{ field: `${match.signal}_${match.field}`, ...base }];
|
|
7797
|
+
}
|
|
7798
|
+
const fields = this.objectLiteralFieldNames(node.getRight());
|
|
7799
|
+
if (fields.length === 0)
|
|
7800
|
+
return [{ field: match.signal, ...base }];
|
|
7801
|
+
return fields.map((f) => ({ field: `${match.signal}_${f}`, ...base }));
|
|
7802
|
+
}
|
|
7803
|
+
matchVerifiedStateWrite(path2, stateVarNames) {
|
|
7804
|
+
for (const varName of stateVarNames) {
|
|
7805
|
+
if (path2 === `${varName}.value`)
|
|
7806
|
+
return { signal: varName };
|
|
7807
|
+
const prefix = `${varName}.value.`;
|
|
7808
|
+
if (path2.startsWith(prefix))
|
|
7809
|
+
return { signal: varName, field: path2.substring(prefix.length) };
|
|
7810
|
+
}
|
|
7811
|
+
return null;
|
|
7812
|
+
}
|
|
7813
|
+
objectLiteralFieldNames(right) {
|
|
7814
|
+
if (!Node2.isObjectLiteralExpression(right))
|
|
7815
|
+
return [];
|
|
7816
|
+
const names = [];
|
|
7817
|
+
for (const prop of right.getProperties()) {
|
|
7818
|
+
if (Node2.isPropertyAssignment(prop) || Node2.isShorthandPropertyAssignment(prop)) {
|
|
7819
|
+
names.push(prop.getName());
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
return names;
|
|
7823
|
+
}
|
|
7824
|
+
enclosingFunctionName(node) {
|
|
7825
|
+
let current = node.getParent();
|
|
7826
|
+
while (current) {
|
|
7827
|
+
const name = this.namedScope(current);
|
|
7828
|
+
if (name !== null)
|
|
7829
|
+
return name;
|
|
7830
|
+
current = current.getParent();
|
|
7831
|
+
}
|
|
7832
|
+
return "<module>";
|
|
7833
|
+
}
|
|
7834
|
+
namedScope(node) {
|
|
7835
|
+
if (Node2.isFunctionDeclaration(node)) {
|
|
7836
|
+
return node.getName() ?? "<anonymous function>";
|
|
7837
|
+
}
|
|
7838
|
+
if (Node2.isMethodDeclaration(node)) {
|
|
7839
|
+
const clsName = node.getFirstAncestorByKind(SyntaxKind.ClassDeclaration)?.getName();
|
|
7840
|
+
const methodName = node.getName();
|
|
7841
|
+
return clsName ? `${clsName}.${methodName}` : methodName;
|
|
7842
|
+
}
|
|
7843
|
+
if (Node2.isGetAccessorDeclaration(node) || Node2.isSetAccessorDeclaration(node)) {
|
|
7844
|
+
return node.getName();
|
|
7845
|
+
}
|
|
7846
|
+
if (Node2.isArrowFunction(node) || Node2.isFunctionExpression(node)) {
|
|
7847
|
+
return this.boundFunctionName(node) ?? null;
|
|
7848
|
+
}
|
|
7849
|
+
return null;
|
|
7850
|
+
}
|
|
7851
|
+
boundFunctionName(fn) {
|
|
7852
|
+
const parent = fn.getParent();
|
|
7853
|
+
if (!parent)
|
|
7854
|
+
return;
|
|
7855
|
+
if (Node2.isVariableDeclaration(parent))
|
|
7856
|
+
return parent.getName();
|
|
7857
|
+
if (Node2.isPropertyAssignment(parent))
|
|
7858
|
+
return parent.getName();
|
|
7859
|
+
return;
|
|
7860
|
+
}
|
|
7595
7861
|
functionNameToMessageType(funcName) {
|
|
7596
7862
|
let name = funcName.replace(/^handle/, "").replace(/^on/, "").replace(/^set/, "Set").replace(/^update/, "Update").replace(/^do/, "");
|
|
7597
7863
|
if (name.length > 0) {
|
|
@@ -7756,7 +8022,8 @@ class TypeExtractor {
|
|
|
7756
8022
|
globalStateConstraints: handlerAnalysis.globalStateConstraints,
|
|
7757
8023
|
verifiedStates: handlerAnalysis.verifiedStates,
|
|
7758
8024
|
meshOrPeerSignals: handlerAnalysis.meshOrPeerSignals,
|
|
7759
|
-
resources: handlerAnalysis.resources
|
|
8025
|
+
resources: handlerAnalysis.resources,
|
|
8026
|
+
offSurfaceMutations: handlerAnalysis.offSurfaceMutations
|
|
7760
8027
|
};
|
|
7761
8028
|
}
|
|
7762
8029
|
extractHandlerAnalysis() {
|
|
@@ -8227,7 +8494,7 @@ async function setupCommand() {
|
|
|
8227
8494
|
displayAnalysisResults(analysis);
|
|
8228
8495
|
displayAnalysisSummary(analysis);
|
|
8229
8496
|
const configContent = generateConfig(analysis);
|
|
8230
|
-
const configPath =
|
|
8497
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8231
8498
|
writeConfigFile(configPath, configContent);
|
|
8232
8499
|
displaySetupSuccess(configPath);
|
|
8233
8500
|
} catch (_error) {
|
|
@@ -8275,7 +8542,7 @@ function getFieldStatus(confidence) {
|
|
|
8275
8542
|
return color("⚠ Manual config", COLORS.red);
|
|
8276
8543
|
}
|
|
8277
8544
|
function writeConfigFile(configPath, configContent) {
|
|
8278
|
-
const configDir =
|
|
8545
|
+
const configDir = path5.dirname(configPath);
|
|
8279
8546
|
if (!fs4.existsSync(configDir)) {
|
|
8280
8547
|
fs4.mkdirSync(configDir, { recursive: true });
|
|
8281
8548
|
}
|
|
@@ -8298,7 +8565,7 @@ function displaySetupSuccess(configPath) {
|
|
|
8298
8565
|
console.log();
|
|
8299
8566
|
}
|
|
8300
8567
|
async function validateCommand() {
|
|
8301
|
-
const configPath =
|
|
8568
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8302
8569
|
console.log(color(`
|
|
8303
8570
|
\uD83D\uDD0D Validating configuration...
|
|
8304
8571
|
`, COLORS.blue));
|
|
@@ -8319,7 +8586,7 @@ async function validateCommand() {
|
|
|
8319
8586
|
process.exit(1);
|
|
8320
8587
|
}
|
|
8321
8588
|
async function estimateCommand() {
|
|
8322
|
-
const configPath =
|
|
8589
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8323
8590
|
console.log(color(`
|
|
8324
8591
|
\uD83D\uDCCA Estimating state space...
|
|
8325
8592
|
`, COLORS.blue));
|
|
@@ -8462,8 +8729,8 @@ function isWitnessMode() {
|
|
|
8462
8729
|
return process.argv.includes("--witness");
|
|
8463
8730
|
}
|
|
8464
8731
|
function displayModelCoverage(report) {
|
|
8465
|
-
const { unwrittenFields, unconstrainedMutators, fieldCoverage } = report;
|
|
8466
|
-
if (unwrittenFields.length === 0 && unconstrainedMutators.length === 0) {
|
|
8732
|
+
const { unwrittenFields, unconstrainedMutators, fieldCoverage, offSurfaceMutations } = report;
|
|
8733
|
+
if (unwrittenFields.length === 0 && unconstrainedMutators.length === 0 && offSurfaceMutations.length === 0) {
|
|
8467
8734
|
console.log(color(`✓ Model coverage: all ${fieldCoverage.length} declared field(s) written by a modelled handler`, COLORS.green));
|
|
8468
8735
|
console.log();
|
|
8469
8736
|
return;
|
|
@@ -8487,11 +8754,24 @@ function displayModelCoverage(report) {
|
|
|
8487
8754
|
console.log(color(" The checker explores these transitions but asserts nothing about their effect.", COLORS.gray));
|
|
8488
8755
|
console.log();
|
|
8489
8756
|
}
|
|
8757
|
+
if (offSurfaceMutations.length > 0) {
|
|
8758
|
+
console.log(color(`
|
|
8759
|
+
⚠️ ${offSurfaceMutations.length} declared state field write(s) outside any modelled transition (polly#163):`, COLORS.yellow));
|
|
8760
|
+
for (const m of offSurfaceMutations) {
|
|
8761
|
+
console.log(color(` • ${m.field} mutated in ${m.function}() — ${m.file}:${m.line}`, COLORS.yellow));
|
|
8762
|
+
}
|
|
8763
|
+
console.log(color(" A non-dispatched path (a method, a non-exported function, a closure) writes", COLORS.gray));
|
|
8764
|
+
console.log(color(" verified state the checker never explores — the register() shape (#160). Even", COLORS.gray));
|
|
8765
|
+
console.log(color(" when a handler also writes the field, model coverage cannot see this writer.", COLORS.gray));
|
|
8766
|
+
console.log(color(" Route the change through a dispatched handler, or drop the field from the", COLORS.gray));
|
|
8767
|
+
console.log(color(" verified surface. See tools/verify/OFF-SURFACE-MUTATORS.md.", COLORS.gray));
|
|
8768
|
+
console.log();
|
|
8769
|
+
}
|
|
8490
8770
|
}
|
|
8491
8771
|
async function runModelCoverage(typedConfig, typedAnalysis, meshFindingCount) {
|
|
8492
8772
|
const { computeModelCoverage: computeModelCoverage2, strictCoverageReasons: strictCoverageReasons2 } = await Promise.resolve().then(() => exports_model_coverage);
|
|
8493
8773
|
const stateFields = Object.keys(typedConfig.state ?? {});
|
|
8494
|
-
const coverage = computeModelCoverage2(stateFields, typedAnalysis.handlers);
|
|
8774
|
+
const coverage = computeModelCoverage2(stateFields, typedAnalysis.handlers, typedAnalysis.offSurfaceMutations ?? []);
|
|
8495
8775
|
displayModelCoverage(coverage);
|
|
8496
8776
|
if (!isStrictMode())
|
|
8497
8777
|
return;
|
|
@@ -8513,7 +8793,7 @@ async function runModelCoverage(typedConfig, typedAnalysis, meshFindingCount) {
|
|
|
8513
8793
|
process.exit(1);
|
|
8514
8794
|
}
|
|
8515
8795
|
async function verifyCommand() {
|
|
8516
|
-
const configPath =
|
|
8796
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8517
8797
|
console.log(color(`
|
|
8518
8798
|
\uD83D\uDD0D Running verification...
|
|
8519
8799
|
`, COLORS.blue));
|
|
@@ -8579,6 +8859,12 @@ function getMaxDepth(config) {
|
|
|
8579
8859
|
}
|
|
8580
8860
|
return;
|
|
8581
8861
|
}
|
|
8862
|
+
function getCustomTLAPaths(config) {
|
|
8863
|
+
if ("messages" in config && config.customTLAPaths) {
|
|
8864
|
+
return config.customTLAPaths;
|
|
8865
|
+
}
|
|
8866
|
+
return {};
|
|
8867
|
+
}
|
|
8582
8868
|
async function runCoupledFieldsLint(config, analysis) {
|
|
8583
8869
|
const groups = config.coupledFields ?? [];
|
|
8584
8870
|
if (groups.length === 0)
|
|
@@ -8726,18 +9012,23 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
8726
9012
|
const maxDepth = getMaxDepth(config);
|
|
8727
9013
|
const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
8728
9014
|
const results = [];
|
|
9015
|
+
const customTLAPaths = getCustomTLAPaths(config);
|
|
8729
9016
|
for (const name of subsystemNames) {
|
|
8730
9017
|
const sub = subsystems[name];
|
|
8731
9018
|
const startTime = Date.now();
|
|
9019
|
+
if (customTLAPaths[name]) {
|
|
9020
|
+
console.log(color(`⏭ ${name}: hand-written spec (customTLAPaths) — witnessed, not generated`, COLORS.gray));
|
|
9021
|
+
continue;
|
|
9022
|
+
}
|
|
8732
9023
|
console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
|
|
8733
9024
|
const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
|
|
8734
9025
|
const ensuresCount = (spec.match(/^EnsuresAfter_\w+ ==/gm) ?? []).length;
|
|
8735
|
-
const specDir =
|
|
9026
|
+
const specDir = path5.join(process.cwd(), "specs", "tla", "generated", name);
|
|
8736
9027
|
if (!fs4.existsSync(specDir)) {
|
|
8737
9028
|
fs4.mkdirSync(specDir, { recursive: true });
|
|
8738
9029
|
}
|
|
8739
|
-
const specPath =
|
|
8740
|
-
const cfgPath =
|
|
9030
|
+
const specPath = path5.join(specDir, `UserApp_${name}.tla`);
|
|
9031
|
+
const cfgPath = path5.join(specDir, `UserApp_${name}.cfg`);
|
|
8741
9032
|
fs4.writeFileSync(specPath, spec);
|
|
8742
9033
|
fs4.writeFileSync(cfgPath, cfg);
|
|
8743
9034
|
findAndCopyBaseSpec(specDir);
|
|
@@ -8766,7 +9057,7 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
8766
9057
|
} else if (result.error) {
|
|
8767
9058
|
console.log(color(` Error: ${result.error}`, COLORS.red));
|
|
8768
9059
|
}
|
|
8769
|
-
fs4.writeFileSync(
|
|
9060
|
+
fs4.writeFileSync(path5.join(specDir, "tlc-output.log"), result.output);
|
|
8770
9061
|
}
|
|
8771
9062
|
}
|
|
8772
9063
|
console.log();
|
|
@@ -8798,15 +9089,19 @@ async function runWitnessVerification(config) {
|
|
|
8798
9089
|
}
|
|
8799
9090
|
const { extractWitnesses: extractWitnesses2 } = await Promise.resolve().then(() => (init_witness(), exports_witness));
|
|
8800
9091
|
const {
|
|
9092
|
+
bareFieldRenderer: bareFieldRenderer2,
|
|
8801
9093
|
bddPredicateToTLA: bddPredicateToTLA2,
|
|
8802
9094
|
buildWitnessCfg: buildWitnessCfg2,
|
|
8803
9095
|
buildWitnessModule: buildWitnessModule2,
|
|
9096
|
+
parseModuleName: parseModuleName2,
|
|
8804
9097
|
routeWitness: routeWitness2,
|
|
8805
9098
|
witnessPolarity: witnessPolarity2,
|
|
9099
|
+
witnessSpecLocation: witnessSpecLocation2,
|
|
8806
9100
|
witnessVerdict: witnessVerdict2,
|
|
8807
9101
|
WITNESS_INVARIANT: WITNESS_INVARIANT2
|
|
8808
9102
|
} = await Promise.resolve().then(() => (init_witness2(), exports_witness2));
|
|
8809
9103
|
const subsystems = config.subsystems;
|
|
9104
|
+
const customTLAPaths = getCustomTLAPaths(config);
|
|
8810
9105
|
const witnesses = await extractWitnesses2(featureFiles, stepFiles);
|
|
8811
9106
|
const docker = await setupDocker();
|
|
8812
9107
|
const timeoutSeconds = getTimeout(config);
|
|
@@ -8836,21 +9131,49 @@ async function runWitnessVerification(config) {
|
|
|
8836
9131
|
});
|
|
8837
9132
|
continue;
|
|
8838
9133
|
}
|
|
8839
|
-
const
|
|
8840
|
-
|
|
8841
|
-
if (
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
9134
|
+
const custom = customTLAPaths[subsystem];
|
|
9135
|
+
let location;
|
|
9136
|
+
if (custom) {
|
|
9137
|
+
const tlaAbs = path5.resolve(cwd, custom.tla);
|
|
9138
|
+
const cfgAbs = path5.resolve(cwd, custom.cfg);
|
|
9139
|
+
if (!fs4.existsSync(tlaAbs) || !fs4.existsSync(cfgAbs)) {
|
|
9140
|
+
results.push({
|
|
9141
|
+
id,
|
|
9142
|
+
status: "error",
|
|
9143
|
+
ok: false,
|
|
9144
|
+
subsystem,
|
|
9145
|
+
note: `custom spec missing: ${custom.tla} / ${custom.cfg}`
|
|
9146
|
+
});
|
|
9147
|
+
continue;
|
|
9148
|
+
}
|
|
9149
|
+
const module = custom.module ?? parseModuleName2(fs4.readFileSync(tlaAbs, "utf8"));
|
|
9150
|
+
if (!module) {
|
|
9151
|
+
results.push({
|
|
9152
|
+
id,
|
|
9153
|
+
status: "error",
|
|
9154
|
+
ok: false,
|
|
9155
|
+
subsystem,
|
|
9156
|
+
note: `cannot read MODULE name from ${custom.tla}; set customTLAPaths.${subsystem}.module`
|
|
9157
|
+
});
|
|
9158
|
+
continue;
|
|
9159
|
+
}
|
|
9160
|
+
location = witnessSpecLocation2(cwd, subsystem, custom, module);
|
|
9161
|
+
} else {
|
|
9162
|
+
location = witnessSpecLocation2(cwd, subsystem, undefined, "");
|
|
9163
|
+
if (!fs4.existsSync(location.tlaPath) || !fs4.existsSync(location.cfgPath)) {
|
|
9164
|
+
results.push({
|
|
9165
|
+
id,
|
|
9166
|
+
status: "error",
|
|
9167
|
+
ok: false,
|
|
9168
|
+
subsystem,
|
|
9169
|
+
note: `subsystem spec missing: ${subsystem}`
|
|
9170
|
+
});
|
|
9171
|
+
continue;
|
|
9172
|
+
}
|
|
8850
9173
|
}
|
|
8851
9174
|
let tlaPredicate;
|
|
8852
9175
|
try {
|
|
8853
|
-
tlaPredicate = bddPredicateToTLA2(w.predicate);
|
|
9176
|
+
tlaPredicate = location.custom ? bddPredicateToTLA2(w.predicate, bareFieldRenderer2(custom?.fields)) : bddPredicateToTLA2(w.predicate);
|
|
8854
9177
|
} catch (err) {
|
|
8855
9178
|
results.push({
|
|
8856
9179
|
id,
|
|
@@ -8862,9 +9185,9 @@ async function runWitnessVerification(config) {
|
|
|
8862
9185
|
continue;
|
|
8863
9186
|
}
|
|
8864
9187
|
const moduleName = `Witness_${subsystem}_${idx++}`;
|
|
8865
|
-
const witnessTla =
|
|
8866
|
-
fs4.writeFileSync(witnessTla, buildWitnessModule2(moduleName,
|
|
8867
|
-
fs4.writeFileSync(
|
|
9188
|
+
const witnessTla = path5.join(location.dir, `${moduleName}.tla`);
|
|
9189
|
+
fs4.writeFileSync(witnessTla, buildWitnessModule2(moduleName, location.module, tlaPredicate, { bare: location.custom }));
|
|
9190
|
+
fs4.writeFileSync(path5.join(location.dir, `${moduleName}.cfg`), buildWitnessCfg2(fs4.readFileSync(location.cfgPath, "utf8")));
|
|
8868
9191
|
const polarityTag = polarity === "forbidden" ? color(" [forbidden — must be unreachable]", COLORS.gray) : "";
|
|
8869
9192
|
console.log(color(`⚙️ ${id}`, COLORS.blue) + polarityTag);
|
|
8870
9193
|
console.log(color(` ${subsystem} ⊨ ${w.predicate}`, COLORS.gray));
|
|
@@ -8895,7 +9218,7 @@ async function runWitnessVerification(config) {
|
|
|
8895
9218
|
note: tlc.error ?? tlc.violation?.name ?? "TLC error"
|
|
8896
9219
|
});
|
|
8897
9220
|
console.log(color(` ! error — ${tlc.error ?? "see log"}`, COLORS.yellow));
|
|
8898
|
-
fs4.writeFileSync(
|
|
9221
|
+
fs4.writeFileSync(path5.join(location.dir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8899
9222
|
continue;
|
|
8900
9223
|
}
|
|
8901
9224
|
const verdict = witnessVerdict2(polarity, reachable);
|
|
@@ -8910,7 +9233,7 @@ async function runWitnessVerification(config) {
|
|
|
8910
9233
|
});
|
|
8911
9234
|
console.log(color(` ${verdict.ok ? "✓" : "✗"} ${verdict.status} — ${verdict.message}`, verdict.ok ? COLORS.green : COLORS.red));
|
|
8912
9235
|
if (!verdict.ok)
|
|
8913
|
-
fs4.writeFileSync(
|
|
9236
|
+
fs4.writeFileSync(path5.join(location.dir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8914
9237
|
}
|
|
8915
9238
|
displayWitnessReport(results);
|
|
8916
9239
|
}
|
|
@@ -9002,7 +9325,7 @@ function displayCompositionalReport(results, nonInterferenceValid) {
|
|
|
9002
9325
|
console.log();
|
|
9003
9326
|
}
|
|
9004
9327
|
async function loadVerificationConfig(configPath) {
|
|
9005
|
-
const resolvedPath =
|
|
9328
|
+
const resolvedPath = path5.resolve(configPath);
|
|
9006
9329
|
const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
|
|
9007
9330
|
return configModule.verificationConfig || configModule.default;
|
|
9008
9331
|
}
|
|
@@ -9024,23 +9347,23 @@ async function generateAndWriteTLASpecs(config, analysis) {
|
|
|
9024
9347
|
const { generateTLA: generateTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
9025
9348
|
console.log(color("\uD83D\uDCDD Generating TLA+ specification...", COLORS.blue));
|
|
9026
9349
|
const { spec, cfg } = await generateTLA2(config, analysis);
|
|
9027
|
-
const specDir =
|
|
9350
|
+
const specDir = path5.join(process.cwd(), "specs", "tla", "generated");
|
|
9028
9351
|
if (!fs4.existsSync(specDir)) {
|
|
9029
9352
|
fs4.mkdirSync(specDir, { recursive: true });
|
|
9030
9353
|
}
|
|
9031
|
-
const specPath =
|
|
9032
|
-
const cfgPath =
|
|
9354
|
+
const specPath = path5.join(specDir, "UserApp.tla");
|
|
9355
|
+
const cfgPath = path5.join(specDir, "UserApp.cfg");
|
|
9033
9356
|
fs4.writeFileSync(specPath, spec);
|
|
9034
9357
|
fs4.writeFileSync(cfgPath, cfg);
|
|
9035
9358
|
return { specPath, specDir };
|
|
9036
9359
|
}
|
|
9037
9360
|
function findAndCopyBaseSpec(specDir) {
|
|
9038
9361
|
const possiblePaths = [
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9362
|
+
path5.join(process.cwd(), "specs", "tla", "MessageRouter.tla"),
|
|
9363
|
+
path5.join(__dirname, "..", "specs", "tla", "MessageRouter.tla"),
|
|
9364
|
+
path5.join(__dirname, "..", "..", "specs", "tla", "MessageRouter.tla"),
|
|
9365
|
+
path5.join(process.cwd(), "external", "polly", "packages", "verify", "specs", "tla", "MessageRouter.tla"),
|
|
9366
|
+
path5.join(process.cwd(), "node_modules", "@fairfox", "polly-verify", "specs", "tla", "MessageRouter.tla")
|
|
9044
9367
|
];
|
|
9045
9368
|
let baseSpecPath = null;
|
|
9046
9369
|
for (const candidatePath of possiblePaths) {
|
|
@@ -9050,7 +9373,7 @@ function findAndCopyBaseSpec(specDir) {
|
|
|
9050
9373
|
}
|
|
9051
9374
|
}
|
|
9052
9375
|
if (baseSpecPath) {
|
|
9053
|
-
const destSpecPath =
|
|
9376
|
+
const destSpecPath = path5.join(specDir, "MessageRouter.tla");
|
|
9054
9377
|
fs4.copyFileSync(baseSpecPath, destSpecPath);
|
|
9055
9378
|
console.log(color("✓ Copied MessageRouter.tla", COLORS.green));
|
|
9056
9379
|
} else {
|
|
@@ -9063,13 +9386,13 @@ function findAndCopyBaseSpec(specDir) {
|
|
|
9063
9386
|
}
|
|
9064
9387
|
function findMeshSeedSpecDir() {
|
|
9065
9388
|
const candidates = [
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9389
|
+
path5.join(process.cwd(), "specs", "tla"),
|
|
9390
|
+
path5.join(__dirname, "..", "specs", "tla"),
|
|
9391
|
+
path5.join(__dirname, "..", "..", "specs", "tla"),
|
|
9392
|
+
path5.join(process.cwd(), "node_modules", "@fairfox", "polly-verify", "specs", "tla")
|
|
9070
9393
|
];
|
|
9071
9394
|
for (const dir of candidates) {
|
|
9072
|
-
if (fs4.existsSync(
|
|
9395
|
+
if (fs4.existsSync(path5.join(dir, "MeshSeed.tla")))
|
|
9073
9396
|
return dir;
|
|
9074
9397
|
}
|
|
9075
9398
|
return null;
|
|
@@ -9083,11 +9406,11 @@ async function runMeshSeedGuard(docker, specDir, config) {
|
|
|
9083
9406
|
return;
|
|
9084
9407
|
}
|
|
9085
9408
|
const fixDisabled = isSeedFixDisabled();
|
|
9086
|
-
fs4.copyFileSync(
|
|
9087
|
-
const baseCfg = fs4.readFileSync(
|
|
9088
|
-
fs4.writeFileSync(
|
|
9409
|
+
fs4.copyFileSync(path5.join(sourceDir, "MeshSeed.tla"), path5.join(specDir, "MeshSeed.tla"));
|
|
9410
|
+
const baseCfg = fs4.readFileSync(path5.join(sourceDir, "MeshSeed.cfg"), "utf8");
|
|
9411
|
+
fs4.writeFileSync(path5.join(specDir, "MeshSeed.cfg"), meshSeedCfg(baseCfg, { disableFix: fixDisabled }));
|
|
9089
9412
|
console.log(color(`⚙️ Running mesh seed-race guard (MeshSeed.tla, SeedDeterministic = ${fixDisabled ? "FALSE" : "TRUE"})...`, COLORS.blue));
|
|
9090
|
-
return docker.runTLC(
|
|
9413
|
+
return docker.runTLC(path5.join(specDir, "MeshSeed.tla"), { workers: 1 });
|
|
9091
9414
|
}
|
|
9092
9415
|
async function setupDocker() {
|
|
9093
9416
|
const { DockerRunner: DockerRunner2 } = await Promise.resolve().then(() => (init_docker(), exports_docker));
|
|
@@ -9169,8 +9492,8 @@ function displayVerificationResults(result, specDir) {
|
|
|
9169
9492
|
}
|
|
9170
9493
|
console.log();
|
|
9171
9494
|
console.log(color("Full output saved to:", COLORS.gray));
|
|
9172
|
-
console.log(color(` ${
|
|
9173
|
-
fs4.writeFileSync(
|
|
9495
|
+
console.log(color(` ${path5.join(specDir, "tlc-output.log")}`, COLORS.gray));
|
|
9496
|
+
fs4.writeFileSync(path5.join(specDir, "tlc-output.log"), result.output);
|
|
9174
9497
|
process.exit(1);
|
|
9175
9498
|
}
|
|
9176
9499
|
function showHelp() {
|
|
@@ -9233,8 +9556,8 @@ ${color("Learn More:", COLORS.blue)}
|
|
|
9233
9556
|
}
|
|
9234
9557
|
function findTsConfig() {
|
|
9235
9558
|
const locations = [
|
|
9236
|
-
|
|
9237
|
-
|
|
9559
|
+
path5.join(process.cwd(), "tsconfig.json"),
|
|
9560
|
+
path5.join(process.cwd(), "packages", "web-ext", "tsconfig.json")
|
|
9238
9561
|
];
|
|
9239
9562
|
for (const loc of locations) {
|
|
9240
9563
|
if (fs4.existsSync(loc)) {
|
|
@@ -9245,9 +9568,9 @@ function findTsConfig() {
|
|
|
9245
9568
|
}
|
|
9246
9569
|
function findStateFile() {
|
|
9247
9570
|
const locations = [
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9571
|
+
path5.join(process.cwd(), "types", "state.ts"),
|
|
9572
|
+
path5.join(process.cwd(), "src", "types", "state.ts"),
|
|
9573
|
+
path5.join(process.cwd(), "packages", "web-ext", "src", "shared", "state", "app-state.ts")
|
|
9251
9574
|
];
|
|
9252
9575
|
for (const loc of locations) {
|
|
9253
9576
|
if (fs4.existsSync(loc)) {
|
|
@@ -9261,4 +9584,4 @@ main().catch((error) => {
|
|
|
9261
9584
|
process.exit(1);
|
|
9262
9585
|
});
|
|
9263
9586
|
|
|
9264
|
-
//# debugId=
|
|
9587
|
+
//# debugId=F55B4EE81F3A37C864756E2164756E21
|