@fairfox/polly 0.82.1 → 0.83.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/cli/polly.js +22 -1
- package/dist/cli/polly.js.map +3 -3
- package/dist/tools/bdd/src/args.d.ts +21 -0
- package/dist/tools/bdd/src/bus-driver.d.ts +36 -0
- package/dist/tools/bdd/src/check-verify.d.ts +15 -0
- package/dist/tools/bdd/src/cli.d.ts +2 -0
- package/dist/tools/bdd/src/cli.js +701 -0
- package/dist/tools/bdd/src/cli.js.map +19 -0
- package/dist/tools/bdd/src/config.d.ts +9 -0
- package/dist/tools/bdd/src/extract.d.ts +2 -0
- package/dist/tools/bdd/src/index.d.ts +19 -0
- package/dist/tools/bdd/src/index.js +540 -0
- package/dist/tools/bdd/src/index.js.map +17 -0
- package/dist/tools/bdd/src/parse.d.ts +3 -0
- package/dist/tools/bdd/src/report.d.ts +6 -0
- package/dist/tools/bdd/src/run.d.ts +8 -0
- package/dist/tools/bdd/src/scaffold.d.ts +7 -0
- package/dist/tools/bdd/src/steps.d.ts +55 -0
- package/dist/tools/bdd/src/types.d.ts +145 -0
- package/dist/tools/bdd/src/witness.d.ts +23 -0
- package/dist/tools/quality/src/cli.js +304 -15
- package/dist/tools/quality/src/cli.js.map +6 -4
- package/dist/tools/quality/src/index.d.ts +2 -0
- package/dist/tools/quality/src/index.js +309 -15
- package/dist/tools/quality/src/index.js.map +6 -4
- package/dist/tools/quality/src/no-fixed-waits.d.ts +52 -0
- package/dist/tools/quality/src/no-tautology-ensures.d.ts +67 -0
- package/dist/tools/quality/src/plugins/core.d.ts +1 -1
- package/dist/tools/test/src/tiers/cli.js +21 -1
- package/dist/tools/test/src/tiers/cli.js.map +3 -3
- package/dist/tools/verify/src/cli.js +521 -1
- package/dist/tools/verify/src/cli.js.map +8 -4
- package/package.json +7 -1
|
@@ -3138,6 +3138,355 @@ var init_tla = __esm(() => {
|
|
|
3138
3138
|
};
|
|
3139
3139
|
});
|
|
3140
3140
|
|
|
3141
|
+
// tools/bdd/src/parse.ts
|
|
3142
|
+
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
3143
|
+
import { IdGenerator } from "@cucumber/messages";
|
|
3144
|
+
function newParser() {
|
|
3145
|
+
return new Parser(new AstBuilder(IdGenerator.uuid()), new GherkinClassicTokenMatcher);
|
|
3146
|
+
}
|
|
3147
|
+
function normalizeKeyword(raw, prev) {
|
|
3148
|
+
const k = raw.trim().toLowerCase();
|
|
3149
|
+
if (k === "given")
|
|
3150
|
+
return "given";
|
|
3151
|
+
if (k === "when")
|
|
3152
|
+
return "when";
|
|
3153
|
+
if (k === "then")
|
|
3154
|
+
return "then";
|
|
3155
|
+
return prev ?? "given";
|
|
3156
|
+
}
|
|
3157
|
+
function normalizeSteps(rawSteps) {
|
|
3158
|
+
const out = [];
|
|
3159
|
+
let prev = null;
|
|
3160
|
+
for (const s of rawSteps) {
|
|
3161
|
+
const keyword = normalizeKeyword(s.keyword, prev);
|
|
3162
|
+
prev = keyword;
|
|
3163
|
+
out.push({
|
|
3164
|
+
keyword,
|
|
3165
|
+
rawKeyword: s.keyword.trim(),
|
|
3166
|
+
text: s.text.trim(),
|
|
3167
|
+
line: s.location?.line ?? 0
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
return out;
|
|
3171
|
+
}
|
|
3172
|
+
function tagNames(tags) {
|
|
3173
|
+
return (tags ?? []).map((t) => t.name.replace(/^@/, ""));
|
|
3174
|
+
}
|
|
3175
|
+
function fillOutline(text, headers, cells) {
|
|
3176
|
+
let filled = text;
|
|
3177
|
+
headers.forEach((h, i) => {
|
|
3178
|
+
filled = filled.split(`<${h}>`).join(cells[i] ?? "");
|
|
3179
|
+
});
|
|
3180
|
+
return filled;
|
|
3181
|
+
}
|
|
3182
|
+
function buildScenarios(sc) {
|
|
3183
|
+
const baseSteps = sc.steps ?? [];
|
|
3184
|
+
const tags = tagNames(sc.tags);
|
|
3185
|
+
const examples = sc.examples ?? [];
|
|
3186
|
+
if (examples.length === 0) {
|
|
3187
|
+
return [
|
|
3188
|
+
{ name: sc.name, tags, steps: normalizeSteps(baseSteps), line: sc.location?.line ?? 0 }
|
|
3189
|
+
];
|
|
3190
|
+
}
|
|
3191
|
+
const out = [];
|
|
3192
|
+
for (const ex of examples) {
|
|
3193
|
+
const headers = (ex.tableHeader?.cells ?? []).map((c) => c.value);
|
|
3194
|
+
for (const row of ex.tableBody ?? []) {
|
|
3195
|
+
const cells = (row.cells ?? []).map((c) => c.value);
|
|
3196
|
+
const rowSteps = baseSteps.map((s) => ({
|
|
3197
|
+
keyword: s.keyword,
|
|
3198
|
+
text: fillOutline(s.text, headers, cells),
|
|
3199
|
+
location: s.location
|
|
3200
|
+
}));
|
|
3201
|
+
const label = headers.map((h, i) => `${h}=${cells[i] ?? ""}`).join(", ");
|
|
3202
|
+
out.push({
|
|
3203
|
+
name: `${sc.name} [${label}]`,
|
|
3204
|
+
tags: [...tags, ...tagNames(ex.tags)],
|
|
3205
|
+
steps: normalizeSteps(rowSteps),
|
|
3206
|
+
line: row.location?.line ?? sc.location?.line ?? 0,
|
|
3207
|
+
fromOutline: true
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
return out;
|
|
3212
|
+
}
|
|
3213
|
+
function parseFeatureText(text, file) {
|
|
3214
|
+
const doc = newParser().parse(text);
|
|
3215
|
+
const feature = doc.feature;
|
|
3216
|
+
if (!feature) {
|
|
3217
|
+
return { name: "", description: "", tags: [], background: [], scenarios: [], file };
|
|
3218
|
+
}
|
|
3219
|
+
let background = [];
|
|
3220
|
+
const scenarios = [];
|
|
3221
|
+
for (const child of feature.children ?? []) {
|
|
3222
|
+
if (child.background) {
|
|
3223
|
+
background = normalizeSteps(child.background.steps ?? []);
|
|
3224
|
+
} else if (child.scenario) {
|
|
3225
|
+
scenarios.push(...buildScenarios(child.scenario));
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
return {
|
|
3229
|
+
name: feature.name,
|
|
3230
|
+
description: (feature.description ?? "").trim(),
|
|
3231
|
+
tags: tagNames(feature.tags),
|
|
3232
|
+
background,
|
|
3233
|
+
scenarios,
|
|
3234
|
+
file
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
async function parseFeatureFile(path3) {
|
|
3238
|
+
const text = await Bun.file(path3).text();
|
|
3239
|
+
return parseFeatureText(text, path3);
|
|
3240
|
+
}
|
|
3241
|
+
var init_parse = () => {};
|
|
3242
|
+
|
|
3243
|
+
// tools/bdd/src/steps.ts
|
|
3244
|
+
function state() {
|
|
3245
|
+
globalThis.__pollyBddRegistry__ ??= { bindings: [], worldDef: null };
|
|
3246
|
+
return globalThis.__pollyBddRegistry__;
|
|
3247
|
+
}
|
|
3248
|
+
function compilePattern(pattern) {
|
|
3249
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3250
|
+
const withGroups = escaped.replace(/\\\{string\\\}/g, `(?:"([^"]*)"|'([^']*)')`).replace(/\\\{int\\\}/g, "([-+]?\\d+)").replace(/\\\{float\\\}/g, "([-+]?\\d*\\.?\\d+)").replace(/\\\{word\\\}/g, "([^\\s]+)");
|
|
3251
|
+
return new RegExp(`^${withGroups}$`);
|
|
3252
|
+
}
|
|
3253
|
+
function defineStep(binding) {
|
|
3254
|
+
state().bindings.push({ binding, regex: compilePattern(binding.pattern) });
|
|
3255
|
+
}
|
|
3256
|
+
function defineWorld(def) {
|
|
3257
|
+
state().worldDef = def;
|
|
3258
|
+
}
|
|
3259
|
+
function getWorldDef() {
|
|
3260
|
+
return state().worldDef;
|
|
3261
|
+
}
|
|
3262
|
+
function resetRegistry() {
|
|
3263
|
+
const s = state();
|
|
3264
|
+
s.bindings.length = 0;
|
|
3265
|
+
s.worldDef = null;
|
|
3266
|
+
}
|
|
3267
|
+
function matchStep(text, keyword) {
|
|
3268
|
+
let textOnlyFallback = null;
|
|
3269
|
+
for (const { binding, regex } of state().bindings) {
|
|
3270
|
+
const m = regex.exec(text);
|
|
3271
|
+
if (!m)
|
|
3272
|
+
continue;
|
|
3273
|
+
const args = m.slice(1).filter((g) => g !== undefined);
|
|
3274
|
+
if (!keyword || binding[keyword])
|
|
3275
|
+
return { binding, args };
|
|
3276
|
+
textOnlyFallback ??= { binding, args };
|
|
3277
|
+
}
|
|
3278
|
+
return textOnlyFallback;
|
|
3279
|
+
}
|
|
3280
|
+
function registeredBindings() {
|
|
3281
|
+
return state().bindings.map((c) => c.binding);
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// tools/bdd/src/witness.ts
|
|
3285
|
+
var exports_witness = {};
|
|
3286
|
+
__export(exports_witness, {
|
|
3287
|
+
extractWitnesses: () => extractWitnesses
|
|
3288
|
+
});
|
|
3289
|
+
async function loadStepModules(stepFiles) {
|
|
3290
|
+
resetRegistry();
|
|
3291
|
+
for (const file of stepFiles) {
|
|
3292
|
+
await import(`${file}?t=${Bun.nanoseconds()}`);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
function substituteArgs(expr, args) {
|
|
3296
|
+
return expr.replace(/\{(\d+)\}/g, (whole, index) => args[Number(index)] ?? whole);
|
|
3297
|
+
}
|
|
3298
|
+
function fieldsIn(expr) {
|
|
3299
|
+
const noStrings = expr.replace(/"[^"]*"|'[^']*'/g, "");
|
|
3300
|
+
const ids = noStrings.match(/[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*/g) ?? [];
|
|
3301
|
+
const ignore = new Set(["true", "false", "null", "undefined", "length", "value"]);
|
|
3302
|
+
return ids.filter((id) => !ignore.has(id) && Number.isNaN(Number(id))).map((id) => id.replace(/\.length$/, ""));
|
|
3303
|
+
}
|
|
3304
|
+
function reduceScenario(feature, scenario) {
|
|
3305
|
+
const thenSteps = [...feature.background, ...scenario.steps].filter((s) => s.keyword === "then");
|
|
3306
|
+
const conjuncts = [];
|
|
3307
|
+
const skipped = [];
|
|
3308
|
+
for (const step of thenSteps) {
|
|
3309
|
+
const match = matchStep(step.text, "then");
|
|
3310
|
+
const expr = match?.binding.stateExpr;
|
|
3311
|
+
if (!expr) {
|
|
3312
|
+
skipped.push(step.text);
|
|
3313
|
+
continue;
|
|
3314
|
+
}
|
|
3315
|
+
const resolved = substituteArgs(expr, match.args);
|
|
3316
|
+
if (!COMPARISON.test(resolved)) {
|
|
3317
|
+
skipped.push(step.text);
|
|
3318
|
+
continue;
|
|
3319
|
+
}
|
|
3320
|
+
conjuncts.push(resolved);
|
|
3321
|
+
}
|
|
3322
|
+
const predicate = conjuncts.length > 0 ? conjuncts.join(" && ") : null;
|
|
3323
|
+
const fields = [...new Set(conjuncts.flatMap(fieldsIn))];
|
|
3324
|
+
return {
|
|
3325
|
+
feature: feature.name,
|
|
3326
|
+
scenario: scenario.name,
|
|
3327
|
+
tags: [...feature.tags, ...scenario.tags],
|
|
3328
|
+
file: feature.file,
|
|
3329
|
+
predicate,
|
|
3330
|
+
fields,
|
|
3331
|
+
skipped
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
async function extractWitnesses(featureFiles, stepFiles) {
|
|
3335
|
+
await loadStepModules(stepFiles);
|
|
3336
|
+
const witnesses = [];
|
|
3337
|
+
for (const file of featureFiles) {
|
|
3338
|
+
const feature = await parseFeatureFile(file);
|
|
3339
|
+
for (const scenario of feature.scenarios) {
|
|
3340
|
+
witnesses.push(reduceScenario({
|
|
3341
|
+
name: feature.name,
|
|
3342
|
+
tags: feature.tags,
|
|
3343
|
+
background: feature.background,
|
|
3344
|
+
file: feature.file
|
|
3345
|
+
}, scenario));
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
return witnesses;
|
|
3349
|
+
}
|
|
3350
|
+
var COMPARISON;
|
|
3351
|
+
var init_witness = __esm(() => {
|
|
3352
|
+
init_parse();
|
|
3353
|
+
COMPARISON = /===|!==|==|!=|<=|>=|<|>/;
|
|
3354
|
+
});
|
|
3355
|
+
|
|
3356
|
+
// tools/verify/src/codegen/witness.ts
|
|
3357
|
+
var exports_witness2 = {};
|
|
3358
|
+
__export(exports_witness2, {
|
|
3359
|
+
routeWitness: () => routeWitness,
|
|
3360
|
+
buildWitnessModule: () => buildWitnessModule,
|
|
3361
|
+
buildWitnessInvariant: () => buildWitnessInvariant,
|
|
3362
|
+
buildWitnessCfg: () => buildWitnessCfg,
|
|
3363
|
+
bddPredicateToTLA: () => bddPredicateToTLA,
|
|
3364
|
+
WitnessTranslationError: () => WitnessTranslationError,
|
|
3365
|
+
WITNESS_INVARIANT: () => WITNESS_INVARIANT
|
|
3366
|
+
});
|
|
3367
|
+
function flattenField(path3) {
|
|
3368
|
+
return path3.split(".").join("_");
|
|
3369
|
+
}
|
|
3370
|
+
function translateOperand(raw) {
|
|
3371
|
+
const op = raw.trim();
|
|
3372
|
+
if (op === "true")
|
|
3373
|
+
return "TRUE";
|
|
3374
|
+
if (op === "false")
|
|
3375
|
+
return "FALSE";
|
|
3376
|
+
if (/^"[^"]*"$/.test(op))
|
|
3377
|
+
return op;
|
|
3378
|
+
if (/^'[^']*'$/.test(op))
|
|
3379
|
+
return `"${op.slice(1, -1)}"`;
|
|
3380
|
+
if (/^[-+]?\d+(?:\.\d+)?$/.test(op))
|
|
3381
|
+
return op;
|
|
3382
|
+
if (!/^[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*$/.test(op)) {
|
|
3383
|
+
throw new WitnessTranslationError(`unsupported operand "${raw}" in witness predicate`);
|
|
3384
|
+
}
|
|
3385
|
+
if (op.endsWith(".length")) {
|
|
3386
|
+
return `Len(contextStates[ctx].${flattenField(op.slice(0, -".length".length))})`;
|
|
3387
|
+
}
|
|
3388
|
+
return `contextStates[ctx].${flattenField(op)}`;
|
|
3389
|
+
}
|
|
3390
|
+
function translateConjunct(conjunct) {
|
|
3391
|
+
const text = conjunct.trim();
|
|
3392
|
+
for (const [js, tla] of COMPARATORS) {
|
|
3393
|
+
const at = text.indexOf(js);
|
|
3394
|
+
if (at === -1)
|
|
3395
|
+
continue;
|
|
3396
|
+
const lhs = text.slice(0, at);
|
|
3397
|
+
const rhs = text.slice(at + js.length);
|
|
3398
|
+
return `${translateOperand(lhs)} ${tla} ${translateOperand(rhs)}`;
|
|
3399
|
+
}
|
|
3400
|
+
throw new WitnessTranslationError(`no comparison operator in witness conjunct "${conjunct}"`);
|
|
3401
|
+
}
|
|
3402
|
+
function bddPredicateToTLA(predicate) {
|
|
3403
|
+
const conjuncts = predicate.split("&&").map((c) => c.trim()).filter(Boolean);
|
|
3404
|
+
if (conjuncts.length === 0) {
|
|
3405
|
+
throw new WitnessTranslationError("empty witness predicate");
|
|
3406
|
+
}
|
|
3407
|
+
return conjuncts.map(translateConjunct).join(" /\\ ");
|
|
3408
|
+
}
|
|
3409
|
+
function buildWitnessInvariant(tlaPredicate) {
|
|
3410
|
+
return `${WITNESS_INVARIANT} == ~(\\E ctx \\in Contexts : ${tlaPredicate})`;
|
|
3411
|
+
}
|
|
3412
|
+
function buildWitnessModule(moduleName, subsystemModule, tlaPredicate) {
|
|
3413
|
+
return [
|
|
3414
|
+
`---- MODULE ${moduleName} ----`,
|
|
3415
|
+
`EXTENDS ${subsystemModule}`,
|
|
3416
|
+
"",
|
|
3417
|
+
buildWitnessInvariant(tlaPredicate),
|
|
3418
|
+
"====",
|
|
3419
|
+
""
|
|
3420
|
+
].join(`
|
|
3421
|
+
`);
|
|
3422
|
+
}
|
|
3423
|
+
function sectionBody(cfg, header) {
|
|
3424
|
+
const body = [];
|
|
3425
|
+
let inSection = false;
|
|
3426
|
+
for (const line of cfg.split(`
|
|
3427
|
+
`)) {
|
|
3428
|
+
const headerMatch = /^([A-Z_]+)\b/.exec(line);
|
|
3429
|
+
if (headerMatch) {
|
|
3430
|
+
inSection = headerMatch[1] === header;
|
|
3431
|
+
continue;
|
|
3432
|
+
}
|
|
3433
|
+
if (!inSection)
|
|
3434
|
+
continue;
|
|
3435
|
+
if (line.trim() === "" || line.trim().startsWith("\\*"))
|
|
3436
|
+
continue;
|
|
3437
|
+
body.push(line);
|
|
3438
|
+
}
|
|
3439
|
+
return body;
|
|
3440
|
+
}
|
|
3441
|
+
function headerLine(cfg, header) {
|
|
3442
|
+
for (const line of cfg.split(`
|
|
3443
|
+
`)) {
|
|
3444
|
+
const m = new RegExp(`^${header}\\b(.*)$`).exec(line);
|
|
3445
|
+
if (m)
|
|
3446
|
+
return line.trimEnd();
|
|
3447
|
+
}
|
|
3448
|
+
return null;
|
|
3449
|
+
}
|
|
3450
|
+
function buildWitnessCfg(baseCfg) {
|
|
3451
|
+
const spec = headerLine(baseCfg, "SPECIFICATION") ?? "SPECIFICATION UserSpec";
|
|
3452
|
+
const constants = sectionBody(baseCfg, "CONSTANTS");
|
|
3453
|
+
const constraint = sectionBody(baseCfg, "CONSTRAINT");
|
|
3454
|
+
const symmetry = sectionBody(baseCfg, "SYMMETRY");
|
|
3455
|
+
const out = [spec, "", "CONSTANTS", ...constants, "", "INVARIANTS", ` ${WITNESS_INVARIANT}`];
|
|
3456
|
+
if (constraint.length > 0)
|
|
3457
|
+
out.push("", "CONSTRAINT", ...constraint);
|
|
3458
|
+
if (symmetry.length > 0)
|
|
3459
|
+
out.push("", "SYMMETRY", ...symmetry);
|
|
3460
|
+
return `${out.join(`
|
|
3461
|
+
`)}
|
|
3462
|
+
`;
|
|
3463
|
+
}
|
|
3464
|
+
function covers(stateKeys, field) {
|
|
3465
|
+
return stateKeys.some((k) => k === field || field.startsWith(`${k}.`) || k.startsWith(`${field}.`));
|
|
3466
|
+
}
|
|
3467
|
+
function routeWitness(fields, subsystems) {
|
|
3468
|
+
if (fields.length === 0)
|
|
3469
|
+
return null;
|
|
3470
|
+
const owners = Object.entries(subsystems).filter(([, sub]) => fields.every((f) => covers(sub.state, f)));
|
|
3471
|
+
const only = owners.length === 1 ? owners[0] : undefined;
|
|
3472
|
+
return only ? only[0] : null;
|
|
3473
|
+
}
|
|
3474
|
+
var WITNESS_INVARIANT = "WitnessReachable", WitnessTranslationError, COMPARATORS;
|
|
3475
|
+
var init_witness2 = __esm(() => {
|
|
3476
|
+
WitnessTranslationError = class WitnessTranslationError extends Error {
|
|
3477
|
+
};
|
|
3478
|
+
COMPARATORS = [
|
|
3479
|
+
["===", "="],
|
|
3480
|
+
["!==", "#"],
|
|
3481
|
+
["==", "="],
|
|
3482
|
+
["!=", "#"],
|
|
3483
|
+
[">=", ">="],
|
|
3484
|
+
["<=", "<="],
|
|
3485
|
+
[">", ">"],
|
|
3486
|
+
["<", "<"]
|
|
3487
|
+
];
|
|
3488
|
+
});
|
|
3489
|
+
|
|
3141
3490
|
// tools/verify/src/runner/docker.ts
|
|
3142
3491
|
var exports_docker = {};
|
|
3143
3492
|
__export(exports_docker, {
|
|
@@ -8090,6 +8439,9 @@ function displayMeshOrPeerSignalWarnings(analysis, declaredMeshDocs) {
|
|
|
8090
8439
|
function isStrictMode() {
|
|
8091
8440
|
return process.argv.includes("--strict") || process.env.POLLY_VERIFY_STRICT === "1";
|
|
8092
8441
|
}
|
|
8442
|
+
function isWitnessMode() {
|
|
8443
|
+
return process.argv.includes("--witness");
|
|
8444
|
+
}
|
|
8093
8445
|
function displayModelCoverage(report) {
|
|
8094
8446
|
const { unwrittenFields, unconstrainedMutators, fieldCoverage } = report;
|
|
8095
8447
|
if (unwrittenFields.length === 0 && unconstrainedMutators.length === 0) {
|
|
@@ -8246,9 +8598,17 @@ async function runFullVerification(configPath) {
|
|
|
8246
8598
|
await runModelCoverage(typedConfig, typedAnalysis, meshFindingCount);
|
|
8247
8599
|
if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
|
|
8248
8600
|
await runSubsystemVerification(typedConfig, typedAnalysis);
|
|
8601
|
+
if (isWitnessMode()) {
|
|
8602
|
+
await runWitnessVerification(typedConfig);
|
|
8603
|
+
}
|
|
8249
8604
|
return;
|
|
8250
8605
|
}
|
|
8251
8606
|
await runMonolithicVerification(config, analysis);
|
|
8607
|
+
if (isWitnessMode()) {
|
|
8608
|
+
console.log(color("\n⚠ --witness needs subsystem-scoped verification (a `subsystems` block in the config);", COLORS.yellow));
|
|
8609
|
+
console.log(color(` reachability witnesses route each scenario to its subsystem spec.
|
|
8610
|
+
`, COLORS.yellow));
|
|
8611
|
+
}
|
|
8252
8612
|
}
|
|
8253
8613
|
async function runMonolithicVerification(config, analysis) {
|
|
8254
8614
|
const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
|
|
@@ -8393,6 +8753,160 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
8393
8753
|
console.log();
|
|
8394
8754
|
displayCompositionalReport(results, interference.valid);
|
|
8395
8755
|
}
|
|
8756
|
+
var DEFAULT_WITNESS_TIMEOUT_MS = 180000;
|
|
8757
|
+
async function globAbsolute(cwd, pattern) {
|
|
8758
|
+
const out = [];
|
|
8759
|
+
for await (const f of new Bun.Glob(pattern).scan({ cwd, absolute: true, onlyFiles: true })) {
|
|
8760
|
+
out.push(f);
|
|
8761
|
+
}
|
|
8762
|
+
return out.sort();
|
|
8763
|
+
}
|
|
8764
|
+
async function runWitnessVerification(config) {
|
|
8765
|
+
console.log(color(`Reachability witnesses (--witness)
|
|
8766
|
+
`, COLORS.blue));
|
|
8767
|
+
const cwd = process.cwd();
|
|
8768
|
+
const featureFiles = await globAbsolute(cwd, "features/**/*.feature");
|
|
8769
|
+
const stepFiles = [
|
|
8770
|
+
...new Set([
|
|
8771
|
+
...await globAbsolute(cwd, "features/**/*.steps.ts"),
|
|
8772
|
+
...await globAbsolute(cwd, "features/steps.ts")
|
|
8773
|
+
])
|
|
8774
|
+
];
|
|
8775
|
+
if (featureFiles.length === 0 || stepFiles.length === 0) {
|
|
8776
|
+
console.log(color(` No .feature files / step modules found — nothing to witness.
|
|
8777
|
+
`, COLORS.gray));
|
|
8778
|
+
return;
|
|
8779
|
+
}
|
|
8780
|
+
const { extractWitnesses: extractWitnesses2 } = await Promise.resolve().then(() => (init_witness(), exports_witness));
|
|
8781
|
+
const {
|
|
8782
|
+
bddPredicateToTLA: bddPredicateToTLA2,
|
|
8783
|
+
buildWitnessCfg: buildWitnessCfg2,
|
|
8784
|
+
buildWitnessModule: buildWitnessModule2,
|
|
8785
|
+
routeWitness: routeWitness2,
|
|
8786
|
+
WITNESS_INVARIANT: WITNESS_INVARIANT2
|
|
8787
|
+
} = await Promise.resolve().then(() => (init_witness2(), exports_witness2));
|
|
8788
|
+
const subsystems = config.subsystems;
|
|
8789
|
+
const witnesses = await extractWitnesses2(featureFiles, stepFiles);
|
|
8790
|
+
const docker = await setupDocker();
|
|
8791
|
+
const timeoutSeconds = getTimeout(config);
|
|
8792
|
+
const timeout = timeoutSeconds > 0 ? timeoutSeconds * 1000 : DEFAULT_WITNESS_TIMEOUT_MS;
|
|
8793
|
+
const workers = getWorkers(config);
|
|
8794
|
+
const results = [];
|
|
8795
|
+
let idx = 0;
|
|
8796
|
+
for (const w of witnesses) {
|
|
8797
|
+
const id = `${w.feature} › ${w.scenario}`;
|
|
8798
|
+
if (!w.predicate) {
|
|
8799
|
+
results.push({ id, status: "skipped", note: "no state-observable Then to witness" });
|
|
8800
|
+
continue;
|
|
8801
|
+
}
|
|
8802
|
+
const subsystem = routeWitness2(w.fields, subsystems);
|
|
8803
|
+
if (!subsystem) {
|
|
8804
|
+
results.push({
|
|
8805
|
+
id,
|
|
8806
|
+
status: "skipped",
|
|
8807
|
+
note: `fields [${w.fields.join(", ")}] not owned by a single subsystem`
|
|
8808
|
+
});
|
|
8809
|
+
continue;
|
|
8810
|
+
}
|
|
8811
|
+
const specDir = path4.join(cwd, "specs", "tla", "generated", subsystem);
|
|
8812
|
+
const subsystemCfg = path4.join(specDir, `UserApp_${subsystem}.cfg`);
|
|
8813
|
+
if (!fs4.existsSync(path4.join(specDir, `UserApp_${subsystem}.tla`)) || !fs4.existsSync(subsystemCfg)) {
|
|
8814
|
+
results.push({
|
|
8815
|
+
id,
|
|
8816
|
+
status: "error",
|
|
8817
|
+
subsystem,
|
|
8818
|
+
note: `subsystem spec missing: ${subsystem}`
|
|
8819
|
+
});
|
|
8820
|
+
continue;
|
|
8821
|
+
}
|
|
8822
|
+
let tlaPredicate;
|
|
8823
|
+
try {
|
|
8824
|
+
tlaPredicate = bddPredicateToTLA2(w.predicate);
|
|
8825
|
+
} catch (err) {
|
|
8826
|
+
results.push({
|
|
8827
|
+
id,
|
|
8828
|
+
status: "error",
|
|
8829
|
+
subsystem,
|
|
8830
|
+
note: err instanceof Error ? err.message : String(err)
|
|
8831
|
+
});
|
|
8832
|
+
continue;
|
|
8833
|
+
}
|
|
8834
|
+
const moduleName = `Witness_${subsystem}_${idx++}`;
|
|
8835
|
+
const witnessTla = path4.join(specDir, `${moduleName}.tla`);
|
|
8836
|
+
fs4.writeFileSync(witnessTla, buildWitnessModule2(moduleName, `UserApp_${subsystem}`, tlaPredicate));
|
|
8837
|
+
fs4.writeFileSync(path4.join(specDir, `${moduleName}.cfg`), buildWitnessCfg2(fs4.readFileSync(subsystemCfg, "utf8")));
|
|
8838
|
+
console.log(color(`⚙️ ${id}`, COLORS.blue));
|
|
8839
|
+
console.log(color(` ${subsystem} ⊨ ${w.predicate}`, COLORS.gray));
|
|
8840
|
+
let tlc;
|
|
8841
|
+
try {
|
|
8842
|
+
tlc = await docker.runTLC(witnessTla, { workers, timeout });
|
|
8843
|
+
} catch (err) {
|
|
8844
|
+
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
|
+
results.push({
|
|
8854
|
+
id,
|
|
8855
|
+
status: "unreachable",
|
|
8856
|
+
subsystem,
|
|
8857
|
+
predicate: w.predicate,
|
|
8858
|
+
note: `${tlc.stats?.distinctStates ?? 0} distinct states, no path`
|
|
8859
|
+
});
|
|
8860
|
+
console.log(color(" ✗ UNREACHABLE — the model proves this outcome impossible (the scenario lies)", COLORS.red));
|
|
8861
|
+
fs4.writeFileSync(path4.join(specDir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8862
|
+
} else {
|
|
8863
|
+
results.push({
|
|
8864
|
+
id,
|
|
8865
|
+
status: "error",
|
|
8866
|
+
subsystem,
|
|
8867
|
+
predicate: w.predicate,
|
|
8868
|
+
note: tlc.error ?? tlc.violation?.name ?? "TLC error"
|
|
8869
|
+
});
|
|
8870
|
+
console.log(color(` ! error — ${tlc.error ?? "see log"}`, COLORS.yellow));
|
|
8871
|
+
fs4.writeFileSync(path4.join(specDir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8872
|
+
}
|
|
8873
|
+
}
|
|
8874
|
+
displayWitnessReport(results);
|
|
8875
|
+
}
|
|
8876
|
+
function displayWitnessReport(results) {
|
|
8877
|
+
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
|
+
console.log();
|
|
8884
|
+
console.log(color(`Witness results:
|
|
8885
|
+
`, COLORS.blue));
|
|
8886
|
+
for (const r of results) {
|
|
8887
|
+
const mark = {
|
|
8888
|
+
reachable: color("✓", COLORS.green),
|
|
8889
|
+
unreachable: color("✗", COLORS.red),
|
|
8890
|
+
inconclusive: color("⚠", COLORS.yellow),
|
|
8891
|
+
error: color("!", COLORS.yellow),
|
|
8892
|
+
skipped: color("·", COLORS.gray)
|
|
8893
|
+
}[r.status];
|
|
8894
|
+
const note = r.note ? color(` (${r.note})`, COLORS.gray) : "";
|
|
8895
|
+
console.log(` ${mark} ${r.status.padEnd(12)} ${r.id}${note}`);
|
|
8896
|
+
}
|
|
8897
|
+
console.log();
|
|
8898
|
+
console.log(color(` ${reachable} reachable, ${unreachable} unreachable, ${inconclusive} inconclusive, ${errors} error, ${skipped} skipped`, COLORS.gray));
|
|
8899
|
+
console.log();
|
|
8900
|
+
if (unreachable > 0 || errors > 0) {
|
|
8901
|
+
console.log(color("Witness result: ✗ FAIL", COLORS.red));
|
|
8902
|
+
console.log(color(" A scenario the exhaustive model cannot reach describes an impossible outcome.", COLORS.gray));
|
|
8903
|
+
console.log();
|
|
8904
|
+
process.exit(1);
|
|
8905
|
+
}
|
|
8906
|
+
console.log(color("Witness result: ✓ PASS", COLORS.green));
|
|
8907
|
+
console.log(color(" Every witnessable scenario's outcome is reachable in the model.", COLORS.gray));
|
|
8908
|
+
console.log();
|
|
8909
|
+
}
|
|
8396
8910
|
function displayEnsuresSummary(results) {
|
|
8397
8911
|
const totalEnsures = results.reduce((sum, r) => sum + r.ensuresCount, 0);
|
|
8398
8912
|
if (totalEnsures === 0)
|
|
@@ -8630,6 +9144,12 @@ ${color("Commands:", COLORS.blue)}
|
|
|
8630
9144
|
field no handler writes, or an unverified $meshState/$peerState predicate.
|
|
8631
9145
|
Also via ${color("POLLY_VERIFY_STRICT=1", COLORS.yellow)}.
|
|
8632
9146
|
|
|
9147
|
+
${color("polly verify --witness", COLORS.green)}
|
|
9148
|
+
After the invariant pass, check each BDD scenario's Then-outcome for
|
|
9149
|
+
reachability: one TLC run per witnessable scenario over its subsystem spec.
|
|
9150
|
+
A scenario the exhaustive model proves unreachable is one that lies, and
|
|
9151
|
+
fails the run. Needs a ${color("subsystems", COLORS.blue)} block and ${color("features/", COLORS.blue)} (see polly bdd).
|
|
9152
|
+
|
|
8633
9153
|
${color("polly verify --setup", COLORS.green)}
|
|
8634
9154
|
Analyze codebase and generate configuration file
|
|
8635
9155
|
|
|
@@ -8693,4 +9213,4 @@ main().catch((error) => {
|
|
|
8693
9213
|
process.exit(1);
|
|
8694
9214
|
});
|
|
8695
9215
|
|
|
8696
|
-
//# debugId=
|
|
9216
|
+
//# debugId=006B6ED4C6E5114F64756E2164756E21
|