@fairfox/polly 0.82.1 → 0.84.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 +705 -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 +544 -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 +152 -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 +569 -1
- package/dist/tools/verify/src/cli.js.map +8 -4
- package/package.json +7 -1
|
@@ -3138,6 +3138,374 @@ 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
|
+
witnessVerdict: () => witnessVerdict,
|
|
3360
|
+
witnessPolarity: () => witnessPolarity,
|
|
3361
|
+
routeWitness: () => routeWitness,
|
|
3362
|
+
buildWitnessModule: () => buildWitnessModule,
|
|
3363
|
+
buildWitnessInvariant: () => buildWitnessInvariant,
|
|
3364
|
+
buildWitnessCfg: () => buildWitnessCfg,
|
|
3365
|
+
bddPredicateToTLA: () => bddPredicateToTLA,
|
|
3366
|
+
WitnessTranslationError: () => WitnessTranslationError,
|
|
3367
|
+
WITNESS_INVARIANT: () => WITNESS_INVARIANT
|
|
3368
|
+
});
|
|
3369
|
+
function witnessPolarity(tags) {
|
|
3370
|
+
return tags.includes("forbidden") ? "forbidden" : "positive";
|
|
3371
|
+
}
|
|
3372
|
+
function witnessVerdict(polarity, reachable) {
|
|
3373
|
+
if (polarity === "forbidden") {
|
|
3374
|
+
return reachable ? {
|
|
3375
|
+
status: "violated",
|
|
3376
|
+
ok: false,
|
|
3377
|
+
message: "the forbidden state is REACHABLE — a defect; the counterexample is the path to it"
|
|
3378
|
+
} : { status: "excluded", ok: true, message: "the model proves this state unreachable" };
|
|
3379
|
+
}
|
|
3380
|
+
return reachable ? { status: "reachable", ok: true, message: "the model reaches this outcome" } : {
|
|
3381
|
+
status: "unreachable",
|
|
3382
|
+
ok: false,
|
|
3383
|
+
message: "the model proves this outcome impossible (the scenario lies)"
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
function flattenField(path3) {
|
|
3387
|
+
return path3.split(".").join("_");
|
|
3388
|
+
}
|
|
3389
|
+
function translateOperand(raw) {
|
|
3390
|
+
const op = raw.trim();
|
|
3391
|
+
if (op === "true")
|
|
3392
|
+
return "TRUE";
|
|
3393
|
+
if (op === "false")
|
|
3394
|
+
return "FALSE";
|
|
3395
|
+
if (/^"[^"]*"$/.test(op))
|
|
3396
|
+
return op;
|
|
3397
|
+
if (/^'[^']*'$/.test(op))
|
|
3398
|
+
return `"${op.slice(1, -1)}"`;
|
|
3399
|
+
if (/^[-+]?\d+(?:\.\d+)?$/.test(op))
|
|
3400
|
+
return op;
|
|
3401
|
+
if (!/^[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*$/.test(op)) {
|
|
3402
|
+
throw new WitnessTranslationError(`unsupported operand "${raw}" in witness predicate`);
|
|
3403
|
+
}
|
|
3404
|
+
if (op.endsWith(".length")) {
|
|
3405
|
+
return `Len(contextStates[ctx].${flattenField(op.slice(0, -".length".length))})`;
|
|
3406
|
+
}
|
|
3407
|
+
return `contextStates[ctx].${flattenField(op)}`;
|
|
3408
|
+
}
|
|
3409
|
+
function translateConjunct(conjunct) {
|
|
3410
|
+
const text = conjunct.trim();
|
|
3411
|
+
for (const [js, tla] of COMPARATORS) {
|
|
3412
|
+
const at = text.indexOf(js);
|
|
3413
|
+
if (at === -1)
|
|
3414
|
+
continue;
|
|
3415
|
+
const lhs = text.slice(0, at);
|
|
3416
|
+
const rhs = text.slice(at + js.length);
|
|
3417
|
+
return `${translateOperand(lhs)} ${tla} ${translateOperand(rhs)}`;
|
|
3418
|
+
}
|
|
3419
|
+
throw new WitnessTranslationError(`no comparison operator in witness conjunct "${conjunct}"`);
|
|
3420
|
+
}
|
|
3421
|
+
function bddPredicateToTLA(predicate) {
|
|
3422
|
+
const conjuncts = predicate.split("&&").map((c) => c.trim()).filter(Boolean);
|
|
3423
|
+
if (conjuncts.length === 0) {
|
|
3424
|
+
throw new WitnessTranslationError("empty witness predicate");
|
|
3425
|
+
}
|
|
3426
|
+
return conjuncts.map(translateConjunct).join(" /\\ ");
|
|
3427
|
+
}
|
|
3428
|
+
function buildWitnessInvariant(tlaPredicate) {
|
|
3429
|
+
return `${WITNESS_INVARIANT} == ~(\\E ctx \\in Contexts : ${tlaPredicate})`;
|
|
3430
|
+
}
|
|
3431
|
+
function buildWitnessModule(moduleName, subsystemModule, tlaPredicate) {
|
|
3432
|
+
return [
|
|
3433
|
+
`---- MODULE ${moduleName} ----`,
|
|
3434
|
+
`EXTENDS ${subsystemModule}`,
|
|
3435
|
+
"",
|
|
3436
|
+
buildWitnessInvariant(tlaPredicate),
|
|
3437
|
+
"====",
|
|
3438
|
+
""
|
|
3439
|
+
].join(`
|
|
3440
|
+
`);
|
|
3441
|
+
}
|
|
3442
|
+
function sectionBody(cfg, header) {
|
|
3443
|
+
const body = [];
|
|
3444
|
+
let inSection = false;
|
|
3445
|
+
for (const line of cfg.split(`
|
|
3446
|
+
`)) {
|
|
3447
|
+
const headerMatch = /^([A-Z_]+)\b/.exec(line);
|
|
3448
|
+
if (headerMatch) {
|
|
3449
|
+
inSection = headerMatch[1] === header;
|
|
3450
|
+
continue;
|
|
3451
|
+
}
|
|
3452
|
+
if (!inSection)
|
|
3453
|
+
continue;
|
|
3454
|
+
if (line.trim() === "" || line.trim().startsWith("\\*"))
|
|
3455
|
+
continue;
|
|
3456
|
+
body.push(line);
|
|
3457
|
+
}
|
|
3458
|
+
return body;
|
|
3459
|
+
}
|
|
3460
|
+
function headerLine(cfg, header) {
|
|
3461
|
+
for (const line of cfg.split(`
|
|
3462
|
+
`)) {
|
|
3463
|
+
const m = new RegExp(`^${header}\\b(.*)$`).exec(line);
|
|
3464
|
+
if (m)
|
|
3465
|
+
return line.trimEnd();
|
|
3466
|
+
}
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
function buildWitnessCfg(baseCfg) {
|
|
3470
|
+
const spec = headerLine(baseCfg, "SPECIFICATION") ?? "SPECIFICATION UserSpec";
|
|
3471
|
+
const constants = sectionBody(baseCfg, "CONSTANTS");
|
|
3472
|
+
const constraint = sectionBody(baseCfg, "CONSTRAINT");
|
|
3473
|
+
const symmetry = sectionBody(baseCfg, "SYMMETRY");
|
|
3474
|
+
const out = [spec, "", "CONSTANTS", ...constants, "", "INVARIANTS", ` ${WITNESS_INVARIANT}`];
|
|
3475
|
+
if (constraint.length > 0)
|
|
3476
|
+
out.push("", "CONSTRAINT", ...constraint);
|
|
3477
|
+
if (symmetry.length > 0)
|
|
3478
|
+
out.push("", "SYMMETRY", ...symmetry);
|
|
3479
|
+
return `${out.join(`
|
|
3480
|
+
`)}
|
|
3481
|
+
`;
|
|
3482
|
+
}
|
|
3483
|
+
function covers(stateKeys, field) {
|
|
3484
|
+
return stateKeys.some((k) => k === field || field.startsWith(`${k}.`) || k.startsWith(`${field}.`));
|
|
3485
|
+
}
|
|
3486
|
+
function routeWitness(fields, subsystems) {
|
|
3487
|
+
if (fields.length === 0)
|
|
3488
|
+
return null;
|
|
3489
|
+
const owners = Object.entries(subsystems).filter(([, sub]) => fields.every((f) => covers(sub.state, f)));
|
|
3490
|
+
const only = owners.length === 1 ? owners[0] : undefined;
|
|
3491
|
+
return only ? only[0] : null;
|
|
3492
|
+
}
|
|
3493
|
+
var WITNESS_INVARIANT = "WitnessReachable", WitnessTranslationError, COMPARATORS;
|
|
3494
|
+
var init_witness2 = __esm(() => {
|
|
3495
|
+
WitnessTranslationError = class WitnessTranslationError extends Error {
|
|
3496
|
+
};
|
|
3497
|
+
COMPARATORS = [
|
|
3498
|
+
["===", "="],
|
|
3499
|
+
["!==", "#"],
|
|
3500
|
+
["==", "="],
|
|
3501
|
+
["!=", "#"],
|
|
3502
|
+
[">=", ">="],
|
|
3503
|
+
["<=", "<="],
|
|
3504
|
+
[">", ">"],
|
|
3505
|
+
["<", "<"]
|
|
3506
|
+
];
|
|
3507
|
+
});
|
|
3508
|
+
|
|
3141
3509
|
// tools/verify/src/runner/docker.ts
|
|
3142
3510
|
var exports_docker = {};
|
|
3143
3511
|
__export(exports_docker, {
|
|
@@ -8090,6 +8458,9 @@ function displayMeshOrPeerSignalWarnings(analysis, declaredMeshDocs) {
|
|
|
8090
8458
|
function isStrictMode() {
|
|
8091
8459
|
return process.argv.includes("--strict") || process.env.POLLY_VERIFY_STRICT === "1";
|
|
8092
8460
|
}
|
|
8461
|
+
function isWitnessMode() {
|
|
8462
|
+
return process.argv.includes("--witness");
|
|
8463
|
+
}
|
|
8093
8464
|
function displayModelCoverage(report) {
|
|
8094
8465
|
const { unwrittenFields, unconstrainedMutators, fieldCoverage } = report;
|
|
8095
8466
|
if (unwrittenFields.length === 0 && unconstrainedMutators.length === 0) {
|
|
@@ -8246,9 +8617,17 @@ async function runFullVerification(configPath) {
|
|
|
8246
8617
|
await runModelCoverage(typedConfig, typedAnalysis, meshFindingCount);
|
|
8247
8618
|
if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
|
|
8248
8619
|
await runSubsystemVerification(typedConfig, typedAnalysis);
|
|
8620
|
+
if (isWitnessMode()) {
|
|
8621
|
+
await runWitnessVerification(typedConfig);
|
|
8622
|
+
}
|
|
8249
8623
|
return;
|
|
8250
8624
|
}
|
|
8251
8625
|
await runMonolithicVerification(config, analysis);
|
|
8626
|
+
if (isWitnessMode()) {
|
|
8627
|
+
console.log(color("\n⚠ --witness needs subsystem-scoped verification (a `subsystems` block in the config);", COLORS.yellow));
|
|
8628
|
+
console.log(color(` reachability witnesses route each scenario to its subsystem spec.
|
|
8629
|
+
`, COLORS.yellow));
|
|
8630
|
+
}
|
|
8252
8631
|
}
|
|
8253
8632
|
async function runMonolithicVerification(config, analysis) {
|
|
8254
8633
|
const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
|
|
@@ -8393,6 +8772,189 @@ async function runSubsystemVerification(config, analysis) {
|
|
|
8393
8772
|
console.log();
|
|
8394
8773
|
displayCompositionalReport(results, interference.valid);
|
|
8395
8774
|
}
|
|
8775
|
+
var DEFAULT_WITNESS_TIMEOUT_MS = 180000;
|
|
8776
|
+
async function globAbsolute(cwd, pattern) {
|
|
8777
|
+
const out = [];
|
|
8778
|
+
for await (const f of new Bun.Glob(pattern).scan({ cwd, absolute: true, onlyFiles: true })) {
|
|
8779
|
+
out.push(f);
|
|
8780
|
+
}
|
|
8781
|
+
return out.sort();
|
|
8782
|
+
}
|
|
8783
|
+
async function runWitnessVerification(config) {
|
|
8784
|
+
console.log(color(`Reachability witnesses (--witness)
|
|
8785
|
+
`, COLORS.blue));
|
|
8786
|
+
const cwd = process.cwd();
|
|
8787
|
+
const featureFiles = await globAbsolute(cwd, "features/**/*.feature");
|
|
8788
|
+
const stepFiles = [
|
|
8789
|
+
...new Set([
|
|
8790
|
+
...await globAbsolute(cwd, "features/**/*.steps.ts"),
|
|
8791
|
+
...await globAbsolute(cwd, "features/steps.ts")
|
|
8792
|
+
])
|
|
8793
|
+
];
|
|
8794
|
+
if (featureFiles.length === 0 || stepFiles.length === 0) {
|
|
8795
|
+
console.log(color(` No .feature files / step modules found — nothing to witness.
|
|
8796
|
+
`, COLORS.gray));
|
|
8797
|
+
return;
|
|
8798
|
+
}
|
|
8799
|
+
const { extractWitnesses: extractWitnesses2 } = await Promise.resolve().then(() => (init_witness(), exports_witness));
|
|
8800
|
+
const {
|
|
8801
|
+
bddPredicateToTLA: bddPredicateToTLA2,
|
|
8802
|
+
buildWitnessCfg: buildWitnessCfg2,
|
|
8803
|
+
buildWitnessModule: buildWitnessModule2,
|
|
8804
|
+
routeWitness: routeWitness2,
|
|
8805
|
+
witnessPolarity: witnessPolarity2,
|
|
8806
|
+
witnessVerdict: witnessVerdict2,
|
|
8807
|
+
WITNESS_INVARIANT: WITNESS_INVARIANT2
|
|
8808
|
+
} = await Promise.resolve().then(() => (init_witness2(), exports_witness2));
|
|
8809
|
+
const subsystems = config.subsystems;
|
|
8810
|
+
const witnesses = await extractWitnesses2(featureFiles, stepFiles);
|
|
8811
|
+
const docker = await setupDocker();
|
|
8812
|
+
const timeoutSeconds = getTimeout(config);
|
|
8813
|
+
const timeout = timeoutSeconds > 0 ? timeoutSeconds * 1000 : DEFAULT_WITNESS_TIMEOUT_MS;
|
|
8814
|
+
const workers = getWorkers(config);
|
|
8815
|
+
const results = [];
|
|
8816
|
+
let idx = 0;
|
|
8817
|
+
for (const w of witnesses) {
|
|
8818
|
+
const id = `${w.feature} › ${w.scenario}`;
|
|
8819
|
+
const polarity = witnessPolarity2(w.tags);
|
|
8820
|
+
if (!w.predicate) {
|
|
8821
|
+
results.push({
|
|
8822
|
+
id,
|
|
8823
|
+
status: "skipped",
|
|
8824
|
+
ok: true,
|
|
8825
|
+
note: "no state-observable Then to witness"
|
|
8826
|
+
});
|
|
8827
|
+
continue;
|
|
8828
|
+
}
|
|
8829
|
+
const subsystem = routeWitness2(w.fields, subsystems);
|
|
8830
|
+
if (!subsystem) {
|
|
8831
|
+
results.push({
|
|
8832
|
+
id,
|
|
8833
|
+
status: "skipped",
|
|
8834
|
+
ok: true,
|
|
8835
|
+
note: `fields [${w.fields.join(", ")}] not owned by a single subsystem`
|
|
8836
|
+
});
|
|
8837
|
+
continue;
|
|
8838
|
+
}
|
|
8839
|
+
const specDir = path4.join(cwd, "specs", "tla", "generated", subsystem);
|
|
8840
|
+
const subsystemCfg = path4.join(specDir, `UserApp_${subsystem}.cfg`);
|
|
8841
|
+
if (!fs4.existsSync(path4.join(specDir, `UserApp_${subsystem}.tla`)) || !fs4.existsSync(subsystemCfg)) {
|
|
8842
|
+
results.push({
|
|
8843
|
+
id,
|
|
8844
|
+
status: "error",
|
|
8845
|
+
ok: false,
|
|
8846
|
+
subsystem,
|
|
8847
|
+
note: `subsystem spec missing: ${subsystem}`
|
|
8848
|
+
});
|
|
8849
|
+
continue;
|
|
8850
|
+
}
|
|
8851
|
+
let tlaPredicate;
|
|
8852
|
+
try {
|
|
8853
|
+
tlaPredicate = bddPredicateToTLA2(w.predicate);
|
|
8854
|
+
} catch (err) {
|
|
8855
|
+
results.push({
|
|
8856
|
+
id,
|
|
8857
|
+
status: "error",
|
|
8858
|
+
ok: false,
|
|
8859
|
+
subsystem,
|
|
8860
|
+
note: err instanceof Error ? err.message : String(err)
|
|
8861
|
+
});
|
|
8862
|
+
continue;
|
|
8863
|
+
}
|
|
8864
|
+
const moduleName = `Witness_${subsystem}_${idx++}`;
|
|
8865
|
+
const witnessTla = path4.join(specDir, `${moduleName}.tla`);
|
|
8866
|
+
fs4.writeFileSync(witnessTla, buildWitnessModule2(moduleName, `UserApp_${subsystem}`, tlaPredicate));
|
|
8867
|
+
fs4.writeFileSync(path4.join(specDir, `${moduleName}.cfg`), buildWitnessCfg2(fs4.readFileSync(subsystemCfg, "utf8")));
|
|
8868
|
+
const polarityTag = polarity === "forbidden" ? color(" [forbidden — must be unreachable]", COLORS.gray) : "";
|
|
8869
|
+
console.log(color(`⚙️ ${id}`, COLORS.blue) + polarityTag);
|
|
8870
|
+
console.log(color(` ${subsystem} ⊨ ${w.predicate}`, COLORS.gray));
|
|
8871
|
+
let tlc;
|
|
8872
|
+
try {
|
|
8873
|
+
tlc = await docker.runTLC(witnessTla, { workers, timeout });
|
|
8874
|
+
} catch (err) {
|
|
8875
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8876
|
+
results.push({
|
|
8877
|
+
id,
|
|
8878
|
+
status: "inconclusive",
|
|
8879
|
+
ok: true,
|
|
8880
|
+
subsystem,
|
|
8881
|
+
predicate: w.predicate,
|
|
8882
|
+
note: msg
|
|
8883
|
+
});
|
|
8884
|
+
console.log(color(" ⚠ inconclusive — TLC did not finish (raise the config timeout)", COLORS.yellow));
|
|
8885
|
+
continue;
|
|
8886
|
+
}
|
|
8887
|
+
const reachable = !tlc.success && tlc.violation?.name === WITNESS_INVARIANT2;
|
|
8888
|
+
if (!tlc.success && !reachable) {
|
|
8889
|
+
results.push({
|
|
8890
|
+
id,
|
|
8891
|
+
status: "error",
|
|
8892
|
+
ok: false,
|
|
8893
|
+
subsystem,
|
|
8894
|
+
predicate: w.predicate,
|
|
8895
|
+
note: tlc.error ?? tlc.violation?.name ?? "TLC error"
|
|
8896
|
+
});
|
|
8897
|
+
console.log(color(` ! error — ${tlc.error ?? "see log"}`, COLORS.yellow));
|
|
8898
|
+
fs4.writeFileSync(path4.join(specDir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8899
|
+
continue;
|
|
8900
|
+
}
|
|
8901
|
+
const verdict = witnessVerdict2(polarity, reachable);
|
|
8902
|
+
const note = reachable ? undefined : `${tlc.stats?.distinctStates ?? 0} distinct states explored`;
|
|
8903
|
+
results.push({
|
|
8904
|
+
id,
|
|
8905
|
+
status: verdict.status,
|
|
8906
|
+
ok: verdict.ok,
|
|
8907
|
+
subsystem,
|
|
8908
|
+
predicate: w.predicate,
|
|
8909
|
+
note
|
|
8910
|
+
});
|
|
8911
|
+
console.log(color(` ${verdict.ok ? "✓" : "✗"} ${verdict.status} — ${verdict.message}`, verdict.ok ? COLORS.green : COLORS.red));
|
|
8912
|
+
if (!verdict.ok)
|
|
8913
|
+
fs4.writeFileSync(path4.join(specDir, `${moduleName}.tlc-output.log`), tlc.output);
|
|
8914
|
+
}
|
|
8915
|
+
displayWitnessReport(results);
|
|
8916
|
+
}
|
|
8917
|
+
function displayWitnessReport(results) {
|
|
8918
|
+
const count = (s) => results.filter((r) => r.status === s).length;
|
|
8919
|
+
console.log();
|
|
8920
|
+
console.log(color(`Witness results:
|
|
8921
|
+
`, COLORS.blue));
|
|
8922
|
+
for (const r of results) {
|
|
8923
|
+
const mark = {
|
|
8924
|
+
reachable: color("✓", COLORS.green),
|
|
8925
|
+
excluded: color("✓", COLORS.green),
|
|
8926
|
+
unreachable: color("✗", COLORS.red),
|
|
8927
|
+
violated: color("✗", COLORS.red),
|
|
8928
|
+
inconclusive: color("⚠", COLORS.yellow),
|
|
8929
|
+
error: color("!", COLORS.yellow),
|
|
8930
|
+
skipped: color("·", COLORS.gray)
|
|
8931
|
+
}[r.status];
|
|
8932
|
+
const note = r.note ? color(` (${r.note})`, COLORS.gray) : "";
|
|
8933
|
+
console.log(` ${mark} ${r.status.padEnd(12)} ${r.id}${note}`);
|
|
8934
|
+
}
|
|
8935
|
+
const tally = [
|
|
8936
|
+
"reachable",
|
|
8937
|
+
"excluded",
|
|
8938
|
+
"unreachable",
|
|
8939
|
+
"violated",
|
|
8940
|
+
"inconclusive",
|
|
8941
|
+
"error",
|
|
8942
|
+
"skipped"
|
|
8943
|
+
].map((s) => `${count(s)} ${s}`).join(", ");
|
|
8944
|
+
console.log();
|
|
8945
|
+
console.log(color(` ${tally}`, COLORS.gray));
|
|
8946
|
+
console.log();
|
|
8947
|
+
const failures = results.filter((r) => !r.ok);
|
|
8948
|
+
if (failures.length > 0) {
|
|
8949
|
+
console.log(color("Witness result: ✗ FAIL", COLORS.red));
|
|
8950
|
+
console.log(color(" An outcome a scenario claims is unreachable, or a forbidden state the model can reach.", COLORS.gray));
|
|
8951
|
+
console.log();
|
|
8952
|
+
process.exit(1);
|
|
8953
|
+
}
|
|
8954
|
+
console.log(color("Witness result: ✓ PASS", COLORS.green));
|
|
8955
|
+
console.log(color(" Every claimed outcome is reachable; every forbidden state is proven unreachable.", COLORS.gray));
|
|
8956
|
+
console.log();
|
|
8957
|
+
}
|
|
8396
8958
|
function displayEnsuresSummary(results) {
|
|
8397
8959
|
const totalEnsures = results.reduce((sum, r) => sum + r.ensuresCount, 0);
|
|
8398
8960
|
if (totalEnsures === 0)
|
|
@@ -8630,6 +9192,12 @@ ${color("Commands:", COLORS.blue)}
|
|
|
8630
9192
|
field no handler writes, or an unverified $meshState/$peerState predicate.
|
|
8631
9193
|
Also via ${color("POLLY_VERIFY_STRICT=1", COLORS.yellow)}.
|
|
8632
9194
|
|
|
9195
|
+
${color("polly verify --witness", COLORS.green)}
|
|
9196
|
+
After the invariant pass, check each BDD scenario's Then-outcome for
|
|
9197
|
+
reachability: one TLC run per witnessable scenario over its subsystem spec.
|
|
9198
|
+
A scenario the exhaustive model proves unreachable is one that lies, and
|
|
9199
|
+
fails the run. Needs a ${color("subsystems", COLORS.blue)} block and ${color("features/", COLORS.blue)} (see polly bdd).
|
|
9200
|
+
|
|
8633
9201
|
${color("polly verify --setup", COLORS.green)}
|
|
8634
9202
|
Analyze codebase and generate configuration file
|
|
8635
9203
|
|
|
@@ -8693,4 +9261,4 @@ main().catch((error) => {
|
|
|
8693
9261
|
process.exit(1);
|
|
8694
9262
|
});
|
|
8695
9263
|
|
|
8696
|
-
//# debugId=
|
|
9264
|
+
//# debugId=F9E0214A7A9A110B64756E2164756E21
|