@fairfox/polly 0.78.0 → 0.80.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 (97) hide show
  1. package/dist/cli/polly.js +76 -3
  2. package/dist/cli/polly.js.map +3 -3
  3. package/dist/src/background/index.js.map +3 -3
  4. package/dist/src/background/message-router.js.map +3 -3
  5. package/dist/src/client/index.js +137 -32
  6. package/dist/src/client/index.js.map +6 -5
  7. package/dist/src/client/wrapper.d.ts +39 -2
  8. package/dist/src/elysia/index.js +22 -3
  9. package/dist/src/elysia/index.js.map +5 -5
  10. package/dist/src/elysia/route-match.d.ts +13 -0
  11. package/dist/src/index.d.ts +1 -1
  12. package/dist/src/index.js +12 -2
  13. package/dist/src/index.js.map +7 -7
  14. package/dist/src/mesh.js +28 -9
  15. package/dist/src/mesh.js.map +10 -9
  16. package/dist/src/peer.js +6 -2
  17. package/dist/src/peer.js.map +5 -5
  18. package/dist/src/polly-ui/Badge.d.ts +5 -0
  19. package/dist/src/polly-ui/Button.d.ts +31 -6
  20. package/dist/src/polly-ui/Dropdown.d.ts +6 -0
  21. package/dist/src/polly-ui/Select.d.ts +11 -1
  22. package/dist/src/polly-ui/TextInput.d.ts +30 -0
  23. package/dist/src/polly-ui/index.css +10 -0
  24. package/dist/src/polly-ui/index.js +81 -32
  25. package/dist/src/polly-ui/index.js.map +10 -10
  26. package/dist/src/polly-ui/styles.css +10 -0
  27. package/dist/src/shared/adapters/index.js.map +3 -3
  28. package/dist/src/shared/lib/context-helpers.js.map +3 -3
  29. package/dist/src/shared/lib/message-bus.js.map +3 -3
  30. package/dist/src/shared/lib/resource.js +11 -2
  31. package/dist/src/shared/lib/resource.js.map +6 -6
  32. package/dist/src/shared/lib/state.d.ts +20 -0
  33. package/dist/src/shared/lib/state.js +11 -1
  34. package/dist/src/shared/lib/state.js.map +5 -5
  35. package/dist/src/shared/state/app-state.js +10 -1
  36. package/dist/src/shared/state/app-state.js.map +5 -5
  37. package/dist/tools/init/src/cli.js +23 -2
  38. package/dist/tools/init/src/cli.js.map +4 -4
  39. package/dist/tools/init/templates/pwa/package.json.template +1 -1
  40. package/dist/tools/init/templates/pwa/src/service-worker.ts.template +26 -15
  41. package/dist/tools/init/templates/pwa/src/shared-worker.ts.template +13 -3
  42. package/dist/tools/init/templates/pwa/tsconfig.json.template +2 -2
  43. package/dist/tools/init/templates/pwa/tsconfig.worker.json.template +17 -0
  44. package/dist/tools/test/src/browser/index.js +5 -2
  45. package/dist/tools/test/src/browser/index.js.map +3 -3
  46. package/dist/tools/test/src/contrast/index.js +20 -15
  47. package/dist/tools/test/src/contrast/index.js.map +3 -3
  48. package/dist/tools/test/src/coverage-policy/cli.d.ts +19 -0
  49. package/dist/tools/test/src/coverage-policy/cli.js +339 -0
  50. package/dist/tools/test/src/coverage-policy/cli.js.map +13 -0
  51. package/dist/tools/test/src/coverage-policy/discover.d.ts +23 -0
  52. package/dist/tools/test/src/coverage-policy/enforce.d.ts +54 -0
  53. package/dist/tools/test/src/coverage-policy/index.d.ts +10 -0
  54. package/dist/tools/test/src/coverage-policy/index.js +242 -0
  55. package/dist/tools/test/src/coverage-policy/index.js.map +13 -0
  56. package/dist/tools/test/src/coverage-policy/mutate-targets.d.ts +30 -0
  57. package/dist/tools/test/src/coverage-policy/types.d.ts +35 -0
  58. package/dist/tools/test/src/e2e-cli/index.d.ts +10 -0
  59. package/dist/tools/test/src/e2e-cli/run-cli.d.ts +25 -0
  60. package/dist/tools/test/src/e2e-cli/with-temp-dir.d.ts +15 -0
  61. package/dist/tools/test/src/e2e-mesh/index.js +29 -8
  62. package/dist/tools/test/src/e2e-mesh/index.js.map +7 -6
  63. package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +7 -1
  64. package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +8 -0
  65. package/dist/tools/test/src/e2e-relay/index.d.ts +12 -0
  66. package/dist/tools/test/src/e2e-relay/index.js +1421 -0
  67. package/dist/tools/test/src/e2e-relay/index.js.map +30 -0
  68. package/dist/tools/test/src/e2e-relay/wait-for-relay-convergence.d.ts +35 -0
  69. package/dist/tools/test/src/e2e-relay/with-repo-server.d.ts +33 -0
  70. package/dist/tools/test/src/e2e-shared/assert.d.ts +18 -0
  71. package/dist/tools/test/src/e2e-shared/contract.d.ts +40 -0
  72. package/dist/tools/test/src/e2e-shared/index.d.ts +3 -0
  73. package/dist/tools/test/src/e2e-shared/timeout-context.d.ts +17 -0
  74. package/dist/tools/test/src/index.d.ts +1 -0
  75. package/dist/tools/test/src/index.js +16 -1
  76. package/dist/tools/test/src/index.js.map +5 -4
  77. package/dist/tools/test/src/tiers/args.d.ts +23 -0
  78. package/dist/tools/test/src/tiers/cli.d.ts +2 -0
  79. package/dist/tools/test/src/tiers/cli.js +490 -0
  80. package/dist/tools/test/src/tiers/cli.js.map +16 -0
  81. package/dist/tools/test/src/tiers/detect.d.ts +12 -0
  82. package/dist/tools/test/src/tiers/discover.d.ts +2 -0
  83. package/dist/tools/test/src/tiers/engine.d.ts +3 -0
  84. package/dist/tools/test/src/tiers/index.d.ts +14 -0
  85. package/dist/tools/test/src/tiers/protocol.d.ts +10 -0
  86. package/dist/tools/test/src/tiers/reporter.d.ts +12 -0
  87. package/dist/tools/test/src/tiers/types.d.ts +94 -0
  88. package/dist/tools/test/src/tiers/worker.d.ts +2 -0
  89. package/dist/tools/test/src/tiers/worker.js +60 -0
  90. package/dist/tools/test/src/tiers/worker.js.map +12 -0
  91. package/dist/tools/verify/src/cli.js +165 -30
  92. package/dist/tools/verify/src/cli.js.map +7 -6
  93. package/dist/tools/verify/src/stryker/index.js +20 -11
  94. package/dist/tools/verify/src/stryker/index.js.map +3 -3
  95. package/dist/tools/visualize/src/cli.js +8 -5
  96. package/dist/tools/visualize/src/cli.js.map +4 -4
  97. package/package.json +26 -6
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Manifest types for the tiered test engine.
3
+ *
4
+ * The engine is deliberately pure: it knows how to *run* a plan of cases as
5
+ * isolated subprocesses, gate them on environment capabilities, time them, and
6
+ * report. It knows nothing about Polly's specific scripts — those are supplied
7
+ * as a {@link TierPlan} by a front-end (Polly's internal registry, or a
8
+ * consumer-discovery step behind `polly test`). This keeps the engine reusable
9
+ * and respects the repo's src → tools → cli → scripts import boundary.
10
+ */
11
+ /** A capability a case needs from the host before it can run. */
12
+ export type Need = "docker" | "browser" | "network";
13
+ /** How a case is executed. Both forms run in their own subprocess. */
14
+ export type CaseExec = {
15
+ /**
16
+ * A module that may `export async function run(ctx)`. The worker imports
17
+ * it; if `run` is exported it is called for a structured result,
18
+ * otherwise the module's import side-effect (legacy top-level `main()`)
19
+ * runs and its `process.exitCode` becomes the result. This is what lets
20
+ * converted and unconverted scripts coexist.
21
+ */
22
+ kind: "module";
23
+ modulePath: string;
24
+ } | {
25
+ /** An arbitrary command; success is exit code 0. */
26
+ kind: "command";
27
+ argv: string[];
28
+ cwd?: string;
29
+ };
30
+ /** One runnable unit within a tier. */
31
+ export interface CaseSpec {
32
+ /** Stable id, e.g. "mesh.blob-transfer". Used for --only matching. */
33
+ id: string;
34
+ /** Human label for reports; defaults to id. */
35
+ label?: string;
36
+ /** Free-form tags for --only filtering (e.g. "mesh", "revocation"). */
37
+ tags?: string[];
38
+ /** Host capabilities required; unmet → skipped with a logged reason. */
39
+ needs?: Need[];
40
+ /** Per-case timeout. Defaults to the engine's tier default. */
41
+ timeoutMs?: number;
42
+ exec: CaseExec;
43
+ }
44
+ /** An ordered group of cases sharing a realism level. */
45
+ export interface Tier {
46
+ name: string;
47
+ /** One-line description shown by --list. */
48
+ description?: string;
49
+ /** Run cases in this tier concurrently up to this many at once. Default 1. */
50
+ concurrency?: number;
51
+ /** Default per-case timeout for this tier. */
52
+ timeoutMs?: number;
53
+ cases: CaseSpec[];
54
+ }
55
+ /** The full thing handed to the engine. Tiers run in array order. */
56
+ export interface TierPlan {
57
+ tiers: Tier[];
58
+ }
59
+ export type CaseOutcome = "pass" | "fail" | "skip" | "timeout";
60
+ export interface CaseReport {
61
+ tier: string;
62
+ id: string;
63
+ label: string;
64
+ outcome: CaseOutcome;
65
+ durationMs: number;
66
+ message?: string;
67
+ /** Reason a case was skipped (unmet need). */
68
+ skipReason?: string;
69
+ }
70
+ export interface RunReport {
71
+ cases: CaseReport[];
72
+ passed: number;
73
+ failed: number;
74
+ skipped: number;
75
+ durationMs: number;
76
+ ok: boolean;
77
+ }
78
+ /** Options controlling a single engine run. */
79
+ export interface EngineOptions {
80
+ /** Only run these tiers (by name). Empty = all tiers in the plan. */
81
+ tiers?: string[];
82
+ /** Only run cases whose id/label/tags match one of these substrings. */
83
+ only?: string[];
84
+ /** Stop after the first tier that has a failure. */
85
+ bail?: boolean;
86
+ /** Treat unmet-need skips as failures (CI-strict). Default false. */
87
+ strictNeeds?: boolean;
88
+ /** Forwarded into each case's environment. */
89
+ env?: Record<string, string | undefined>;
90
+ /** Default working directory for cases that don't set their own. */
91
+ cwd?: string;
92
+ /** Sink for engine-level progress lines. Defaults to console.log. */
93
+ log?: (message: string) => void;
94
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bun
2
+ import { createRequire } from "node:module";
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
+
20
+ // tools/test/src/e2e-shared/contract.ts
21
+ function standaloneContext() {
22
+ return {
23
+ log: (message) => console.log(message),
24
+ env: process.env
25
+ };
26
+ }
27
+
28
+ // tools/test/src/tiers/protocol.ts
29
+ var SENTINEL = "__TIER_RESULT__";
30
+
31
+ // tools/test/src/tiers/worker.ts
32
+ function emit(result) {
33
+ process.stdout.write(`
34
+ ${SENTINEL}${JSON.stringify(result)}
35
+ `);
36
+ }
37
+ async function runWorker() {
38
+ const modulePath = process.argv[2];
39
+ if (!modulePath) {
40
+ emit({ pass: false, message: "worker: no module path given" });
41
+ return;
42
+ }
43
+ try {
44
+ const mod = await import(modulePath);
45
+ if (typeof mod.run === "function") {
46
+ const result = await mod.run(standaloneContext());
47
+ emit({ pass: Boolean(result?.pass), message: result?.message, detail: result?.detail });
48
+ } else {
49
+ emit({ pass: (process.exitCode ?? 0) === 0 });
50
+ }
51
+ } catch (err) {
52
+ emit({ pass: false, message: err instanceof Error ? err.message : String(err) });
53
+ }
54
+ }
55
+ if (__require.main == __require.module) {
56
+ await runWorker();
57
+ process.exit(0);
58
+ }
59
+
60
+ //# debugId=0FA2FEFC484C1D5964756E2164756E21
@@ -0,0 +1,12 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../tools/test/src/e2e-shared/contract.ts", "../tools/test/src/tiers/protocol.ts", "../tools/test/src/tiers/worker.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * The contract every tiered test case speaks.\n *\n * An e2e script is \"tier-aware\" when it exports an async `run(ctx)` that\n * returns a {@link TierResult} instead of signalling success through\n * `process.exitCode`. The engine prefers this export so it can collect a\n * structured result; scripts that do not export it still run (the engine\n * falls back to executing the file and reading its exit code), so the\n * migration is incremental and never breaks the suite.\n *\n * Standalone execution is preserved: a converted script keeps a\n * `if (import.meta.main) selfRun(run)` footer so `bun scripts/e2e-foo.ts`\n * behaves exactly as before.\n */\n\n/** Logger handed to a case so its output can be captured/prefixed by the engine. */\nexport type TierLog = (message: string) => void;\n\n/** Context passed to a case's `run()`. Kept small and forward-compatible. */\nexport interface TierContext {\n /** Emit a progress line. Defaults to `console.log` when run standalone. */\n log: TierLog;\n /** Extra environment the engine wants the case to honour (e.g. SIGNALING_URL). */\n env: Record<string, string | undefined>;\n}\n\n/** What a case reports back. `pass: false` with a `message` is a clean failure. */\nexport interface TierResult {\n pass: boolean;\n /** Human-readable failure reason; omitted on success. */\n message?: string;\n /** Optional structured detail surfaced in `--json` output. */\n detail?: Record<string, unknown>;\n}\n\nexport type TierRun = (ctx: TierContext) => Promise<TierResult>;\n\n/** Build a default context for standalone (`import.meta.main`) execution. */\nexport function standaloneContext(): TierContext {\n return {\n log: (message: string) => console.log(message),\n env: process.env,\n };\n}\n\n/**\n * Footer helper for converted scripts: runs the case standalone, prints a\n * PASS/FAIL line in the historical `[e2e] <capability>: PASS` format, and sets\n * the process exit code. Keeps the file runnable on its own.\n */\nexport async function selfRun(capability: string, run: TierRun): Promise<void> {\n const ctx = standaloneContext();\n try {\n const result = await run(ctx);\n if (result.pass) {\n ctx.log(`[e2e] ${capability}: PASS`);\n } else {\n ctx.log(`[e2e] ${capability}: FAIL — ${result.message ?? \"no reason given\"}`);\n process.exitCode = 1;\n }\n } catch (err) {\n ctx.log(`[e2e] ${capability}: FAIL — ${err instanceof Error ? err.message : String(err)}`);\n process.exitCode = 1;\n }\n}\n",
6
+ "/**\n * The one shared constant between the engine and its subprocess worker.\n *\n * It lives in its own leaf module so the engine never has to *import* the\n * worker (only spawn it by path). If the engine imported the worker, a\n * `splitting: false` bundle would inline the worker's `import.meta.main`\n * self-exec block into the CLI bundle and run it on startup. Keeping the\n * sentinel here avoids that.\n */\nexport const SENTINEL = \"__TIER_RESULT__\";\n",
7
+ "#!/usr/bin/env bun\n/**\n * Subprocess entry for a single \"module\" case.\n *\n * Spawned by the engine as `bun worker.ts <modulePath> <id>`. Running each case\n * in its own process is deliberate: the e2e scripts mutate globals (happy-dom),\n * bind ephemeral ports, and drive Puppeteer — they were written assuming\n * process isolation, and the engine preserves it.\n *\n * Protocol: the worker prints exactly one sentinel line\n * __TIER_RESULT__{\"pass\":true,...}\n * to stdout. Everything else on stdout is the case's own logging, forwarded by\n * the parent. If the case hard-exits (legacy `process.exit(1)`) before the\n * sentinel, the parent falls back to the worker's exit code.\n */\nimport { standaloneContext, type TierResult } from \"../e2e-shared/contract\";\nimport { SENTINEL } from \"./protocol\";\n\nfunction emit(result: TierResult): void {\n process.stdout.write(`\\n${SENTINEL}${JSON.stringify(result)}\\n`);\n}\n\nasync function runWorker(): Promise<void> {\n const modulePath = process.argv[2];\n if (!modulePath) {\n emit({ pass: false, message: \"worker: no module path given\" });\n return;\n }\n\n try {\n // Importing a converted script is side-effect-free (its self-run footer is\n // guarded by import.meta.main). Importing a legacy script runs its top-level\n // main() now, which sets process.exitCode.\n const mod: { run?: (ctx: unknown) => Promise<TierResult> } = await import(modulePath);\n\n if (typeof mod.run === \"function\") {\n const result = await mod.run(standaloneContext());\n emit({ pass: Boolean(result?.pass), message: result?.message, detail: result?.detail });\n } else {\n // Legacy: the import side-effect already ran the test; trust its exit code.\n emit({ pass: (process.exitCode ?? 0) === 0 });\n }\n } catch (err) {\n emit({ pass: false, message: err instanceof Error ? err.message : String(err) });\n }\n}\n\n// Only execute when invoked directly; the engine also imports SENTINEL from here.\nif (import.meta.main) {\n await runWorker();\n // Force exit so an open Puppeteer/server handle can't keep the worker alive;\n // the result has already been emitted and captured by the parent.\n process.exit(0);\n}\n"
8
+ ],
9
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAsCO,SAAS,iBAAiB,GAAgB;AAAA,EAC/C,OAAO;AAAA,IACL,KAAK,CAAC,YAAoB,QAAQ,IAAI,OAAO;AAAA,IAC7C,KAAK,QAAQ;AAAA,EACf;AAAA;;;ACjCK,IAAM,WAAW;;;ACSxB,SAAS,IAAI,CAAC,QAA0B;AAAA,EACtC,QAAQ,OAAO,MAAM;AAAA,EAAK,WAAW,KAAK,UAAU,MAAM;AAAA,CAAK;AAAA;AAGjE,eAAe,SAAS,GAAkB;AAAA,EACxC,MAAM,aAAa,QAAQ,KAAK;AAAA,EAChC,IAAI,CAAC,YAAY;AAAA,IACf,KAAK,EAAE,MAAM,OAAO,SAAS,+BAA+B,CAAC;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,IAAI;AAAA,IAIF,MAAM,MAAuD,MAAa;AAAA,IAE1E,IAAI,OAAO,IAAI,QAAQ,YAAY;AAAA,MACjC,MAAM,SAAS,MAAM,IAAI,IAAI,kBAAkB,CAAC;AAAA,MAChD,KAAK,EAAE,MAAM,QAAQ,QAAQ,IAAI,GAAG,SAAS,QAAQ,SAAS,QAAQ,QAAQ,OAAO,CAAC;AAAA,IACxF,EAAO;AAAA,MAEL,KAAK,EAAE,OAAO,QAAQ,YAAY,OAAO,EAAE,CAAC;AAAA;AAAA,IAE9C,OAAO,KAAK;AAAA,IACZ,KAAK,EAAE,MAAM,OAAO,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC;AAAA;AAAA;AAKnF,IAAI,oCAAkB;AAAA,EACpB,MAAM,UAAU;AAAA,EAGhB,QAAQ,KAAK,CAAC;AAChB;",
10
+ "debugId": "0FA2FEFC484C1D5964756E2164756E21",
11
+ "names": []
12
+ }
@@ -249,18 +249,78 @@ var init_expression_validator = __esm(() => {
249
249
  WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
250
250
  });
251
251
 
252
+ // tools/verify/src/analysis/model-coverage.ts
253
+ var exports_model_coverage = {};
254
+ __export(exports_model_coverage, {
255
+ strictCoverageReasons: () => strictCoverageReasons,
256
+ computeModelCoverage: () => computeModelCoverage
257
+ });
258
+ function norm(field) {
259
+ return field.replace(/_/g, ".");
260
+ }
261
+ function computeModelCoverage(stateFields, handlers) {
262
+ const writersByField = new Map;
263
+ for (const field of stateFields) {
264
+ writersByField.set(norm(field), new Set);
265
+ }
266
+ const declaredOrder = stateFields.map((f) => ({ raw: f, key: norm(f) }));
267
+ for (const handler of handlers) {
268
+ for (const assignment of handler.assignments) {
269
+ const key = norm(assignment.field);
270
+ const writers = writersByField.get(key);
271
+ if (writers)
272
+ writers.add(handler.messageType);
273
+ }
274
+ }
275
+ const fieldCoverage = declaredOrder.map(({ raw, key }) => ({
276
+ field: raw,
277
+ writers: Array.from(writersByField.get(key) ?? []).sort()
278
+ }));
279
+ const unwrittenFields = fieldCoverage.filter((f) => f.writers.length === 0).map((f) => f.field);
280
+ const declaredKeys = new Set(declaredOrder.map((d) => d.key));
281
+ const unconstrainedMutators = [];
282
+ for (const handler of handlers) {
283
+ if (handler.postconditions.length > 0)
284
+ continue;
285
+ const fields = Array.from(new Set(handler.assignments.map((a) => norm(a.field)).filter((f) => declaredKeys.has(f)))).sort();
286
+ if (fields.length > 0) {
287
+ unconstrainedMutators.push({
288
+ handler: handler.messageType,
289
+ fields,
290
+ location: handler.location
291
+ });
292
+ }
293
+ }
294
+ return {
295
+ fieldCoverage,
296
+ unwrittenFields,
297
+ unconstrainedMutators,
298
+ hasStrictViolation: unwrittenFields.length > 0
299
+ };
300
+ }
301
+ function strictCoverageReasons(report, meshFindingCount) {
302
+ const reasons = [];
303
+ if (report.hasStrictViolation) {
304
+ reasons.push(`${report.unwrittenFields.length} declared state field(s) written by no modelled handler`);
305
+ }
306
+ if (meshFindingCount > 0) {
307
+ reasons.push(`${meshFindingCount} unverified $meshState/$peerState predicate(s)`);
308
+ }
309
+ return reasons;
310
+ }
311
+
252
312
  // tools/verify/src/analysis/coupled-fields.ts
253
313
  var exports_coupled_fields = {};
254
314
  __export(exports_coupled_fields, {
255
315
  checkCoupledFields: () => checkCoupledFields
256
316
  });
257
- function norm(field) {
317
+ function norm2(field) {
258
318
  return field.replace(/_/g, ".");
259
319
  }
260
320
  function checkCoupledFields(coupledFields, handlers) {
261
321
  const violations = [];
262
322
  for (const rawGroup of coupledFields) {
263
- const group = rawGroup.map(norm);
323
+ const group = rawGroup.map(norm2);
264
324
  const groupSet = new Set(group);
265
325
  if (groupSet.size < 2)
266
326
  continue;
@@ -275,7 +335,7 @@ function checkCoupledFields(coupledFields, handlers) {
275
335
  function writtenFields(group, handler) {
276
336
  const written = new Set;
277
337
  for (const assignment of handler.assignments) {
278
- const field = norm(assignment.field);
338
+ const field = norm2(assignment.field);
279
339
  if (group.has(field))
280
340
  written.add(field);
281
341
  }
@@ -1297,28 +1357,35 @@ var init_tla = __esm(() => {
1297
1357
  return key.replace(/[^A-Za-z0-9_]/g, "_");
1298
1358
  }
1299
1359
  fieldConfigInitialValue(_path, fieldConfig, _config) {
1300
- const fc = fieldConfig;
1301
- if (fc["type"] === "boolean")
1302
- return "FALSE";
1303
- if (fc["type"] === "number") {
1304
- const min = typeof fc["min"] === "number" ? fc["min"] : 0;
1305
- return String(min);
1306
- }
1307
- if (fc["type"] === "enum" && Array.isArray(fc["values"]) && fc["values"].length > 0) {
1308
- return `"${String(fc["values"][0])}"`;
1309
- }
1310
- if (fc["type"] === "string") {
1311
- return typeof fc["initial"] === "string" ? `"${fc["initial"]}"` : '""';
1312
- }
1313
- if (fc["type"] === "array") {
1314
- return "<<>>";
1315
- }
1316
- if (Array.isArray(fc.values)) {
1317
- const values = fc.values;
1318
- if (values.length > 0)
1319
- return `"${values[0]}"`;
1360
+ return this.typedFieldInitialValue(fieldConfig) ?? this.legacyValuesInitialValue(fieldConfig) ?? '"v1"';
1361
+ }
1362
+ typedFieldInitialValue(fieldConfig) {
1363
+ if (!("type" in fieldConfig))
1364
+ return;
1365
+ switch (fieldConfig.type) {
1366
+ case "boolean":
1367
+ return "FALSE";
1368
+ case "number":
1369
+ return String(typeof fieldConfig.min === "number" ? fieldConfig.min : 0);
1370
+ case "enum": {
1371
+ const values = fieldConfig.values;
1372
+ if (Array.isArray(values) && values.length > 0)
1373
+ return `"${String(values[0])}"`;
1374
+ return;
1375
+ }
1376
+ case "string":
1377
+ return typeof fieldConfig.initial === "string" ? `"${fieldConfig.initial}"` : '""';
1378
+ case "array":
1379
+ return "<<>>";
1380
+ default:
1381
+ return;
1320
1382
  }
1321
- return '"v1"';
1383
+ }
1384
+ legacyValuesInitialValue(fieldConfig) {
1385
+ if (!("values" in fieldConfig) || !Array.isArray(fieldConfig.values))
1386
+ return;
1387
+ const [first] = fieldConfig.values;
1388
+ return first === undefined ? undefined : `"${first}"`;
1322
1389
  }
1323
1390
  defineValueTypes() {
1324
1391
  this.line("\\* Generic value type for sequences and maps");
@@ -7995,7 +8062,7 @@ function displayExpressionWarnings(result) {
7995
8062
  function displayMeshOrPeerSignalWarnings(analysis, declaredMeshDocs) {
7996
8063
  const findings = computeMeshOrPeerSignalFindings(analysis, declaredMeshDocs);
7997
8064
  if (findings.length === 0)
7998
- return;
8065
+ return 0;
7999
8066
  const hasUndeclaredMesh = findings.some((f) => f.signalKind === "mesh");
8000
8067
  const hasPeer = findings.some((f) => f.signalKind === "peer");
8001
8068
  console.log(color(`
@@ -8018,6 +8085,61 @@ function displayMeshOrPeerSignalWarnings(analysis, declaredMeshDocs) {
8018
8085
  console.log(color(` at ${f.location.file}:${f.location.line}`, COLORS.gray));
8019
8086
  console.log();
8020
8087
  }
8088
+ return findings.length;
8089
+ }
8090
+ function isStrictMode() {
8091
+ return process.argv.includes("--strict") || process.env.POLLY_VERIFY_STRICT === "1";
8092
+ }
8093
+ function displayModelCoverage(report) {
8094
+ const { unwrittenFields, unconstrainedMutators, fieldCoverage } = report;
8095
+ if (unwrittenFields.length === 0 && unconstrainedMutators.length === 0) {
8096
+ console.log(color(`✓ Model coverage: all ${fieldCoverage.length} declared field(s) written by a modelled handler`, COLORS.green));
8097
+ console.log();
8098
+ return;
8099
+ }
8100
+ if (unwrittenFields.length > 0) {
8101
+ console.log(color(`
8102
+ ⚠️ ${unwrittenFields.length} declared state field(s) written by NO modelled handler (polly#160):`, COLORS.yellow));
8103
+ for (const f of unwrittenFields) {
8104
+ console.log(color(` • ${f}`, COLORS.yellow));
8105
+ }
8106
+ console.log(color(" The model carries a variable no transition can change. Either it is dead", COLORS.gray));
8107
+ console.log(color(" state (drop it from the verified surface) or its mutating path was never", COLORS.gray));
8108
+ console.log(color(" modelled — the omission class both TLC and mutation testing miss.", COLORS.gray));
8109
+ console.log();
8110
+ }
8111
+ if (unconstrainedMutators.length > 0) {
8112
+ console.log(color(`ℹ️ ${unconstrainedMutators.length} handler(s) mutate declared state with no ensures() postcondition:`, COLORS.gray));
8113
+ for (const m of unconstrainedMutators) {
8114
+ console.log(color(` • ${m.handler} writes {${m.fields.join(", ")}} — ${m.location.file}:${m.location.line}`, COLORS.gray));
8115
+ }
8116
+ console.log(color(" The checker explores these transitions but asserts nothing about their effect.", COLORS.gray));
8117
+ console.log();
8118
+ }
8119
+ }
8120
+ async function runModelCoverage(typedConfig, typedAnalysis, meshFindingCount) {
8121
+ const { computeModelCoverage: computeModelCoverage2, strictCoverageReasons: strictCoverageReasons2 } = await Promise.resolve().then(() => exports_model_coverage);
8122
+ const stateFields = Object.keys(typedConfig.state ?? {});
8123
+ const coverage = computeModelCoverage2(stateFields, typedAnalysis.handlers);
8124
+ displayModelCoverage(coverage);
8125
+ if (!isStrictMode())
8126
+ return;
8127
+ const reasons = strictCoverageReasons2(coverage, meshFindingCount);
8128
+ if (reasons.length === 0) {
8129
+ console.log(color("✓ Strict mode: model coverage complete", COLORS.green));
8130
+ console.log();
8131
+ return;
8132
+ }
8133
+ console.log(color(`❌ Strict mode: model coverage incomplete
8134
+ `, COLORS.red));
8135
+ for (const reason of reasons) {
8136
+ console.log(color(` • ${reason}`, COLORS.red));
8137
+ }
8138
+ console.log();
8139
+ console.log(color(" --strict fails closed on these so an unmodelled path cannot pass with a", COLORS.gray));
8140
+ console.log(color(" green check. Re-run without --strict to treat them as warnings.", COLORS.gray));
8141
+ console.log();
8142
+ process.exit(1);
8021
8143
  }
8022
8144
  async function verifyCommand() {
8023
8145
  const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
@@ -8066,6 +8188,14 @@ Stack trace:`, COLORS.gray));
8066
8188
  process.exit(1);
8067
8189
  }
8068
8190
  }
8191
+ function getMeshDocIds(config) {
8192
+ if (typeof config !== "object" || config === null || !("mesh" in config))
8193
+ return [];
8194
+ const mesh = config.mesh;
8195
+ if (typeof mesh !== "object" || mesh === null)
8196
+ return [];
8197
+ return Object.keys(mesh);
8198
+ }
8069
8199
  function getTimeout(config) {
8070
8200
  return config.verification?.timeout ?? 0;
8071
8201
  }
@@ -8110,9 +8240,10 @@ async function runFullVerification(configPath) {
8110
8240
  if (exprValidation.warnings.length > 0) {
8111
8241
  displayExpressionWarnings(exprValidation);
8112
8242
  }
8113
- const declaredMeshDocs = new Set(Object.keys(typedConfig.mesh ?? {}));
8114
- displayMeshOrPeerSignalWarnings(typedAnalysis, declaredMeshDocs);
8243
+ const declaredMeshDocs = new Set(getMeshDocIds(typedConfig));
8244
+ const meshFindingCount = displayMeshOrPeerSignalWarnings(typedAnalysis, declaredMeshDocs);
8115
8245
  await runCoupledFieldsLint(typedConfig, typedAnalysis);
8246
+ await runModelCoverage(typedConfig, typedAnalysis, meshFindingCount);
8116
8247
  if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
8117
8248
  await runSubsystemVerification(typedConfig, typedAnalysis);
8118
8249
  return;
@@ -8382,8 +8513,7 @@ function findMeshSeedSpecDir() {
8382
8513
  return null;
8383
8514
  }
8384
8515
  async function runMeshSeedGuard(docker, specDir, config) {
8385
- const mesh = config.mesh;
8386
- if (!mesh || Object.keys(mesh).length === 0)
8516
+ if (getMeshDocIds(config).length === 0)
8387
8517
  return;
8388
8518
  const sourceDir = findMeshSeedSpecDir();
8389
8519
  if (!sourceDir) {
@@ -8490,6 +8620,11 @@ ${color("Commands:", COLORS.blue)}
8490
8620
  ${color("bun verify", COLORS.green)}
8491
8621
  Run verification (validates config, generates specs, runs TLC)
8492
8622
 
8623
+ ${color("bun verify --strict", COLORS.green)}
8624
+ Fail closed (non-zero exit) on model-coverage gaps: a declared state
8625
+ field no handler writes, or an unverified $meshState/$peerState predicate.
8626
+ Also via ${color("POLLY_VERIFY_STRICT=1", COLORS.yellow)}.
8627
+
8493
8628
  ${color("bun verify --setup", COLORS.green)}
8494
8629
  Analyze codebase and generate configuration file
8495
8630
 
@@ -8553,4 +8688,4 @@ main().catch((error) => {
8553
8688
  process.exit(1);
8554
8689
  });
8555
8690
 
8556
- //# debugId=43F6AECD3B4E1E6264756E2164756E21
8691
+ //# debugId=9530CFE43867B27164756E2164756E21