@fairfox/polly 0.82.0 → 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.
Files changed (45) hide show
  1. package/dist/cli/polly.js +22 -1
  2. package/dist/cli/polly.js.map +3 -3
  3. package/dist/tools/bdd/src/args.d.ts +21 -0
  4. package/dist/tools/bdd/src/bus-driver.d.ts +36 -0
  5. package/dist/tools/bdd/src/check-verify.d.ts +15 -0
  6. package/dist/tools/bdd/src/cli.d.ts +2 -0
  7. package/dist/tools/bdd/src/cli.js +701 -0
  8. package/dist/tools/bdd/src/cli.js.map +19 -0
  9. package/dist/tools/bdd/src/config.d.ts +9 -0
  10. package/dist/tools/bdd/src/extract.d.ts +2 -0
  11. package/dist/tools/bdd/src/index.d.ts +19 -0
  12. package/dist/tools/bdd/src/index.js +540 -0
  13. package/dist/tools/bdd/src/index.js.map +17 -0
  14. package/dist/tools/bdd/src/parse.d.ts +3 -0
  15. package/dist/tools/bdd/src/report.d.ts +6 -0
  16. package/dist/tools/bdd/src/run.d.ts +8 -0
  17. package/dist/tools/bdd/src/scaffold.d.ts +7 -0
  18. package/dist/tools/bdd/src/steps.d.ts +55 -0
  19. package/dist/tools/bdd/src/types.d.ts +145 -0
  20. package/dist/tools/bdd/src/witness.d.ts +23 -0
  21. package/dist/tools/gallery/src/cli.js +4 -3
  22. package/dist/tools/gallery/src/cli.js.map +3 -3
  23. package/dist/tools/mutate/src/cli.js +8 -1
  24. package/dist/tools/mutate/src/cli.js.map +3 -3
  25. package/dist/tools/quality/src/cli.js +304 -15
  26. package/dist/tools/quality/src/cli.js.map +6 -4
  27. package/dist/tools/quality/src/index.d.ts +2 -0
  28. package/dist/tools/quality/src/index.js +309 -15
  29. package/dist/tools/quality/src/index.js.map +6 -4
  30. package/dist/tools/quality/src/no-fixed-waits.d.ts +52 -0
  31. package/dist/tools/quality/src/no-tautology-ensures.d.ts +67 -0
  32. package/dist/tools/quality/src/plugins/core.d.ts +1 -1
  33. package/dist/tools/test/src/coverage-policy/cli.js +5 -1
  34. package/dist/tools/test/src/coverage-policy/cli.js.map +3 -3
  35. package/dist/tools/test/src/tiers/args.d.ts +5 -0
  36. package/dist/tools/test/src/tiers/cli.js +97 -23
  37. package/dist/tools/test/src/tiers/cli.js.map +9 -8
  38. package/dist/tools/test/src/tiers/index.d.ts +2 -1
  39. package/dist/tools/test/src/tiers/order.d.ts +25 -0
  40. package/dist/tools/test/src/tiers/types.d.ts +15 -0
  41. package/dist/tools/verify/src/cli.js +540 -15
  42. package/dist/tools/verify/src/cli.js.map +8 -4
  43. package/dist/tools/visualize/src/cli.js +17 -13
  44. package/dist/tools/visualize/src/cli.js.map +3 -3
  45. package/package.json +9 -16
@@ -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, {
@@ -7923,7 +8272,7 @@ function displaySetupSuccess(configPath) {
7923
8272
  console.log();
7924
8273
  console.log(" 1. Review the generated configuration file");
7925
8274
  console.log(" 2. Fill in values marked with /* CONFIGURE */");
7926
- console.log(" 3. Run 'bun verify' to check your configuration");
8275
+ console.log(" 3. Run 'polly verify' to check your configuration");
7927
8276
  console.log();
7928
8277
  console.log(color("\uD83D\uDCA1 Tip:", COLORS.gray));
7929
8278
  console.log(color(" Look for comments explaining what each field needs.", COLORS.gray));
@@ -7938,7 +8287,7 @@ async function validateCommand() {
7938
8287
  if (result.valid) {
7939
8288
  console.log(color(`✅ Configuration is complete and valid!
7940
8289
  `, COLORS.green));
7941
- console.log(" You can now run 'bun verify' to start verification.");
8290
+ console.log(" You can now run 'polly verify' to start verification.");
7942
8291
  console.log();
7943
8292
  return;
7944
8293
  }
@@ -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) {
@@ -8162,8 +8514,8 @@ async function verifyCommand() {
8162
8514
  console.log(color(` ... and ${errors.length - 3} more error(s)`, COLORS.gray));
8163
8515
  console.log();
8164
8516
  }
8165
- console.log(" Run 'bun verify --validate' to see all issues");
8166
- console.log(" Run 'bun verify --setup' to regenerate configuration");
8517
+ console.log(" Run 'polly verify --validate' to see all issues");
8518
+ console.log(" Run 'polly verify --setup' to regenerate configuration");
8167
8519
  console.log();
8168
8520
  process.exit(1);
8169
8521
  }
@@ -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)
@@ -8613,36 +9127,47 @@ function displayVerificationResults(result, specDir) {
8613
9127
  }
8614
9128
  function showHelp() {
8615
9129
  console.log(`
8616
- ${color("bun verify", COLORS.blue)} - Formal verification for web extensions
9130
+ ${color("polly verify", COLORS.blue)} - Formal verification for web extensions
9131
+
9132
+ Tests sample a few executions; a model checker explores every reachable state.
9133
+ This compiles your handlers and state into TLA+ and runs TLC to prove safety
9134
+ invariants hold under all interleavings — the ordering and concurrency bugs
9135
+ tests rarely reach.
8617
9136
 
8618
9137
  ${color("Commands:", COLORS.blue)}
8619
9138
 
8620
- ${color("bun verify", COLORS.green)}
9139
+ ${color("polly verify", COLORS.green)}
8621
9140
  Run verification (validates config, generates specs, runs TLC)
8622
9141
 
8623
- ${color("bun verify --strict", COLORS.green)}
9142
+ ${color("polly verify --strict", COLORS.green)}
8624
9143
  Fail closed (non-zero exit) on model-coverage gaps: a declared state
8625
9144
  field no handler writes, or an unverified $meshState/$peerState predicate.
8626
9145
  Also via ${color("POLLY_VERIFY_STRICT=1", COLORS.yellow)}.
8627
9146
 
8628
- ${color("bun verify --setup", COLORS.green)}
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
+
9153
+ ${color("polly verify --setup", COLORS.green)}
8629
9154
  Analyze codebase and generate configuration file
8630
9155
 
8631
- ${color("bun verify --validate", COLORS.green)}
9156
+ ${color("polly verify --validate", COLORS.green)}
8632
9157
  Validate existing configuration without running verification
8633
9158
 
8634
- ${color("bun verify --estimate", COLORS.green)}
9159
+ ${color("polly verify --estimate", COLORS.green)}
8635
9160
  Estimate state space without running TLC
8636
9161
 
8637
- ${color("bun verify --help", COLORS.green)}
9162
+ ${color("polly verify --help", COLORS.green)}
8638
9163
  Show this help message
8639
9164
 
8640
9165
  ${color("Getting Started:", COLORS.blue)}
8641
9166
 
8642
- 1. Run ${color("bun verify --setup", COLORS.green)} to generate configuration
9167
+ 1. Run ${color("polly verify --setup", COLORS.green)} to generate configuration
8643
9168
  2. Review ${color("specs/verification.config.ts", COLORS.blue)} and fill in marked fields
8644
- 3. Run ${color("bun verify --validate", COLORS.green)} to check your configuration
8645
- 4. Run ${color("bun verify", COLORS.green)} to start verification
9169
+ 3. Run ${color("polly verify --validate", COLORS.green)} to check your configuration
9170
+ 4. Run ${color("polly verify", COLORS.green)} to start verification
8646
9171
 
8647
9172
  ${color("Configuration Help:", COLORS.blue)}
8648
9173
 
@@ -8688,4 +9213,4 @@ main().catch((error) => {
8688
9213
  process.exit(1);
8689
9214
  });
8690
9215
 
8691
- //# debugId=9530CFE43867B27164756E2164756E21
9216
+ //# debugId=006B6ED4C6E5114F64756E2164756E21