@fairfox/polly 0.84.0 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/verify/src/cli.js +392 -91
- 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;
|
|
@@ -3467,11 +3537,22 @@ function headerLine(cfg, header) {
|
|
|
3467
3537
|
return null;
|
|
3468
3538
|
}
|
|
3469
3539
|
function buildWitnessCfg(baseCfg) {
|
|
3470
|
-
const spec = headerLine(baseCfg, "SPECIFICATION")
|
|
3540
|
+
const spec = headerLine(baseCfg, "SPECIFICATION");
|
|
3541
|
+
const init = headerLine(baseCfg, "INIT");
|
|
3542
|
+
const next = headerLine(baseCfg, "NEXT");
|
|
3543
|
+
const behaviour = spec ? [spec] : init && next ? [init, next] : ["SPECIFICATION UserSpec"];
|
|
3471
3544
|
const constants = sectionBody(baseCfg, "CONSTANTS");
|
|
3472
3545
|
const constraint = sectionBody(baseCfg, "CONSTRAINT");
|
|
3473
3546
|
const symmetry = sectionBody(baseCfg, "SYMMETRY");
|
|
3474
|
-
const out = [
|
|
3547
|
+
const out = [
|
|
3548
|
+
...behaviour,
|
|
3549
|
+
"",
|
|
3550
|
+
"CONSTANTS",
|
|
3551
|
+
...constants,
|
|
3552
|
+
"",
|
|
3553
|
+
"INVARIANTS",
|
|
3554
|
+
` ${WITNESS_INVARIANT}`
|
|
3555
|
+
];
|
|
3475
3556
|
if (constraint.length > 0)
|
|
3476
3557
|
out.push("", "CONSTRAINT", ...constraint);
|
|
3477
3558
|
if (symmetry.length > 0)
|
|
@@ -3490,7 +3571,7 @@ function routeWitness(fields, subsystems) {
|
|
|
3490
3571
|
const only = owners.length === 1 ? owners[0] : undefined;
|
|
3491
3572
|
return only ? only[0] : null;
|
|
3492
3573
|
}
|
|
3493
|
-
var WITNESS_INVARIANT = "WitnessReachable", WitnessTranslationError, COMPARATORS
|
|
3574
|
+
var WITNESS_INVARIANT = "WitnessReachable", WitnessTranslationError, COMPARATORS, contextFieldRenderer = (fieldPath) => `contextStates[ctx].${flattenField(fieldPath)}`;
|
|
3494
3575
|
var init_witness2 = __esm(() => {
|
|
3495
3576
|
WitnessTranslationError = class WitnessTranslationError extends Error {
|
|
3496
3577
|
};
|
|
@@ -3513,11 +3594,11 @@ __export(exports_docker, {
|
|
|
3513
3594
|
});
|
|
3514
3595
|
import { spawn } from "node:child_process";
|
|
3515
3596
|
import * as fs3 from "node:fs";
|
|
3516
|
-
import * as
|
|
3597
|
+
import * as path4 from "node:path";
|
|
3517
3598
|
|
|
3518
3599
|
class DockerRunner {
|
|
3519
3600
|
IMAGE_NAME = "polly-tla:latest";
|
|
3520
|
-
DOCKERFILE_PATH =
|
|
3601
|
+
DOCKERFILE_PATH = path4.join(__dirname, "../../Dockerfile");
|
|
3521
3602
|
async isDockerAvailable() {
|
|
3522
3603
|
try {
|
|
3523
3604
|
const result = await this.runCommand("docker", ["info"], {
|
|
@@ -3545,7 +3626,7 @@ class DockerRunner {
|
|
|
3545
3626
|
}
|
|
3546
3627
|
}
|
|
3547
3628
|
async buildImage(onProgress) {
|
|
3548
|
-
const dockerfileDir =
|
|
3629
|
+
const dockerfileDir = path4.dirname(this.DOCKERFILE_PATH);
|
|
3549
3630
|
await this.runCommandStreaming("docker", ["build", "-f", this.DOCKERFILE_PATH, "-t", this.IMAGE_NAME, dockerfileDir], onProgress, 300000);
|
|
3550
3631
|
}
|
|
3551
3632
|
async pullImage(onProgress) {
|
|
@@ -3555,13 +3636,13 @@ class DockerRunner {
|
|
|
3555
3636
|
if (!fs3.existsSync(specPath)) {
|
|
3556
3637
|
throw new Error(`Spec file not found: ${specPath}`);
|
|
3557
3638
|
}
|
|
3558
|
-
const specDir =
|
|
3559
|
-
const specName =
|
|
3560
|
-
const cfgPath =
|
|
3639
|
+
const specDir = path4.dirname(specPath);
|
|
3640
|
+
const specName = path4.basename(specPath, ".tla");
|
|
3641
|
+
const cfgPath = path4.join(specDir, `${specName}.cfg`);
|
|
3561
3642
|
if (!fs3.existsSync(cfgPath)) {
|
|
3562
3643
|
throw new Error(`Config file not found: ${cfgPath}`);
|
|
3563
3644
|
}
|
|
3564
|
-
const statesDir =
|
|
3645
|
+
const statesDir = path4.join(specDir, "states");
|
|
3565
3646
|
if (fs3.existsSync(statesDir)) {
|
|
3566
3647
|
fs3.rmSync(statesDir, { recursive: true, force: true });
|
|
3567
3648
|
}
|
|
@@ -3654,7 +3735,7 @@ class DockerRunner {
|
|
|
3654
3735
|
return "Unknown error occurred during model checking";
|
|
3655
3736
|
}
|
|
3656
3737
|
runCommand(command, args, options) {
|
|
3657
|
-
return new Promise((
|
|
3738
|
+
return new Promise((resolve3, reject) => {
|
|
3658
3739
|
const proc = spawn(command, args);
|
|
3659
3740
|
let stdout = "";
|
|
3660
3741
|
let stderr = "";
|
|
@@ -3672,7 +3753,7 @@ class DockerRunner {
|
|
|
3672
3753
|
proc.on("close", (exitCode) => {
|
|
3673
3754
|
if (timeout)
|
|
3674
3755
|
clearTimeout(timeout);
|
|
3675
|
-
|
|
3756
|
+
resolve3({
|
|
3676
3757
|
exitCode: exitCode || 0,
|
|
3677
3758
|
stdout,
|
|
3678
3759
|
stderr
|
|
@@ -3686,7 +3767,7 @@ class DockerRunner {
|
|
|
3686
3767
|
});
|
|
3687
3768
|
}
|
|
3688
3769
|
runCommandStreaming(command, args, onOutput, timeout) {
|
|
3689
|
-
return new Promise((
|
|
3770
|
+
return new Promise((resolve3, reject) => {
|
|
3690
3771
|
const proc = spawn(command, args);
|
|
3691
3772
|
const timeoutHandle = timeout && timeout > 0 ? setTimeout(() => {
|
|
3692
3773
|
proc.kill();
|
|
@@ -3718,7 +3799,7 @@ class DockerRunner {
|
|
|
3718
3799
|
if (timeoutHandle)
|
|
3719
3800
|
clearTimeout(timeoutHandle);
|
|
3720
3801
|
if (exitCode === 0) {
|
|
3721
|
-
|
|
3802
|
+
resolve3();
|
|
3722
3803
|
} else {
|
|
3723
3804
|
reject(new Error(`Command failed with exit code ${exitCode}`));
|
|
3724
3805
|
}
|
|
@@ -3737,7 +3818,7 @@ var init_docker = () => {};
|
|
|
3737
3818
|
// tools/verify/src/cli.ts
|
|
3738
3819
|
init_expression_validator();
|
|
3739
3820
|
import * as fs4 from "node:fs";
|
|
3740
|
-
import * as
|
|
3821
|
+
import * as path5 from "node:path";
|
|
3741
3822
|
|
|
3742
3823
|
// tools/verify/src/analysis/mesh-signal-warnings.ts
|
|
3743
3824
|
function computeMeshOrPeerSignalFindings(analysis, declaredMeshDocs) {
|
|
@@ -5464,6 +5545,7 @@ class HandlerExtractor {
|
|
|
5464
5545
|
warnings;
|
|
5465
5546
|
currentFunctionParams = [];
|
|
5466
5547
|
contextOverrides;
|
|
5548
|
+
onSurfaceSpans = [];
|
|
5467
5549
|
constructor(tsConfigPath, contextOverrides) {
|
|
5468
5550
|
this.project = new Project({
|
|
5469
5551
|
tsConfigFilePath: tsConfigPath
|
|
@@ -5523,6 +5605,7 @@ class HandlerExtractor {
|
|
|
5523
5605
|
const meshOrPeerSignals = [];
|
|
5524
5606
|
const resources = [];
|
|
5525
5607
|
this.warnings = [];
|
|
5608
|
+
this.onSurfaceSpans = [];
|
|
5526
5609
|
const allSourceFiles = this.project.getSourceFiles();
|
|
5527
5610
|
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
5528
5611
|
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
@@ -5557,6 +5640,30 @@ class HandlerExtractor {
|
|
|
5557
5640
|
continue;
|
|
5558
5641
|
meshOrPeerSignals.push(...this.extractMeshOrPeerSignalsFromFile(sourceFile));
|
|
5559
5642
|
}
|
|
5643
|
+
const stateSignalNames = new Set;
|
|
5644
|
+
for (const filePath of this.analyzedFiles) {
|
|
5645
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
5646
|
+
if (!sourceFile)
|
|
5647
|
+
continue;
|
|
5648
|
+
for (const name of this.extractStateSignalVariableNames(sourceFile)) {
|
|
5649
|
+
stateSignalNames.add(name);
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
for (const v of verifiedStates)
|
|
5653
|
+
stateSignalNames.add(v.variableName);
|
|
5654
|
+
for (const m of meshOrPeerSignals)
|
|
5655
|
+
stateSignalNames.add(m.variableName);
|
|
5656
|
+
const offSurfaceMutations = [];
|
|
5657
|
+
if (stateSignalNames.size > 0) {
|
|
5658
|
+
for (const filePath of this.analyzedFiles) {
|
|
5659
|
+
if (!this.isOffSurfaceScannable(filePath))
|
|
5660
|
+
continue;
|
|
5661
|
+
const sourceFile = this.project.getSourceFile(filePath);
|
|
5662
|
+
if (!sourceFile)
|
|
5663
|
+
continue;
|
|
5664
|
+
offSurfaceMutations.push(...this.findOffSurfaceMutations(sourceFile, stateSignalNames));
|
|
5665
|
+
}
|
|
5666
|
+
}
|
|
5560
5667
|
this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
|
|
5561
5668
|
this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
|
|
5562
5669
|
return {
|
|
@@ -5567,6 +5674,7 @@ class HandlerExtractor {
|
|
|
5567
5674
|
verifiedStates,
|
|
5568
5675
|
meshOrPeerSignals,
|
|
5569
5676
|
resources,
|
|
5677
|
+
offSurfaceMutations,
|
|
5570
5678
|
warnings: this.warnings
|
|
5571
5679
|
};
|
|
5572
5680
|
}
|
|
@@ -5977,6 +6085,7 @@ class HandlerExtractor {
|
|
|
5977
6085
|
return false;
|
|
5978
6086
|
}
|
|
5979
6087
|
extractAssignments(funcNode, assignments) {
|
|
6088
|
+
this.recordOnSurfaceSpan(funcNode);
|
|
5980
6089
|
funcNode.forEachDescendant((node) => {
|
|
5981
6090
|
if (Node2.isBinaryExpression(node)) {
|
|
5982
6091
|
this.extractBinaryExpressionAssignment(node, assignments);
|
|
@@ -7563,6 +7672,7 @@ class HandlerExtractor {
|
|
|
7563
7672
|
}
|
|
7564
7673
|
findStateMutationsInFunction(func, stateVarNames) {
|
|
7565
7674
|
const mutations = [];
|
|
7675
|
+
this.recordOnSurfaceSpan(func);
|
|
7566
7676
|
func.forEachDescendant((node) => {
|
|
7567
7677
|
if (!Node2.isBinaryExpression(node))
|
|
7568
7678
|
return;
|
|
@@ -7592,6 +7702,140 @@ class HandlerExtractor {
|
|
|
7592
7702
|
});
|
|
7593
7703
|
return mutations;
|
|
7594
7704
|
}
|
|
7705
|
+
isOffSurfaceScannable(filePath) {
|
|
7706
|
+
return !/(?:\.(?:test|spec|stories)\.[cm]?[jt]sx?$)|(?:\/(?:__tests__|tests|test|features|e2e|stories|__mocks__)\/)/i.test(filePath);
|
|
7707
|
+
}
|
|
7708
|
+
static STATE_SIGNAL_FACTORIES = new Set([
|
|
7709
|
+
"$state",
|
|
7710
|
+
"$sharedState",
|
|
7711
|
+
"$syncedState",
|
|
7712
|
+
"$persistedState",
|
|
7713
|
+
"$meshState",
|
|
7714
|
+
"$peerState"
|
|
7715
|
+
]);
|
|
7716
|
+
extractStateSignalVariableNames(sourceFile) {
|
|
7717
|
+
const names = [];
|
|
7718
|
+
sourceFile.forEachDescendant((node) => {
|
|
7719
|
+
if (!Node2.isCallExpression(node))
|
|
7720
|
+
return;
|
|
7721
|
+
const expr = node.getExpression();
|
|
7722
|
+
if (!Node2.isIdentifier(expr))
|
|
7723
|
+
return;
|
|
7724
|
+
if (!HandlerExtractor.STATE_SIGNAL_FACTORIES.has(expr.getText()))
|
|
7725
|
+
return;
|
|
7726
|
+
const varName = this.getVariableNameFromParent(node);
|
|
7727
|
+
if (varName)
|
|
7728
|
+
names.push(varName);
|
|
7729
|
+
});
|
|
7730
|
+
return names;
|
|
7731
|
+
}
|
|
7732
|
+
recordOnSurfaceSpan(node) {
|
|
7733
|
+
this.onSurfaceSpans.push({
|
|
7734
|
+
file: node.getSourceFile().getFilePath(),
|
|
7735
|
+
start: node.getStart(),
|
|
7736
|
+
end: node.getEnd()
|
|
7737
|
+
});
|
|
7738
|
+
}
|
|
7739
|
+
isWithinOnSurfaceSpan(file, pos) {
|
|
7740
|
+
for (const span of this.onSurfaceSpans) {
|
|
7741
|
+
if (span.file === file && pos >= span.start && pos <= span.end)
|
|
7742
|
+
return true;
|
|
7743
|
+
}
|
|
7744
|
+
return false;
|
|
7745
|
+
}
|
|
7746
|
+
findOffSurfaceMutations(sourceFile, stateVarNames) {
|
|
7747
|
+
const out = [];
|
|
7748
|
+
const filePath = sourceFile.getFilePath();
|
|
7749
|
+
sourceFile.forEachDescendant((node) => {
|
|
7750
|
+
if (Node2.isBinaryExpression(node)) {
|
|
7751
|
+
out.push(...this.offSurfaceWritesAt(node, stateVarNames, filePath));
|
|
7752
|
+
}
|
|
7753
|
+
});
|
|
7754
|
+
return out;
|
|
7755
|
+
}
|
|
7756
|
+
offSurfaceWritesAt(node, stateVarNames, filePath) {
|
|
7757
|
+
if (node.getOperatorToken().getText() !== "=")
|
|
7758
|
+
return [];
|
|
7759
|
+
const left = node.getLeft();
|
|
7760
|
+
if (!Node2.isPropertyAccessExpression(left))
|
|
7761
|
+
return [];
|
|
7762
|
+
const match = this.matchVerifiedStateWrite(this.getPropertyPath(left), stateVarNames);
|
|
7763
|
+
if (!match)
|
|
7764
|
+
return [];
|
|
7765
|
+
if (this.isWithinOnSurfaceSpan(filePath, left.getStart()))
|
|
7766
|
+
return [];
|
|
7767
|
+
const base = {
|
|
7768
|
+
signalVariable: match.signal,
|
|
7769
|
+
functionName: this.enclosingFunctionName(node),
|
|
7770
|
+
filePath,
|
|
7771
|
+
line: node.getStartLineNumber()
|
|
7772
|
+
};
|
|
7773
|
+
if (match.field !== undefined) {
|
|
7774
|
+
return [{ field: `${match.signal}_${match.field}`, ...base }];
|
|
7775
|
+
}
|
|
7776
|
+
const fields = this.objectLiteralFieldNames(node.getRight());
|
|
7777
|
+
if (fields.length === 0)
|
|
7778
|
+
return [{ field: match.signal, ...base }];
|
|
7779
|
+
return fields.map((f) => ({ field: `${match.signal}_${f}`, ...base }));
|
|
7780
|
+
}
|
|
7781
|
+
matchVerifiedStateWrite(path2, stateVarNames) {
|
|
7782
|
+
for (const varName of stateVarNames) {
|
|
7783
|
+
if (path2 === `${varName}.value`)
|
|
7784
|
+
return { signal: varName };
|
|
7785
|
+
const prefix = `${varName}.value.`;
|
|
7786
|
+
if (path2.startsWith(prefix))
|
|
7787
|
+
return { signal: varName, field: path2.substring(prefix.length) };
|
|
7788
|
+
}
|
|
7789
|
+
return null;
|
|
7790
|
+
}
|
|
7791
|
+
objectLiteralFieldNames(right) {
|
|
7792
|
+
if (!Node2.isObjectLiteralExpression(right))
|
|
7793
|
+
return [];
|
|
7794
|
+
const names = [];
|
|
7795
|
+
for (const prop of right.getProperties()) {
|
|
7796
|
+
if (Node2.isPropertyAssignment(prop) || Node2.isShorthandPropertyAssignment(prop)) {
|
|
7797
|
+
names.push(prop.getName());
|
|
7798
|
+
}
|
|
7799
|
+
}
|
|
7800
|
+
return names;
|
|
7801
|
+
}
|
|
7802
|
+
enclosingFunctionName(node) {
|
|
7803
|
+
let current = node.getParent();
|
|
7804
|
+
while (current) {
|
|
7805
|
+
const name = this.namedScope(current);
|
|
7806
|
+
if (name !== null)
|
|
7807
|
+
return name;
|
|
7808
|
+
current = current.getParent();
|
|
7809
|
+
}
|
|
7810
|
+
return "<module>";
|
|
7811
|
+
}
|
|
7812
|
+
namedScope(node) {
|
|
7813
|
+
if (Node2.isFunctionDeclaration(node)) {
|
|
7814
|
+
return node.getName() ?? "<anonymous function>";
|
|
7815
|
+
}
|
|
7816
|
+
if (Node2.isMethodDeclaration(node)) {
|
|
7817
|
+
const clsName = node.getFirstAncestorByKind(SyntaxKind.ClassDeclaration)?.getName();
|
|
7818
|
+
const methodName = node.getName();
|
|
7819
|
+
return clsName ? `${clsName}.${methodName}` : methodName;
|
|
7820
|
+
}
|
|
7821
|
+
if (Node2.isGetAccessorDeclaration(node) || Node2.isSetAccessorDeclaration(node)) {
|
|
7822
|
+
return node.getName();
|
|
7823
|
+
}
|
|
7824
|
+
if (Node2.isArrowFunction(node) || Node2.isFunctionExpression(node)) {
|
|
7825
|
+
return this.boundFunctionName(node) ?? null;
|
|
7826
|
+
}
|
|
7827
|
+
return null;
|
|
7828
|
+
}
|
|
7829
|
+
boundFunctionName(fn) {
|
|
7830
|
+
const parent = fn.getParent();
|
|
7831
|
+
if (!parent)
|
|
7832
|
+
return;
|
|
7833
|
+
if (Node2.isVariableDeclaration(parent))
|
|
7834
|
+
return parent.getName();
|
|
7835
|
+
if (Node2.isPropertyAssignment(parent))
|
|
7836
|
+
return parent.getName();
|
|
7837
|
+
return;
|
|
7838
|
+
}
|
|
7595
7839
|
functionNameToMessageType(funcName) {
|
|
7596
7840
|
let name = funcName.replace(/^handle/, "").replace(/^on/, "").replace(/^set/, "Set").replace(/^update/, "Update").replace(/^do/, "");
|
|
7597
7841
|
if (name.length > 0) {
|
|
@@ -7756,7 +8000,8 @@ class TypeExtractor {
|
|
|
7756
8000
|
globalStateConstraints: handlerAnalysis.globalStateConstraints,
|
|
7757
8001
|
verifiedStates: handlerAnalysis.verifiedStates,
|
|
7758
8002
|
meshOrPeerSignals: handlerAnalysis.meshOrPeerSignals,
|
|
7759
|
-
resources: handlerAnalysis.resources
|
|
8003
|
+
resources: handlerAnalysis.resources,
|
|
8004
|
+
offSurfaceMutations: handlerAnalysis.offSurfaceMutations
|
|
7760
8005
|
};
|
|
7761
8006
|
}
|
|
7762
8007
|
extractHandlerAnalysis() {
|
|
@@ -8227,7 +8472,7 @@ async function setupCommand() {
|
|
|
8227
8472
|
displayAnalysisResults(analysis);
|
|
8228
8473
|
displayAnalysisSummary(analysis);
|
|
8229
8474
|
const configContent = generateConfig(analysis);
|
|
8230
|
-
const configPath =
|
|
8475
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8231
8476
|
writeConfigFile(configPath, configContent);
|
|
8232
8477
|
displaySetupSuccess(configPath);
|
|
8233
8478
|
} catch (_error) {
|
|
@@ -8275,7 +8520,7 @@ function getFieldStatus(confidence) {
|
|
|
8275
8520
|
return color("⚠ Manual config", COLORS.red);
|
|
8276
8521
|
}
|
|
8277
8522
|
function writeConfigFile(configPath, configContent) {
|
|
8278
|
-
const configDir =
|
|
8523
|
+
const configDir = path5.dirname(configPath);
|
|
8279
8524
|
if (!fs4.existsSync(configDir)) {
|
|
8280
8525
|
fs4.mkdirSync(configDir, { recursive: true });
|
|
8281
8526
|
}
|
|
@@ -8298,7 +8543,7 @@ function displaySetupSuccess(configPath) {
|
|
|
8298
8543
|
console.log();
|
|
8299
8544
|
}
|
|
8300
8545
|
async function validateCommand() {
|
|
8301
|
-
const configPath =
|
|
8546
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8302
8547
|
console.log(color(`
|
|
8303
8548
|
\uD83D\uDD0D Validating configuration...
|
|
8304
8549
|
`, COLORS.blue));
|
|
@@ -8319,7 +8564,7 @@ async function validateCommand() {
|
|
|
8319
8564
|
process.exit(1);
|
|
8320
8565
|
}
|
|
8321
8566
|
async function estimateCommand() {
|
|
8322
|
-
const configPath =
|
|
8567
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8323
8568
|
console.log(color(`
|
|
8324
8569
|
\uD83D\uDCCA Estimating state space...
|
|
8325
8570
|
`, COLORS.blue));
|
|
@@ -8462,8 +8707,8 @@ function isWitnessMode() {
|
|
|
8462
8707
|
return process.argv.includes("--witness");
|
|
8463
8708
|
}
|
|
8464
8709
|
function displayModelCoverage(report) {
|
|
8465
|
-
const { unwrittenFields, unconstrainedMutators, fieldCoverage } = report;
|
|
8466
|
-
if (unwrittenFields.length === 0 && unconstrainedMutators.length === 0) {
|
|
8710
|
+
const { unwrittenFields, unconstrainedMutators, fieldCoverage, offSurfaceMutations } = report;
|
|
8711
|
+
if (unwrittenFields.length === 0 && unconstrainedMutators.length === 0 && offSurfaceMutations.length === 0) {
|
|
8467
8712
|
console.log(color(`✓ Model coverage: all ${fieldCoverage.length} declared field(s) written by a modelled handler`, COLORS.green));
|
|
8468
8713
|
console.log();
|
|
8469
8714
|
return;
|
|
@@ -8487,11 +8732,24 @@ function displayModelCoverage(report) {
|
|
|
8487
8732
|
console.log(color(" The checker explores these transitions but asserts nothing about their effect.", COLORS.gray));
|
|
8488
8733
|
console.log();
|
|
8489
8734
|
}
|
|
8735
|
+
if (offSurfaceMutations.length > 0) {
|
|
8736
|
+
console.log(color(`
|
|
8737
|
+
⚠️ ${offSurfaceMutations.length} declared state field write(s) outside any modelled transition (polly#163):`, COLORS.yellow));
|
|
8738
|
+
for (const m of offSurfaceMutations) {
|
|
8739
|
+
console.log(color(` • ${m.field} mutated in ${m.function}() — ${m.file}:${m.line}`, COLORS.yellow));
|
|
8740
|
+
}
|
|
8741
|
+
console.log(color(" A non-dispatched path (a method, a non-exported function, a closure) writes", COLORS.gray));
|
|
8742
|
+
console.log(color(" verified state the checker never explores — the register() shape (#160). Even", COLORS.gray));
|
|
8743
|
+
console.log(color(" when a handler also writes the field, model coverage cannot see this writer.", COLORS.gray));
|
|
8744
|
+
console.log(color(" Route the change through a dispatched handler, or drop the field from the", COLORS.gray));
|
|
8745
|
+
console.log(color(" verified surface. See tools/verify/OFF-SURFACE-MUTATORS.md.", COLORS.gray));
|
|
8746
|
+
console.log();
|
|
8747
|
+
}
|
|
8490
8748
|
}
|
|
8491
8749
|
async function runModelCoverage(typedConfig, typedAnalysis, meshFindingCount) {
|
|
8492
8750
|
const { computeModelCoverage: computeModelCoverage2, strictCoverageReasons: strictCoverageReasons2 } = await Promise.resolve().then(() => exports_model_coverage);
|
|
8493
8751
|
const stateFields = Object.keys(typedConfig.state ?? {});
|
|
8494
|
-
const coverage = computeModelCoverage2(stateFields, typedAnalysis.handlers);
|
|
8752
|
+
const coverage = computeModelCoverage2(stateFields, typedAnalysis.handlers, typedAnalysis.offSurfaceMutations ?? []);
|
|
8495
8753
|
displayModelCoverage(coverage);
|
|
8496
8754
|
if (!isStrictMode())
|
|
8497
8755
|
return;
|
|
@@ -8513,7 +8771,7 @@ async function runModelCoverage(typedConfig, typedAnalysis, meshFindingCount) {
|
|
|
8513
8771
|
process.exit(1);
|
|
8514
8772
|
}
|
|
8515
8773
|
async function verifyCommand() {
|
|
8516
|
-
const configPath =
|
|
8774
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8517
8775
|
console.log(color(`
|
|
8518
8776
|
\uD83D\uDD0D Running verification...
|
|
8519
8777
|
`, COLORS.blue));
|
|
@@ -8579,6 +8837,12 @@ function getMaxDepth(config) {
|
|
|
8579
8837
|
}
|
|
8580
8838
|
return;
|
|
8581
8839
|
}
|
|
8840
|
+
function getCustomTLAPaths(config) {
|
|
8841
|
+
if ("messages" in config && config.customTLAPaths) {
|
|
8842
|
+
return config.customTLAPaths;
|
|
8843
|
+
}
|
|
8844
|
+
return {};
|
|
8845
|
+
}
|
|
8582
8846
|
async function runCoupledFieldsLint(config, analysis) {
|
|
8583
8847
|
const groups = config.coupledFields ?? [];
|
|
8584
8848
|
if (groups.length === 0)
|
|
@@ -8726,18 +8990,23 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
8726
8990
|
const maxDepth = getMaxDepth(config);
|
|
8727
8991
|
const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
8728
8992
|
const results = [];
|
|
8993
|
+
const customTLAPaths = getCustomTLAPaths(config);
|
|
8729
8994
|
for (const name of subsystemNames) {
|
|
8730
8995
|
const sub = subsystems[name];
|
|
8731
8996
|
const startTime = Date.now();
|
|
8997
|
+
if (customTLAPaths[name]) {
|
|
8998
|
+
console.log(color(`⏭ ${name}: hand-written spec (customTLAPaths) — witnessed, not generated`, COLORS.gray));
|
|
8999
|
+
continue;
|
|
9000
|
+
}
|
|
8732
9001
|
console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
|
|
8733
9002
|
const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
|
|
8734
9003
|
const ensuresCount = (spec.match(/^EnsuresAfter_\w+ ==/gm) ?? []).length;
|
|
8735
|
-
const specDir =
|
|
9004
|
+
const specDir = path5.join(process.cwd(), "specs", "tla", "generated", name);
|
|
8736
9005
|
if (!fs4.existsSync(specDir)) {
|
|
8737
9006
|
fs4.mkdirSync(specDir, { recursive: true });
|
|
8738
9007
|
}
|
|
8739
|
-
const specPath =
|
|
8740
|
-
const cfgPath =
|
|
9008
|
+
const specPath = path5.join(specDir, `UserApp_${name}.tla`);
|
|
9009
|
+
const cfgPath = path5.join(specDir, `UserApp_${name}.cfg`);
|
|
8741
9010
|
fs4.writeFileSync(specPath, spec);
|
|
8742
9011
|
fs4.writeFileSync(cfgPath, cfg);
|
|
8743
9012
|
findAndCopyBaseSpec(specDir);
|
|
@@ -8766,7 +9035,7 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
8766
9035
|
} else if (result.error) {
|
|
8767
9036
|
console.log(color(` Error: ${result.error}`, COLORS.red));
|
|
8768
9037
|
}
|
|
8769
|
-
fs4.writeFileSync(
|
|
9038
|
+
fs4.writeFileSync(path5.join(specDir, "tlc-output.log"), result.output);
|
|
8770
9039
|
}
|
|
8771
9040
|
}
|
|
8772
9041
|
console.log();
|
|
@@ -8798,15 +9067,19 @@ async function runWitnessVerification(config) {
|
|
|
8798
9067
|
}
|
|
8799
9068
|
const { extractWitnesses: extractWitnesses2 } = await Promise.resolve().then(() => (init_witness(), exports_witness));
|
|
8800
9069
|
const {
|
|
9070
|
+
bareFieldRenderer: bareFieldRenderer2,
|
|
8801
9071
|
bddPredicateToTLA: bddPredicateToTLA2,
|
|
8802
9072
|
buildWitnessCfg: buildWitnessCfg2,
|
|
8803
9073
|
buildWitnessModule: buildWitnessModule2,
|
|
9074
|
+
parseModuleName: parseModuleName2,
|
|
8804
9075
|
routeWitness: routeWitness2,
|
|
8805
9076
|
witnessPolarity: witnessPolarity2,
|
|
9077
|
+
witnessSpecLocation: witnessSpecLocation2,
|
|
8806
9078
|
witnessVerdict: witnessVerdict2,
|
|
8807
9079
|
WITNESS_INVARIANT: WITNESS_INVARIANT2
|
|
8808
9080
|
} = await Promise.resolve().then(() => (init_witness2(), exports_witness2));
|
|
8809
9081
|
const subsystems = config.subsystems;
|
|
9082
|
+
const customTLAPaths = getCustomTLAPaths(config);
|
|
8810
9083
|
const witnesses = await extractWitnesses2(featureFiles, stepFiles);
|
|
8811
9084
|
const docker = await setupDocker();
|
|
8812
9085
|
const timeoutSeconds = getTimeout(config);
|
|
@@ -8836,21 +9109,49 @@ async function runWitnessVerification(config) {
|
|
|
8836
9109
|
});
|
|
8837
9110
|
continue;
|
|
8838
9111
|
}
|
|
8839
|
-
const
|
|
8840
|
-
|
|
8841
|
-
if (
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
9112
|
+
const custom = customTLAPaths[subsystem];
|
|
9113
|
+
let location;
|
|
9114
|
+
if (custom) {
|
|
9115
|
+
const tlaAbs = path5.resolve(cwd, custom.tla);
|
|
9116
|
+
const cfgAbs = path5.resolve(cwd, custom.cfg);
|
|
9117
|
+
if (!fs4.existsSync(tlaAbs) || !fs4.existsSync(cfgAbs)) {
|
|
9118
|
+
results.push({
|
|
9119
|
+
id,
|
|
9120
|
+
status: "error",
|
|
9121
|
+
ok: false,
|
|
9122
|
+
subsystem,
|
|
9123
|
+
note: `custom spec missing: ${custom.tla} / ${custom.cfg}`
|
|
9124
|
+
});
|
|
9125
|
+
continue;
|
|
9126
|
+
}
|
|
9127
|
+
const module = custom.module ?? parseModuleName2(fs4.readFileSync(tlaAbs, "utf8"));
|
|
9128
|
+
if (!module) {
|
|
9129
|
+
results.push({
|
|
9130
|
+
id,
|
|
9131
|
+
status: "error",
|
|
9132
|
+
ok: false,
|
|
9133
|
+
subsystem,
|
|
9134
|
+
note: `cannot read MODULE name from ${custom.tla}; set customTLAPaths.${subsystem}.module`
|
|
9135
|
+
});
|
|
9136
|
+
continue;
|
|
9137
|
+
}
|
|
9138
|
+
location = witnessSpecLocation2(cwd, subsystem, custom, module);
|
|
9139
|
+
} else {
|
|
9140
|
+
location = witnessSpecLocation2(cwd, subsystem, undefined, "");
|
|
9141
|
+
if (!fs4.existsSync(location.tlaPath) || !fs4.existsSync(location.cfgPath)) {
|
|
9142
|
+
results.push({
|
|
9143
|
+
id,
|
|
9144
|
+
status: "error",
|
|
9145
|
+
ok: false,
|
|
9146
|
+
subsystem,
|
|
9147
|
+
note: `subsystem spec missing: ${subsystem}`
|
|
9148
|
+
});
|
|
9149
|
+
continue;
|
|
9150
|
+
}
|
|
8850
9151
|
}
|
|
8851
9152
|
let tlaPredicate;
|
|
8852
9153
|
try {
|
|
8853
|
-
tlaPredicate = bddPredicateToTLA2(w.predicate);
|
|
9154
|
+
tlaPredicate = location.custom ? bddPredicateToTLA2(w.predicate, bareFieldRenderer2(custom?.fields)) : bddPredicateToTLA2(w.predicate);
|
|
8854
9155
|
} catch (err) {
|
|
8855
9156
|
results.push({
|
|
8856
9157
|
id,
|
|
@@ -8862,9 +9163,9 @@ async function runWitnessVerification(config) {
|
|
|
8862
9163
|
continue;
|
|
8863
9164
|
}
|
|
8864
9165
|
const moduleName = `Witness_${subsystem}_${idx++}`;
|
|
8865
|
-
const witnessTla =
|
|
8866
|
-
fs4.writeFileSync(witnessTla, buildWitnessModule2(moduleName,
|
|
8867
|
-
fs4.writeFileSync(
|
|
9166
|
+
const witnessTla = path5.join(location.dir, `${moduleName}.tla`);
|
|
9167
|
+
fs4.writeFileSync(witnessTla, buildWitnessModule2(moduleName, location.module, tlaPredicate, { bare: location.custom }));
|
|
9168
|
+
fs4.writeFileSync(path5.join(location.dir, `${moduleName}.cfg`), buildWitnessCfg2(fs4.readFileSync(location.cfgPath, "utf8")));
|
|
8868
9169
|
const polarityTag = polarity === "forbidden" ? color(" [forbidden — must be unreachable]", COLORS.gray) : "";
|
|
8869
9170
|
console.log(color(`⚙️ ${id}`, COLORS.blue) + polarityTag);
|
|
8870
9171
|
console.log(color(` ${subsystem} ⊨ ${w.predicate}`, COLORS.gray));
|
|
@@ -8895,7 +9196,7 @@ async function runWitnessVerification(config) {
|
|
|
8895
9196
|
note: tlc.error ?? tlc.violation?.name ?? "TLC error"
|
|
8896
9197
|
});
|
|
8897
9198
|
console.log(color(` ! error — ${tlc.error ?? "see log"}`, COLORS.yellow));
|
|
8898
|
-
fs4.writeFileSync(
|
|
9199
|
+
fs4.writeFileSync(path5.join(location.dir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8899
9200
|
continue;
|
|
8900
9201
|
}
|
|
8901
9202
|
const verdict = witnessVerdict2(polarity, reachable);
|
|
@@ -8910,7 +9211,7 @@ async function runWitnessVerification(config) {
|
|
|
8910
9211
|
});
|
|
8911
9212
|
console.log(color(` ${verdict.ok ? "✓" : "✗"} ${verdict.status} — ${verdict.message}`, verdict.ok ? COLORS.green : COLORS.red));
|
|
8912
9213
|
if (!verdict.ok)
|
|
8913
|
-
fs4.writeFileSync(
|
|
9214
|
+
fs4.writeFileSync(path5.join(location.dir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8914
9215
|
}
|
|
8915
9216
|
displayWitnessReport(results);
|
|
8916
9217
|
}
|
|
@@ -9002,7 +9303,7 @@ function displayCompositionalReport(results, nonInterferenceValid) {
|
|
|
9002
9303
|
console.log();
|
|
9003
9304
|
}
|
|
9004
9305
|
async function loadVerificationConfig(configPath) {
|
|
9005
|
-
const resolvedPath =
|
|
9306
|
+
const resolvedPath = path5.resolve(configPath);
|
|
9006
9307
|
const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
|
|
9007
9308
|
return configModule.verificationConfig || configModule.default;
|
|
9008
9309
|
}
|
|
@@ -9024,23 +9325,23 @@ async function generateAndWriteTLASpecs(config, analysis) {
|
|
|
9024
9325
|
const { generateTLA: generateTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
9025
9326
|
console.log(color("\uD83D\uDCDD Generating TLA+ specification...", COLORS.blue));
|
|
9026
9327
|
const { spec, cfg } = await generateTLA2(config, analysis);
|
|
9027
|
-
const specDir =
|
|
9328
|
+
const specDir = path5.join(process.cwd(), "specs", "tla", "generated");
|
|
9028
9329
|
if (!fs4.existsSync(specDir)) {
|
|
9029
9330
|
fs4.mkdirSync(specDir, { recursive: true });
|
|
9030
9331
|
}
|
|
9031
|
-
const specPath =
|
|
9032
|
-
const cfgPath =
|
|
9332
|
+
const specPath = path5.join(specDir, "UserApp.tla");
|
|
9333
|
+
const cfgPath = path5.join(specDir, "UserApp.cfg");
|
|
9033
9334
|
fs4.writeFileSync(specPath, spec);
|
|
9034
9335
|
fs4.writeFileSync(cfgPath, cfg);
|
|
9035
9336
|
return { specPath, specDir };
|
|
9036
9337
|
}
|
|
9037
9338
|
function findAndCopyBaseSpec(specDir) {
|
|
9038
9339
|
const possiblePaths = [
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9340
|
+
path5.join(process.cwd(), "specs", "tla", "MessageRouter.tla"),
|
|
9341
|
+
path5.join(__dirname, "..", "specs", "tla", "MessageRouter.tla"),
|
|
9342
|
+
path5.join(__dirname, "..", "..", "specs", "tla", "MessageRouter.tla"),
|
|
9343
|
+
path5.join(process.cwd(), "external", "polly", "packages", "verify", "specs", "tla", "MessageRouter.tla"),
|
|
9344
|
+
path5.join(process.cwd(), "node_modules", "@fairfox", "polly-verify", "specs", "tla", "MessageRouter.tla")
|
|
9044
9345
|
];
|
|
9045
9346
|
let baseSpecPath = null;
|
|
9046
9347
|
for (const candidatePath of possiblePaths) {
|
|
@@ -9050,7 +9351,7 @@ function findAndCopyBaseSpec(specDir) {
|
|
|
9050
9351
|
}
|
|
9051
9352
|
}
|
|
9052
9353
|
if (baseSpecPath) {
|
|
9053
|
-
const destSpecPath =
|
|
9354
|
+
const destSpecPath = path5.join(specDir, "MessageRouter.tla");
|
|
9054
9355
|
fs4.copyFileSync(baseSpecPath, destSpecPath);
|
|
9055
9356
|
console.log(color("✓ Copied MessageRouter.tla", COLORS.green));
|
|
9056
9357
|
} else {
|
|
@@ -9063,13 +9364,13 @@ function findAndCopyBaseSpec(specDir) {
|
|
|
9063
9364
|
}
|
|
9064
9365
|
function findMeshSeedSpecDir() {
|
|
9065
9366
|
const candidates = [
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9367
|
+
path5.join(process.cwd(), "specs", "tla"),
|
|
9368
|
+
path5.join(__dirname, "..", "specs", "tla"),
|
|
9369
|
+
path5.join(__dirname, "..", "..", "specs", "tla"),
|
|
9370
|
+
path5.join(process.cwd(), "node_modules", "@fairfox", "polly-verify", "specs", "tla")
|
|
9070
9371
|
];
|
|
9071
9372
|
for (const dir of candidates) {
|
|
9072
|
-
if (fs4.existsSync(
|
|
9373
|
+
if (fs4.existsSync(path5.join(dir, "MeshSeed.tla")))
|
|
9073
9374
|
return dir;
|
|
9074
9375
|
}
|
|
9075
9376
|
return null;
|
|
@@ -9083,11 +9384,11 @@ async function runMeshSeedGuard(docker, specDir, config) {
|
|
|
9083
9384
|
return;
|
|
9084
9385
|
}
|
|
9085
9386
|
const fixDisabled = isSeedFixDisabled();
|
|
9086
|
-
fs4.copyFileSync(
|
|
9087
|
-
const baseCfg = fs4.readFileSync(
|
|
9088
|
-
fs4.writeFileSync(
|
|
9387
|
+
fs4.copyFileSync(path5.join(sourceDir, "MeshSeed.tla"), path5.join(specDir, "MeshSeed.tla"));
|
|
9388
|
+
const baseCfg = fs4.readFileSync(path5.join(sourceDir, "MeshSeed.cfg"), "utf8");
|
|
9389
|
+
fs4.writeFileSync(path5.join(specDir, "MeshSeed.cfg"), meshSeedCfg(baseCfg, { disableFix: fixDisabled }));
|
|
9089
9390
|
console.log(color(`⚙️ Running mesh seed-race guard (MeshSeed.tla, SeedDeterministic = ${fixDisabled ? "FALSE" : "TRUE"})...`, COLORS.blue));
|
|
9090
|
-
return docker.runTLC(
|
|
9391
|
+
return docker.runTLC(path5.join(specDir, "MeshSeed.tla"), { workers: 1 });
|
|
9091
9392
|
}
|
|
9092
9393
|
async function setupDocker() {
|
|
9093
9394
|
const { DockerRunner: DockerRunner2 } = await Promise.resolve().then(() => (init_docker(), exports_docker));
|
|
@@ -9169,8 +9470,8 @@ function displayVerificationResults(result, specDir) {
|
|
|
9169
9470
|
}
|
|
9170
9471
|
console.log();
|
|
9171
9472
|
console.log(color("Full output saved to:", COLORS.gray));
|
|
9172
|
-
console.log(color(` ${
|
|
9173
|
-
fs4.writeFileSync(
|
|
9473
|
+
console.log(color(` ${path5.join(specDir, "tlc-output.log")}`, COLORS.gray));
|
|
9474
|
+
fs4.writeFileSync(path5.join(specDir, "tlc-output.log"), result.output);
|
|
9174
9475
|
process.exit(1);
|
|
9175
9476
|
}
|
|
9176
9477
|
function showHelp() {
|
|
@@ -9233,8 +9534,8 @@ ${color("Learn More:", COLORS.blue)}
|
|
|
9233
9534
|
}
|
|
9234
9535
|
function findTsConfig() {
|
|
9235
9536
|
const locations = [
|
|
9236
|
-
|
|
9237
|
-
|
|
9537
|
+
path5.join(process.cwd(), "tsconfig.json"),
|
|
9538
|
+
path5.join(process.cwd(), "packages", "web-ext", "tsconfig.json")
|
|
9238
9539
|
];
|
|
9239
9540
|
for (const loc of locations) {
|
|
9240
9541
|
if (fs4.existsSync(loc)) {
|
|
@@ -9245,9 +9546,9 @@ function findTsConfig() {
|
|
|
9245
9546
|
}
|
|
9246
9547
|
function findStateFile() {
|
|
9247
9548
|
const locations = [
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9549
|
+
path5.join(process.cwd(), "types", "state.ts"),
|
|
9550
|
+
path5.join(process.cwd(), "src", "types", "state.ts"),
|
|
9551
|
+
path5.join(process.cwd(), "packages", "web-ext", "src", "shared", "state", "app-state.ts")
|
|
9251
9552
|
];
|
|
9252
9553
|
for (const loc of locations) {
|
|
9253
9554
|
if (fs4.existsSync(loc)) {
|
|
@@ -9261,4 +9562,4 @@ main().catch((error) => {
|
|
|
9261
9562
|
process.exit(1);
|
|
9262
9563
|
});
|
|
9263
9564
|
|
|
9264
|
-
//# debugId=
|
|
9565
|
+
//# debugId=0E6C5C2B308F1EA464756E2164756E21
|