@fairfox/polly 0.83.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/bdd/src/cli.js +23 -19
- package/dist/tools/bdd/src/cli.js.map +4 -4
- package/dist/tools/bdd/src/index.js +23 -19
- package/dist/tools/bdd/src/index.js.map +4 -4
- package/dist/tools/bdd/src/types.d.ts +7 -0
- package/dist/tools/verify/src/cli.js +462 -113
- 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
|
}
|
|
@@ -3356,18 +3390,45 @@ var init_witness = __esm(() => {
|
|
|
3356
3390
|
// tools/verify/src/codegen/witness.ts
|
|
3357
3391
|
var exports_witness2 = {};
|
|
3358
3392
|
__export(exports_witness2, {
|
|
3393
|
+
witnessVerdict: () => witnessVerdict,
|
|
3394
|
+
witnessSpecLocation: () => witnessSpecLocation,
|
|
3395
|
+
witnessPolarity: () => witnessPolarity,
|
|
3359
3396
|
routeWitness: () => routeWitness,
|
|
3397
|
+
parseModuleName: () => parseModuleName,
|
|
3360
3398
|
buildWitnessModule: () => buildWitnessModule,
|
|
3399
|
+
buildWitnessInvariantBare: () => buildWitnessInvariantBare,
|
|
3361
3400
|
buildWitnessInvariant: () => buildWitnessInvariant,
|
|
3362
3401
|
buildWitnessCfg: () => buildWitnessCfg,
|
|
3363
3402
|
bddPredicateToTLA: () => bddPredicateToTLA,
|
|
3403
|
+
bareFieldRenderer: () => bareFieldRenderer,
|
|
3364
3404
|
WitnessTranslationError: () => WitnessTranslationError,
|
|
3365
3405
|
WITNESS_INVARIANT: () => WITNESS_INVARIANT
|
|
3366
3406
|
});
|
|
3367
|
-
|
|
3368
|
-
|
|
3407
|
+
import * as path3 from "node:path";
|
|
3408
|
+
function witnessPolarity(tags) {
|
|
3409
|
+
return tags.includes("forbidden") ? "forbidden" : "positive";
|
|
3410
|
+
}
|
|
3411
|
+
function witnessVerdict(polarity, reachable) {
|
|
3412
|
+
if (polarity === "forbidden") {
|
|
3413
|
+
return reachable ? {
|
|
3414
|
+
status: "violated",
|
|
3415
|
+
ok: false,
|
|
3416
|
+
message: "the forbidden state is REACHABLE — a defect; the counterexample is the path to it"
|
|
3417
|
+
} : { status: "excluded", ok: true, message: "the model proves this state unreachable" };
|
|
3418
|
+
}
|
|
3419
|
+
return reachable ? { status: "reachable", ok: true, message: "the model reaches this outcome" } : {
|
|
3420
|
+
status: "unreachable",
|
|
3421
|
+
ok: false,
|
|
3422
|
+
message: "the model proves this outcome impossible (the scenario lies)"
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
function flattenField(path4) {
|
|
3426
|
+
return path4.split(".").join("_");
|
|
3369
3427
|
}
|
|
3370
|
-
function
|
|
3428
|
+
function bareFieldRenderer(fields = {}) {
|
|
3429
|
+
return (fieldPath) => fields[fieldPath] ?? flattenField(fieldPath);
|
|
3430
|
+
}
|
|
3431
|
+
function translateOperand(raw, render) {
|
|
3371
3432
|
const op = raw.trim();
|
|
3372
3433
|
if (op === "true")
|
|
3373
3434
|
return "TRUE";
|
|
@@ -3383,11 +3444,11 @@ function translateOperand(raw) {
|
|
|
3383
3444
|
throw new WitnessTranslationError(`unsupported operand "${raw}" in witness predicate`);
|
|
3384
3445
|
}
|
|
3385
3446
|
if (op.endsWith(".length")) {
|
|
3386
|
-
return `Len(
|
|
3447
|
+
return `Len(${render(op.slice(0, -".length".length))})`;
|
|
3387
3448
|
}
|
|
3388
|
-
return
|
|
3449
|
+
return render(op);
|
|
3389
3450
|
}
|
|
3390
|
-
function translateConjunct(conjunct) {
|
|
3451
|
+
function translateConjunct(conjunct, render) {
|
|
3391
3452
|
const text = conjunct.trim();
|
|
3392
3453
|
for (const [js, tla] of COMPARATORS) {
|
|
3393
3454
|
const at = text.indexOf(js);
|
|
@@ -3395,31 +3456,59 @@ function translateConjunct(conjunct) {
|
|
|
3395
3456
|
continue;
|
|
3396
3457
|
const lhs = text.slice(0, at);
|
|
3397
3458
|
const rhs = text.slice(at + js.length);
|
|
3398
|
-
return `${translateOperand(lhs)} ${tla} ${translateOperand(rhs)}`;
|
|
3459
|
+
return `${translateOperand(lhs, render)} ${tla} ${translateOperand(rhs, render)}`;
|
|
3399
3460
|
}
|
|
3400
3461
|
throw new WitnessTranslationError(`no comparison operator in witness conjunct "${conjunct}"`);
|
|
3401
3462
|
}
|
|
3402
|
-
function bddPredicateToTLA(predicate) {
|
|
3463
|
+
function bddPredicateToTLA(predicate, render = contextFieldRenderer) {
|
|
3403
3464
|
const conjuncts = predicate.split("&&").map((c) => c.trim()).filter(Boolean);
|
|
3404
3465
|
if (conjuncts.length === 0) {
|
|
3405
3466
|
throw new WitnessTranslationError("empty witness predicate");
|
|
3406
3467
|
}
|
|
3407
|
-
return conjuncts.map(translateConjunct).join(" /\\ ");
|
|
3468
|
+
return conjuncts.map((c) => translateConjunct(c, render)).join(" /\\ ");
|
|
3408
3469
|
}
|
|
3409
3470
|
function buildWitnessInvariant(tlaPredicate) {
|
|
3410
3471
|
return `${WITNESS_INVARIANT} == ~(\\E ctx \\in Contexts : ${tlaPredicate})`;
|
|
3411
3472
|
}
|
|
3412
|
-
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);
|
|
3413
3478
|
return [
|
|
3414
3479
|
`---- MODULE ${moduleName} ----`,
|
|
3415
|
-
`EXTENDS ${
|
|
3480
|
+
`EXTENDS ${baseModule}`,
|
|
3416
3481
|
"",
|
|
3417
|
-
|
|
3482
|
+
invariant,
|
|
3418
3483
|
"====",
|
|
3419
3484
|
""
|
|
3420
3485
|
].join(`
|
|
3421
3486
|
`);
|
|
3422
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
|
+
}
|
|
3423
3512
|
function sectionBody(cfg, header) {
|
|
3424
3513
|
const body = [];
|
|
3425
3514
|
let inSection = false;
|
|
@@ -3448,11 +3537,22 @@ function headerLine(cfg, header) {
|
|
|
3448
3537
|
return null;
|
|
3449
3538
|
}
|
|
3450
3539
|
function buildWitnessCfg(baseCfg) {
|
|
3451
|
-
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"];
|
|
3452
3544
|
const constants = sectionBody(baseCfg, "CONSTANTS");
|
|
3453
3545
|
const constraint = sectionBody(baseCfg, "CONSTRAINT");
|
|
3454
3546
|
const symmetry = sectionBody(baseCfg, "SYMMETRY");
|
|
3455
|
-
const out = [
|
|
3547
|
+
const out = [
|
|
3548
|
+
...behaviour,
|
|
3549
|
+
"",
|
|
3550
|
+
"CONSTANTS",
|
|
3551
|
+
...constants,
|
|
3552
|
+
"",
|
|
3553
|
+
"INVARIANTS",
|
|
3554
|
+
` ${WITNESS_INVARIANT}`
|
|
3555
|
+
];
|
|
3456
3556
|
if (constraint.length > 0)
|
|
3457
3557
|
out.push("", "CONSTRAINT", ...constraint);
|
|
3458
3558
|
if (symmetry.length > 0)
|
|
@@ -3471,7 +3571,7 @@ function routeWitness(fields, subsystems) {
|
|
|
3471
3571
|
const only = owners.length === 1 ? owners[0] : undefined;
|
|
3472
3572
|
return only ? only[0] : null;
|
|
3473
3573
|
}
|
|
3474
|
-
var WITNESS_INVARIANT = "WitnessReachable", WitnessTranslationError, COMPARATORS
|
|
3574
|
+
var WITNESS_INVARIANT = "WitnessReachable", WitnessTranslationError, COMPARATORS, contextFieldRenderer = (fieldPath) => `contextStates[ctx].${flattenField(fieldPath)}`;
|
|
3475
3575
|
var init_witness2 = __esm(() => {
|
|
3476
3576
|
WitnessTranslationError = class WitnessTranslationError extends Error {
|
|
3477
3577
|
};
|
|
@@ -3494,11 +3594,11 @@ __export(exports_docker, {
|
|
|
3494
3594
|
});
|
|
3495
3595
|
import { spawn } from "node:child_process";
|
|
3496
3596
|
import * as fs3 from "node:fs";
|
|
3497
|
-
import * as
|
|
3597
|
+
import * as path4 from "node:path";
|
|
3498
3598
|
|
|
3499
3599
|
class DockerRunner {
|
|
3500
3600
|
IMAGE_NAME = "polly-tla:latest";
|
|
3501
|
-
DOCKERFILE_PATH =
|
|
3601
|
+
DOCKERFILE_PATH = path4.join(__dirname, "../../Dockerfile");
|
|
3502
3602
|
async isDockerAvailable() {
|
|
3503
3603
|
try {
|
|
3504
3604
|
const result = await this.runCommand("docker", ["info"], {
|
|
@@ -3526,7 +3626,7 @@ class DockerRunner {
|
|
|
3526
3626
|
}
|
|
3527
3627
|
}
|
|
3528
3628
|
async buildImage(onProgress) {
|
|
3529
|
-
const dockerfileDir =
|
|
3629
|
+
const dockerfileDir = path4.dirname(this.DOCKERFILE_PATH);
|
|
3530
3630
|
await this.runCommandStreaming("docker", ["build", "-f", this.DOCKERFILE_PATH, "-t", this.IMAGE_NAME, dockerfileDir], onProgress, 300000);
|
|
3531
3631
|
}
|
|
3532
3632
|
async pullImage(onProgress) {
|
|
@@ -3536,13 +3636,13 @@ class DockerRunner {
|
|
|
3536
3636
|
if (!fs3.existsSync(specPath)) {
|
|
3537
3637
|
throw new Error(`Spec file not found: ${specPath}`);
|
|
3538
3638
|
}
|
|
3539
|
-
const specDir =
|
|
3540
|
-
const specName =
|
|
3541
|
-
const cfgPath =
|
|
3639
|
+
const specDir = path4.dirname(specPath);
|
|
3640
|
+
const specName = path4.basename(specPath, ".tla");
|
|
3641
|
+
const cfgPath = path4.join(specDir, `${specName}.cfg`);
|
|
3542
3642
|
if (!fs3.existsSync(cfgPath)) {
|
|
3543
3643
|
throw new Error(`Config file not found: ${cfgPath}`);
|
|
3544
3644
|
}
|
|
3545
|
-
const statesDir =
|
|
3645
|
+
const statesDir = path4.join(specDir, "states");
|
|
3546
3646
|
if (fs3.existsSync(statesDir)) {
|
|
3547
3647
|
fs3.rmSync(statesDir, { recursive: true, force: true });
|
|
3548
3648
|
}
|
|
@@ -3635,7 +3735,7 @@ class DockerRunner {
|
|
|
3635
3735
|
return "Unknown error occurred during model checking";
|
|
3636
3736
|
}
|
|
3637
3737
|
runCommand(command, args, options) {
|
|
3638
|
-
return new Promise((
|
|
3738
|
+
return new Promise((resolve3, reject) => {
|
|
3639
3739
|
const proc = spawn(command, args);
|
|
3640
3740
|
let stdout = "";
|
|
3641
3741
|
let stderr = "";
|
|
@@ -3653,7 +3753,7 @@ class DockerRunner {
|
|
|
3653
3753
|
proc.on("close", (exitCode) => {
|
|
3654
3754
|
if (timeout)
|
|
3655
3755
|
clearTimeout(timeout);
|
|
3656
|
-
|
|
3756
|
+
resolve3({
|
|
3657
3757
|
exitCode: exitCode || 0,
|
|
3658
3758
|
stdout,
|
|
3659
3759
|
stderr
|
|
@@ -3667,7 +3767,7 @@ class DockerRunner {
|
|
|
3667
3767
|
});
|
|
3668
3768
|
}
|
|
3669
3769
|
runCommandStreaming(command, args, onOutput, timeout) {
|
|
3670
|
-
return new Promise((
|
|
3770
|
+
return new Promise((resolve3, reject) => {
|
|
3671
3771
|
const proc = spawn(command, args);
|
|
3672
3772
|
const timeoutHandle = timeout && timeout > 0 ? setTimeout(() => {
|
|
3673
3773
|
proc.kill();
|
|
@@ -3699,7 +3799,7 @@ class DockerRunner {
|
|
|
3699
3799
|
if (timeoutHandle)
|
|
3700
3800
|
clearTimeout(timeoutHandle);
|
|
3701
3801
|
if (exitCode === 0) {
|
|
3702
|
-
|
|
3802
|
+
resolve3();
|
|
3703
3803
|
} else {
|
|
3704
3804
|
reject(new Error(`Command failed with exit code ${exitCode}`));
|
|
3705
3805
|
}
|
|
@@ -3718,7 +3818,7 @@ var init_docker = () => {};
|
|
|
3718
3818
|
// tools/verify/src/cli.ts
|
|
3719
3819
|
init_expression_validator();
|
|
3720
3820
|
import * as fs4 from "node:fs";
|
|
3721
|
-
import * as
|
|
3821
|
+
import * as path5 from "node:path";
|
|
3722
3822
|
|
|
3723
3823
|
// tools/verify/src/analysis/mesh-signal-warnings.ts
|
|
3724
3824
|
function computeMeshOrPeerSignalFindings(analysis, declaredMeshDocs) {
|
|
@@ -5445,6 +5545,7 @@ class HandlerExtractor {
|
|
|
5445
5545
|
warnings;
|
|
5446
5546
|
currentFunctionParams = [];
|
|
5447
5547
|
contextOverrides;
|
|
5548
|
+
onSurfaceSpans = [];
|
|
5448
5549
|
constructor(tsConfigPath, contextOverrides) {
|
|
5449
5550
|
this.project = new Project({
|
|
5450
5551
|
tsConfigFilePath: tsConfigPath
|
|
@@ -5504,6 +5605,7 @@ class HandlerExtractor {
|
|
|
5504
5605
|
const meshOrPeerSignals = [];
|
|
5505
5606
|
const resources = [];
|
|
5506
5607
|
this.warnings = [];
|
|
5608
|
+
this.onSurfaceSpans = [];
|
|
5507
5609
|
const allSourceFiles = this.project.getSourceFiles();
|
|
5508
5610
|
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
5509
5611
|
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
@@ -5538,6 +5640,30 @@ class HandlerExtractor {
|
|
|
5538
5640
|
continue;
|
|
5539
5641
|
meshOrPeerSignals.push(...this.extractMeshOrPeerSignalsFromFile(sourceFile));
|
|
5540
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
|
+
}
|
|
5541
5667
|
this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
|
|
5542
5668
|
this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
|
|
5543
5669
|
return {
|
|
@@ -5548,6 +5674,7 @@ class HandlerExtractor {
|
|
|
5548
5674
|
verifiedStates,
|
|
5549
5675
|
meshOrPeerSignals,
|
|
5550
5676
|
resources,
|
|
5677
|
+
offSurfaceMutations,
|
|
5551
5678
|
warnings: this.warnings
|
|
5552
5679
|
};
|
|
5553
5680
|
}
|
|
@@ -5958,6 +6085,7 @@ class HandlerExtractor {
|
|
|
5958
6085
|
return false;
|
|
5959
6086
|
}
|
|
5960
6087
|
extractAssignments(funcNode, assignments) {
|
|
6088
|
+
this.recordOnSurfaceSpan(funcNode);
|
|
5961
6089
|
funcNode.forEachDescendant((node) => {
|
|
5962
6090
|
if (Node2.isBinaryExpression(node)) {
|
|
5963
6091
|
this.extractBinaryExpressionAssignment(node, assignments);
|
|
@@ -7544,6 +7672,7 @@ class HandlerExtractor {
|
|
|
7544
7672
|
}
|
|
7545
7673
|
findStateMutationsInFunction(func, stateVarNames) {
|
|
7546
7674
|
const mutations = [];
|
|
7675
|
+
this.recordOnSurfaceSpan(func);
|
|
7547
7676
|
func.forEachDescendant((node) => {
|
|
7548
7677
|
if (!Node2.isBinaryExpression(node))
|
|
7549
7678
|
return;
|
|
@@ -7573,6 +7702,140 @@ class HandlerExtractor {
|
|
|
7573
7702
|
});
|
|
7574
7703
|
return mutations;
|
|
7575
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
|
+
}
|
|
7576
7839
|
functionNameToMessageType(funcName) {
|
|
7577
7840
|
let name = funcName.replace(/^handle/, "").replace(/^on/, "").replace(/^set/, "Set").replace(/^update/, "Update").replace(/^do/, "");
|
|
7578
7841
|
if (name.length > 0) {
|
|
@@ -7737,7 +8000,8 @@ class TypeExtractor {
|
|
|
7737
8000
|
globalStateConstraints: handlerAnalysis.globalStateConstraints,
|
|
7738
8001
|
verifiedStates: handlerAnalysis.verifiedStates,
|
|
7739
8002
|
meshOrPeerSignals: handlerAnalysis.meshOrPeerSignals,
|
|
7740
|
-
resources: handlerAnalysis.resources
|
|
8003
|
+
resources: handlerAnalysis.resources,
|
|
8004
|
+
offSurfaceMutations: handlerAnalysis.offSurfaceMutations
|
|
7741
8005
|
};
|
|
7742
8006
|
}
|
|
7743
8007
|
extractHandlerAnalysis() {
|
|
@@ -8208,7 +8472,7 @@ async function setupCommand() {
|
|
|
8208
8472
|
displayAnalysisResults(analysis);
|
|
8209
8473
|
displayAnalysisSummary(analysis);
|
|
8210
8474
|
const configContent = generateConfig(analysis);
|
|
8211
|
-
const configPath =
|
|
8475
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8212
8476
|
writeConfigFile(configPath, configContent);
|
|
8213
8477
|
displaySetupSuccess(configPath);
|
|
8214
8478
|
} catch (_error) {
|
|
@@ -8256,7 +8520,7 @@ function getFieldStatus(confidence) {
|
|
|
8256
8520
|
return color("⚠ Manual config", COLORS.red);
|
|
8257
8521
|
}
|
|
8258
8522
|
function writeConfigFile(configPath, configContent) {
|
|
8259
|
-
const configDir =
|
|
8523
|
+
const configDir = path5.dirname(configPath);
|
|
8260
8524
|
if (!fs4.existsSync(configDir)) {
|
|
8261
8525
|
fs4.mkdirSync(configDir, { recursive: true });
|
|
8262
8526
|
}
|
|
@@ -8279,7 +8543,7 @@ function displaySetupSuccess(configPath) {
|
|
|
8279
8543
|
console.log();
|
|
8280
8544
|
}
|
|
8281
8545
|
async function validateCommand() {
|
|
8282
|
-
const configPath =
|
|
8546
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8283
8547
|
console.log(color(`
|
|
8284
8548
|
\uD83D\uDD0D Validating configuration...
|
|
8285
8549
|
`, COLORS.blue));
|
|
@@ -8300,7 +8564,7 @@ async function validateCommand() {
|
|
|
8300
8564
|
process.exit(1);
|
|
8301
8565
|
}
|
|
8302
8566
|
async function estimateCommand() {
|
|
8303
|
-
const configPath =
|
|
8567
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8304
8568
|
console.log(color(`
|
|
8305
8569
|
\uD83D\uDCCA Estimating state space...
|
|
8306
8570
|
`, COLORS.blue));
|
|
@@ -8443,8 +8707,8 @@ function isWitnessMode() {
|
|
|
8443
8707
|
return process.argv.includes("--witness");
|
|
8444
8708
|
}
|
|
8445
8709
|
function displayModelCoverage(report) {
|
|
8446
|
-
const { unwrittenFields, unconstrainedMutators, fieldCoverage } = report;
|
|
8447
|
-
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) {
|
|
8448
8712
|
console.log(color(`✓ Model coverage: all ${fieldCoverage.length} declared field(s) written by a modelled handler`, COLORS.green));
|
|
8449
8713
|
console.log();
|
|
8450
8714
|
return;
|
|
@@ -8468,11 +8732,24 @@ function displayModelCoverage(report) {
|
|
|
8468
8732
|
console.log(color(" The checker explores these transitions but asserts nothing about their effect.", COLORS.gray));
|
|
8469
8733
|
console.log();
|
|
8470
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
|
+
}
|
|
8471
8748
|
}
|
|
8472
8749
|
async function runModelCoverage(typedConfig, typedAnalysis, meshFindingCount) {
|
|
8473
8750
|
const { computeModelCoverage: computeModelCoverage2, strictCoverageReasons: strictCoverageReasons2 } = await Promise.resolve().then(() => exports_model_coverage);
|
|
8474
8751
|
const stateFields = Object.keys(typedConfig.state ?? {});
|
|
8475
|
-
const coverage = computeModelCoverage2(stateFields, typedAnalysis.handlers);
|
|
8752
|
+
const coverage = computeModelCoverage2(stateFields, typedAnalysis.handlers, typedAnalysis.offSurfaceMutations ?? []);
|
|
8476
8753
|
displayModelCoverage(coverage);
|
|
8477
8754
|
if (!isStrictMode())
|
|
8478
8755
|
return;
|
|
@@ -8494,7 +8771,7 @@ async function runModelCoverage(typedConfig, typedAnalysis, meshFindingCount) {
|
|
|
8494
8771
|
process.exit(1);
|
|
8495
8772
|
}
|
|
8496
8773
|
async function verifyCommand() {
|
|
8497
|
-
const configPath =
|
|
8774
|
+
const configPath = path5.join(process.cwd(), "specs", "verification.config.ts");
|
|
8498
8775
|
console.log(color(`
|
|
8499
8776
|
\uD83D\uDD0D Running verification...
|
|
8500
8777
|
`, COLORS.blue));
|
|
@@ -8560,6 +8837,12 @@ function getMaxDepth(config) {
|
|
|
8560
8837
|
}
|
|
8561
8838
|
return;
|
|
8562
8839
|
}
|
|
8840
|
+
function getCustomTLAPaths(config) {
|
|
8841
|
+
if ("messages" in config && config.customTLAPaths) {
|
|
8842
|
+
return config.customTLAPaths;
|
|
8843
|
+
}
|
|
8844
|
+
return {};
|
|
8845
|
+
}
|
|
8563
8846
|
async function runCoupledFieldsLint(config, analysis) {
|
|
8564
8847
|
const groups = config.coupledFields ?? [];
|
|
8565
8848
|
if (groups.length === 0)
|
|
@@ -8707,18 +8990,23 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
8707
8990
|
const maxDepth = getMaxDepth(config);
|
|
8708
8991
|
const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
8709
8992
|
const results = [];
|
|
8993
|
+
const customTLAPaths = getCustomTLAPaths(config);
|
|
8710
8994
|
for (const name of subsystemNames) {
|
|
8711
8995
|
const sub = subsystems[name];
|
|
8712
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
|
+
}
|
|
8713
9001
|
console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
|
|
8714
9002
|
const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
|
|
8715
9003
|
const ensuresCount = (spec.match(/^EnsuresAfter_\w+ ==/gm) ?? []).length;
|
|
8716
|
-
const specDir =
|
|
9004
|
+
const specDir = path5.join(process.cwd(), "specs", "tla", "generated", name);
|
|
8717
9005
|
if (!fs4.existsSync(specDir)) {
|
|
8718
9006
|
fs4.mkdirSync(specDir, { recursive: true });
|
|
8719
9007
|
}
|
|
8720
|
-
const specPath =
|
|
8721
|
-
const cfgPath =
|
|
9008
|
+
const specPath = path5.join(specDir, `UserApp_${name}.tla`);
|
|
9009
|
+
const cfgPath = path5.join(specDir, `UserApp_${name}.cfg`);
|
|
8722
9010
|
fs4.writeFileSync(specPath, spec);
|
|
8723
9011
|
fs4.writeFileSync(cfgPath, cfg);
|
|
8724
9012
|
findAndCopyBaseSpec(specDir);
|
|
@@ -8747,7 +9035,7 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
8747
9035
|
} else if (result.error) {
|
|
8748
9036
|
console.log(color(` Error: ${result.error}`, COLORS.red));
|
|
8749
9037
|
}
|
|
8750
|
-
fs4.writeFileSync(
|
|
9038
|
+
fs4.writeFileSync(path5.join(specDir, "tlc-output.log"), result.output);
|
|
8751
9039
|
}
|
|
8752
9040
|
}
|
|
8753
9041
|
console.log();
|
|
@@ -8779,13 +9067,19 @@ async function runWitnessVerification(config) {
|
|
|
8779
9067
|
}
|
|
8780
9068
|
const { extractWitnesses: extractWitnesses2 } = await Promise.resolve().then(() => (init_witness(), exports_witness));
|
|
8781
9069
|
const {
|
|
9070
|
+
bareFieldRenderer: bareFieldRenderer2,
|
|
8782
9071
|
bddPredicateToTLA: bddPredicateToTLA2,
|
|
8783
9072
|
buildWitnessCfg: buildWitnessCfg2,
|
|
8784
9073
|
buildWitnessModule: buildWitnessModule2,
|
|
9074
|
+
parseModuleName: parseModuleName2,
|
|
8785
9075
|
routeWitness: routeWitness2,
|
|
9076
|
+
witnessPolarity: witnessPolarity2,
|
|
9077
|
+
witnessSpecLocation: witnessSpecLocation2,
|
|
9078
|
+
witnessVerdict: witnessVerdict2,
|
|
8786
9079
|
WITNESS_INVARIANT: WITNESS_INVARIANT2
|
|
8787
9080
|
} = await Promise.resolve().then(() => (init_witness2(), exports_witness2));
|
|
8788
9081
|
const subsystems = config.subsystems;
|
|
9082
|
+
const customTLAPaths = getCustomTLAPaths(config);
|
|
8789
9083
|
const witnesses = await extractWitnesses2(featureFiles, stepFiles);
|
|
8790
9084
|
const docker = await setupDocker();
|
|
8791
9085
|
const timeoutSeconds = getTimeout(config);
|
|
@@ -8795,8 +9089,14 @@ async function runWitnessVerification(config) {
|
|
|
8795
9089
|
let idx = 0;
|
|
8796
9090
|
for (const w of witnesses) {
|
|
8797
9091
|
const id = `${w.feature} › ${w.scenario}`;
|
|
9092
|
+
const polarity = witnessPolarity2(w.tags);
|
|
8798
9093
|
if (!w.predicate) {
|
|
8799
|
-
results.push({
|
|
9094
|
+
results.push({
|
|
9095
|
+
id,
|
|
9096
|
+
status: "skipped",
|
|
9097
|
+
ok: true,
|
|
9098
|
+
note: "no state-observable Then to witness"
|
|
9099
|
+
});
|
|
8800
9100
|
continue;
|
|
8801
9101
|
}
|
|
8802
9102
|
const subsystem = routeWitness2(w.fields, subsystems);
|
|
@@ -8804,89 +9104,128 @@ async function runWitnessVerification(config) {
|
|
|
8804
9104
|
results.push({
|
|
8805
9105
|
id,
|
|
8806
9106
|
status: "skipped",
|
|
9107
|
+
ok: true,
|
|
8807
9108
|
note: `fields [${w.fields.join(", ")}] not owned by a single subsystem`
|
|
8808
9109
|
});
|
|
8809
9110
|
continue;
|
|
8810
9111
|
}
|
|
8811
|
-
const
|
|
8812
|
-
|
|
8813
|
-
if (
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
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
|
+
}
|
|
8821
9151
|
}
|
|
8822
9152
|
let tlaPredicate;
|
|
8823
9153
|
try {
|
|
8824
|
-
tlaPredicate = bddPredicateToTLA2(w.predicate);
|
|
9154
|
+
tlaPredicate = location.custom ? bddPredicateToTLA2(w.predicate, bareFieldRenderer2(custom?.fields)) : bddPredicateToTLA2(w.predicate);
|
|
8825
9155
|
} catch (err) {
|
|
8826
9156
|
results.push({
|
|
8827
9157
|
id,
|
|
8828
9158
|
status: "error",
|
|
9159
|
+
ok: false,
|
|
8829
9160
|
subsystem,
|
|
8830
9161
|
note: err instanceof Error ? err.message : String(err)
|
|
8831
9162
|
});
|
|
8832
9163
|
continue;
|
|
8833
9164
|
}
|
|
8834
9165
|
const moduleName = `Witness_${subsystem}_${idx++}`;
|
|
8835
|
-
const witnessTla =
|
|
8836
|
-
fs4.writeFileSync(witnessTla, buildWitnessModule2(moduleName,
|
|
8837
|
-
fs4.writeFileSync(
|
|
8838
|
-
|
|
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")));
|
|
9169
|
+
const polarityTag = polarity === "forbidden" ? color(" [forbidden — must be unreachable]", COLORS.gray) : "";
|
|
9170
|
+
console.log(color(`⚙️ ${id}`, COLORS.blue) + polarityTag);
|
|
8839
9171
|
console.log(color(` ${subsystem} ⊨ ${w.predicate}`, COLORS.gray));
|
|
8840
9172
|
let tlc;
|
|
8841
9173
|
try {
|
|
8842
9174
|
tlc = await docker.runTLC(witnessTla, { workers, timeout });
|
|
8843
9175
|
} catch (err) {
|
|
8844
9176
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8845
|
-
results.push({ id, status: "inconclusive", subsystem, predicate: w.predicate, note: msg });
|
|
8846
|
-
console.log(color(" ⚠ inconclusive — TLC did not finish (raise the config timeout)", COLORS.yellow));
|
|
8847
|
-
continue;
|
|
8848
|
-
}
|
|
8849
|
-
if (!tlc.success && tlc.violation?.name === WITNESS_INVARIANT2) {
|
|
8850
|
-
results.push({ id, status: "reachable", subsystem, predicate: w.predicate });
|
|
8851
|
-
console.log(color(" ✓ reachable — the model reaches this outcome", COLORS.green));
|
|
8852
|
-
} else if (tlc.success) {
|
|
8853
9177
|
results.push({
|
|
8854
9178
|
id,
|
|
8855
|
-
status: "
|
|
9179
|
+
status: "inconclusive",
|
|
9180
|
+
ok: true,
|
|
8856
9181
|
subsystem,
|
|
8857
9182
|
predicate: w.predicate,
|
|
8858
|
-
note:
|
|
9183
|
+
note: msg
|
|
8859
9184
|
});
|
|
8860
|
-
console.log(color("
|
|
8861
|
-
|
|
8862
|
-
}
|
|
9185
|
+
console.log(color(" ⚠ inconclusive — TLC did not finish (raise the config timeout)", COLORS.yellow));
|
|
9186
|
+
continue;
|
|
9187
|
+
}
|
|
9188
|
+
const reachable = !tlc.success && tlc.violation?.name === WITNESS_INVARIANT2;
|
|
9189
|
+
if (!tlc.success && !reachable) {
|
|
8863
9190
|
results.push({
|
|
8864
9191
|
id,
|
|
8865
9192
|
status: "error",
|
|
9193
|
+
ok: false,
|
|
8866
9194
|
subsystem,
|
|
8867
9195
|
predicate: w.predicate,
|
|
8868
9196
|
note: tlc.error ?? tlc.violation?.name ?? "TLC error"
|
|
8869
9197
|
});
|
|
8870
9198
|
console.log(color(` ! error — ${tlc.error ?? "see log"}`, COLORS.yellow));
|
|
8871
|
-
fs4.writeFileSync(
|
|
9199
|
+
fs4.writeFileSync(path5.join(location.dir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
9200
|
+
continue;
|
|
8872
9201
|
}
|
|
9202
|
+
const verdict = witnessVerdict2(polarity, reachable);
|
|
9203
|
+
const note = reachable ? undefined : `${tlc.stats?.distinctStates ?? 0} distinct states explored`;
|
|
9204
|
+
results.push({
|
|
9205
|
+
id,
|
|
9206
|
+
status: verdict.status,
|
|
9207
|
+
ok: verdict.ok,
|
|
9208
|
+
subsystem,
|
|
9209
|
+
predicate: w.predicate,
|
|
9210
|
+
note
|
|
9211
|
+
});
|
|
9212
|
+
console.log(color(` ${verdict.ok ? "✓" : "✗"} ${verdict.status} — ${verdict.message}`, verdict.ok ? COLORS.green : COLORS.red));
|
|
9213
|
+
if (!verdict.ok)
|
|
9214
|
+
fs4.writeFileSync(path5.join(location.dir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8873
9215
|
}
|
|
8874
9216
|
displayWitnessReport(results);
|
|
8875
9217
|
}
|
|
8876
9218
|
function displayWitnessReport(results) {
|
|
8877
9219
|
const count = (s) => results.filter((r) => r.status === s).length;
|
|
8878
|
-
const reachable = count("reachable");
|
|
8879
|
-
const unreachable = count("unreachable");
|
|
8880
|
-
const inconclusive = count("inconclusive");
|
|
8881
|
-
const errors = count("error");
|
|
8882
|
-
const skipped = count("skipped");
|
|
8883
9220
|
console.log();
|
|
8884
9221
|
console.log(color(`Witness results:
|
|
8885
9222
|
`, COLORS.blue));
|
|
8886
9223
|
for (const r of results) {
|
|
8887
9224
|
const mark = {
|
|
8888
9225
|
reachable: color("✓", COLORS.green),
|
|
9226
|
+
excluded: color("✓", COLORS.green),
|
|
8889
9227
|
unreachable: color("✗", COLORS.red),
|
|
9228
|
+
violated: color("✗", COLORS.red),
|
|
8890
9229
|
inconclusive: color("⚠", COLORS.yellow),
|
|
8891
9230
|
error: color("!", COLORS.yellow),
|
|
8892
9231
|
skipped: color("·", COLORS.gray)
|
|
@@ -8894,17 +9233,27 @@ function displayWitnessReport(results) {
|
|
|
8894
9233
|
const note = r.note ? color(` (${r.note})`, COLORS.gray) : "";
|
|
8895
9234
|
console.log(` ${mark} ${r.status.padEnd(12)} ${r.id}${note}`);
|
|
8896
9235
|
}
|
|
9236
|
+
const tally = [
|
|
9237
|
+
"reachable",
|
|
9238
|
+
"excluded",
|
|
9239
|
+
"unreachable",
|
|
9240
|
+
"violated",
|
|
9241
|
+
"inconclusive",
|
|
9242
|
+
"error",
|
|
9243
|
+
"skipped"
|
|
9244
|
+
].map((s) => `${count(s)} ${s}`).join(", ");
|
|
8897
9245
|
console.log();
|
|
8898
|
-
console.log(color(` ${
|
|
9246
|
+
console.log(color(` ${tally}`, COLORS.gray));
|
|
8899
9247
|
console.log();
|
|
8900
|
-
|
|
9248
|
+
const failures = results.filter((r) => !r.ok);
|
|
9249
|
+
if (failures.length > 0) {
|
|
8901
9250
|
console.log(color("Witness result: ✗ FAIL", COLORS.red));
|
|
8902
|
-
console.log(color("
|
|
9251
|
+
console.log(color(" An outcome a scenario claims is unreachable, or a forbidden state the model can reach.", COLORS.gray));
|
|
8903
9252
|
console.log();
|
|
8904
9253
|
process.exit(1);
|
|
8905
9254
|
}
|
|
8906
9255
|
console.log(color("Witness result: ✓ PASS", COLORS.green));
|
|
8907
|
-
console.log(color(" Every
|
|
9256
|
+
console.log(color(" Every claimed outcome is reachable; every forbidden state is proven unreachable.", COLORS.gray));
|
|
8908
9257
|
console.log();
|
|
8909
9258
|
}
|
|
8910
9259
|
function displayEnsuresSummary(results) {
|
|
@@ -8954,7 +9303,7 @@ function displayCompositionalReport(results, nonInterferenceValid) {
|
|
|
8954
9303
|
console.log();
|
|
8955
9304
|
}
|
|
8956
9305
|
async function loadVerificationConfig(configPath) {
|
|
8957
|
-
const resolvedPath =
|
|
9306
|
+
const resolvedPath = path5.resolve(configPath);
|
|
8958
9307
|
const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
|
|
8959
9308
|
return configModule.verificationConfig || configModule.default;
|
|
8960
9309
|
}
|
|
@@ -8976,23 +9325,23 @@ async function generateAndWriteTLASpecs(config, analysis) {
|
|
|
8976
9325
|
const { generateTLA: generateTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
8977
9326
|
console.log(color("\uD83D\uDCDD Generating TLA+ specification...", COLORS.blue));
|
|
8978
9327
|
const { spec, cfg } = await generateTLA2(config, analysis);
|
|
8979
|
-
const specDir =
|
|
9328
|
+
const specDir = path5.join(process.cwd(), "specs", "tla", "generated");
|
|
8980
9329
|
if (!fs4.existsSync(specDir)) {
|
|
8981
9330
|
fs4.mkdirSync(specDir, { recursive: true });
|
|
8982
9331
|
}
|
|
8983
|
-
const specPath =
|
|
8984
|
-
const cfgPath =
|
|
9332
|
+
const specPath = path5.join(specDir, "UserApp.tla");
|
|
9333
|
+
const cfgPath = path5.join(specDir, "UserApp.cfg");
|
|
8985
9334
|
fs4.writeFileSync(specPath, spec);
|
|
8986
9335
|
fs4.writeFileSync(cfgPath, cfg);
|
|
8987
9336
|
return { specPath, specDir };
|
|
8988
9337
|
}
|
|
8989
9338
|
function findAndCopyBaseSpec(specDir) {
|
|
8990
9339
|
const possiblePaths = [
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
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")
|
|
8996
9345
|
];
|
|
8997
9346
|
let baseSpecPath = null;
|
|
8998
9347
|
for (const candidatePath of possiblePaths) {
|
|
@@ -9002,7 +9351,7 @@ function findAndCopyBaseSpec(specDir) {
|
|
|
9002
9351
|
}
|
|
9003
9352
|
}
|
|
9004
9353
|
if (baseSpecPath) {
|
|
9005
|
-
const destSpecPath =
|
|
9354
|
+
const destSpecPath = path5.join(specDir, "MessageRouter.tla");
|
|
9006
9355
|
fs4.copyFileSync(baseSpecPath, destSpecPath);
|
|
9007
9356
|
console.log(color("✓ Copied MessageRouter.tla", COLORS.green));
|
|
9008
9357
|
} else {
|
|
@@ -9015,13 +9364,13 @@ function findAndCopyBaseSpec(specDir) {
|
|
|
9015
9364
|
}
|
|
9016
9365
|
function findMeshSeedSpecDir() {
|
|
9017
9366
|
const candidates = [
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
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")
|
|
9022
9371
|
];
|
|
9023
9372
|
for (const dir of candidates) {
|
|
9024
|
-
if (fs4.existsSync(
|
|
9373
|
+
if (fs4.existsSync(path5.join(dir, "MeshSeed.tla")))
|
|
9025
9374
|
return dir;
|
|
9026
9375
|
}
|
|
9027
9376
|
return null;
|
|
@@ -9035,11 +9384,11 @@ async function runMeshSeedGuard(docker, specDir, config) {
|
|
|
9035
9384
|
return;
|
|
9036
9385
|
}
|
|
9037
9386
|
const fixDisabled = isSeedFixDisabled();
|
|
9038
|
-
fs4.copyFileSync(
|
|
9039
|
-
const baseCfg = fs4.readFileSync(
|
|
9040
|
-
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 }));
|
|
9041
9390
|
console.log(color(`⚙️ Running mesh seed-race guard (MeshSeed.tla, SeedDeterministic = ${fixDisabled ? "FALSE" : "TRUE"})...`, COLORS.blue));
|
|
9042
|
-
return docker.runTLC(
|
|
9391
|
+
return docker.runTLC(path5.join(specDir, "MeshSeed.tla"), { workers: 1 });
|
|
9043
9392
|
}
|
|
9044
9393
|
async function setupDocker() {
|
|
9045
9394
|
const { DockerRunner: DockerRunner2 } = await Promise.resolve().then(() => (init_docker(), exports_docker));
|
|
@@ -9121,8 +9470,8 @@ function displayVerificationResults(result, specDir) {
|
|
|
9121
9470
|
}
|
|
9122
9471
|
console.log();
|
|
9123
9472
|
console.log(color("Full output saved to:", COLORS.gray));
|
|
9124
|
-
console.log(color(` ${
|
|
9125
|
-
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);
|
|
9126
9475
|
process.exit(1);
|
|
9127
9476
|
}
|
|
9128
9477
|
function showHelp() {
|
|
@@ -9185,8 +9534,8 @@ ${color("Learn More:", COLORS.blue)}
|
|
|
9185
9534
|
}
|
|
9186
9535
|
function findTsConfig() {
|
|
9187
9536
|
const locations = [
|
|
9188
|
-
|
|
9189
|
-
|
|
9537
|
+
path5.join(process.cwd(), "tsconfig.json"),
|
|
9538
|
+
path5.join(process.cwd(), "packages", "web-ext", "tsconfig.json")
|
|
9190
9539
|
];
|
|
9191
9540
|
for (const loc of locations) {
|
|
9192
9541
|
if (fs4.existsSync(loc)) {
|
|
@@ -9197,9 +9546,9 @@ function findTsConfig() {
|
|
|
9197
9546
|
}
|
|
9198
9547
|
function findStateFile() {
|
|
9199
9548
|
const locations = [
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
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")
|
|
9203
9552
|
];
|
|
9204
9553
|
for (const loc of locations) {
|
|
9205
9554
|
if (fs4.existsSync(loc)) {
|
|
@@ -9213,4 +9562,4 @@ main().catch((error) => {
|
|
|
9213
9562
|
process.exit(1);
|
|
9214
9563
|
});
|
|
9215
9564
|
|
|
9216
|
-
//# debugId=
|
|
9565
|
+
//# debugId=0E6C5C2B308F1EA464756E2164756E21
|