@fairfox/polly 0.77.3 → 0.79.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 (87) hide show
  1. package/dist/cli/polly.js +46 -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 +87 -46
  15. package/dist/src/mesh.js.map +12 -11
  16. package/dist/src/peer.js +7 -3
  17. package/dist/src/peer.js.map +6 -6
  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/mesh-client.d.ts +38 -0
  30. package/dist/src/shared/lib/mesh-signaling-client.d.ts +6 -5
  31. package/dist/src/shared/lib/mesh-state.d.ts +21 -0
  32. package/dist/src/shared/lib/message-bus.js.map +3 -3
  33. package/dist/src/shared/lib/peer-relay-adapter.d.ts +5 -0
  34. package/dist/src/shared/lib/peer-repo-server.d.ts +15 -0
  35. package/dist/src/shared/lib/resource.js +11 -2
  36. package/dist/src/shared/lib/resource.js.map +6 -6
  37. package/dist/src/shared/lib/state.d.ts +20 -0
  38. package/dist/src/shared/lib/state.js +11 -1
  39. package/dist/src/shared/lib/state.js.map +5 -5
  40. package/dist/src/shared/state/app-state.js +10 -1
  41. package/dist/src/shared/state/app-state.js.map +5 -5
  42. package/dist/tools/init/src/cli.js +23 -2
  43. package/dist/tools/init/src/cli.js.map +4 -4
  44. package/dist/tools/init/templates/pwa/package.json.template +1 -1
  45. package/dist/tools/init/templates/pwa/src/service-worker.ts.template +26 -15
  46. package/dist/tools/init/templates/pwa/src/shared-worker.ts.template +13 -3
  47. package/dist/tools/init/templates/pwa/tsconfig.json.template +2 -2
  48. package/dist/tools/init/templates/pwa/tsconfig.worker.json.template +17 -0
  49. package/dist/tools/test/src/browser/index.js +5 -2
  50. package/dist/tools/test/src/browser/index.js.map +3 -3
  51. package/dist/tools/test/src/contrast/index.js +20 -15
  52. package/dist/tools/test/src/contrast/index.js.map +3 -3
  53. package/dist/tools/test/src/e2e-cli/index.d.ts +10 -0
  54. package/dist/tools/test/src/e2e-cli/run-cli.d.ts +25 -0
  55. package/dist/tools/test/src/e2e-cli/with-temp-dir.d.ts +15 -0
  56. package/dist/tools/test/src/e2e-mesh/index.js +12 -7
  57. package/dist/tools/test/src/e2e-mesh/index.js.map +4 -4
  58. package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +7 -1
  59. package/dist/tools/test/src/e2e-relay/index.d.ts +12 -0
  60. package/dist/tools/test/src/e2e-relay/wait-for-relay-convergence.d.ts +27 -0
  61. package/dist/tools/test/src/e2e-relay/with-repo-server.d.ts +24 -0
  62. package/dist/tools/test/src/e2e-shared/assert.d.ts +18 -0
  63. package/dist/tools/test/src/e2e-shared/contract.d.ts +40 -0
  64. package/dist/tools/test/src/e2e-shared/index.d.ts +2 -0
  65. package/dist/tools/test/src/tiers/args.d.ts +23 -0
  66. package/dist/tools/test/src/tiers/cli.d.ts +2 -0
  67. package/dist/tools/test/src/tiers/cli.js +490 -0
  68. package/dist/tools/test/src/tiers/cli.js.map +16 -0
  69. package/dist/tools/test/src/tiers/detect.d.ts +12 -0
  70. package/dist/tools/test/src/tiers/discover.d.ts +2 -0
  71. package/dist/tools/test/src/tiers/engine.d.ts +3 -0
  72. package/dist/tools/test/src/tiers/index.d.ts +14 -0
  73. package/dist/tools/test/src/tiers/protocol.d.ts +10 -0
  74. package/dist/tools/test/src/tiers/reporter.d.ts +12 -0
  75. package/dist/tools/test/src/tiers/types.d.ts +94 -0
  76. package/dist/tools/test/src/tiers/worker.d.ts +2 -0
  77. package/dist/tools/test/src/tiers/worker.js +60 -0
  78. package/dist/tools/test/src/tiers/worker.js.map +12 -0
  79. package/dist/tools/verify/src/cli.js +322 -27
  80. package/dist/tools/verify/src/cli.js.map +13 -10
  81. package/dist/tools/verify/src/config.d.ts +10 -0
  82. package/dist/tools/verify/src/config.js.map +2 -2
  83. package/dist/tools/verify/src/stryker/index.js +20 -11
  84. package/dist/tools/verify/src/stryker/index.js.map +3 -3
  85. package/dist/tools/visualize/src/cli.js +8 -5
  86. package/dist/tools/visualize/src/cli.js.map +4 -4
  87. package/package.json +16 -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,6 +249,110 @@ 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
+
312
+ // tools/verify/src/analysis/coupled-fields.ts
313
+ var exports_coupled_fields = {};
314
+ __export(exports_coupled_fields, {
315
+ checkCoupledFields: () => checkCoupledFields
316
+ });
317
+ function norm2(field) {
318
+ return field.replace(/_/g, ".");
319
+ }
320
+ function checkCoupledFields(coupledFields, handlers) {
321
+ const violations = [];
322
+ for (const rawGroup of coupledFields) {
323
+ const group = rawGroup.map(norm2);
324
+ const groupSet = new Set(group);
325
+ if (groupSet.size < 2)
326
+ continue;
327
+ for (const handler of handlers) {
328
+ const violation = subsetViolation(group, groupSet, handler);
329
+ if (violation)
330
+ violations.push(violation);
331
+ }
332
+ }
333
+ return { valid: violations.length === 0, violations };
334
+ }
335
+ function writtenFields(group, handler) {
336
+ const written = new Set;
337
+ for (const assignment of handler.assignments) {
338
+ const field = norm2(assignment.field);
339
+ if (group.has(field))
340
+ written.add(field);
341
+ }
342
+ return written;
343
+ }
344
+ function subsetViolation(group, groupSet, handler) {
345
+ const written = writtenFields(groupSet, handler);
346
+ if (written.size === 0 || written.size === groupSet.size)
347
+ return null;
348
+ return {
349
+ group,
350
+ handler: handler.messageType,
351
+ written: group.filter((f) => written.has(f)),
352
+ missing: group.filter((f) => !written.has(f))
353
+ };
354
+ }
355
+
252
356
  // tools/verify/src/analysis/non-interference.ts
253
357
  var exports_non_interference = {};
254
358
  __export(exports_non_interference, {
@@ -1253,28 +1357,35 @@ var init_tla = __esm(() => {
1253
1357
  return key.replace(/[^A-Za-z0-9_]/g, "_");
1254
1358
  }
1255
1359
  fieldConfigInitialValue(_path, fieldConfig, _config) {
1256
- const fc = fieldConfig;
1257
- if (fc["type"] === "boolean")
1258
- return "FALSE";
1259
- if (fc["type"] === "number") {
1260
- const min = typeof fc["min"] === "number" ? fc["min"] : 0;
1261
- return String(min);
1262
- }
1263
- if (fc["type"] === "enum" && Array.isArray(fc["values"]) && fc["values"].length > 0) {
1264
- return `"${String(fc["values"][0])}"`;
1265
- }
1266
- if (fc["type"] === "string") {
1267
- return typeof fc["initial"] === "string" ? `"${fc["initial"]}"` : '""';
1268
- }
1269
- if (fc["type"] === "array") {
1270
- return "<<>>";
1271
- }
1272
- if (Array.isArray(fc.values)) {
1273
- const values = fc.values;
1274
- if (values.length > 0)
1275
- 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;
1276
1382
  }
1277
- 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}"`;
1278
1389
  }
1279
1390
  defineValueTypes() {
1280
1391
  this.line("\\* Generic value type for sequences and maps");
@@ -2678,6 +2789,9 @@ var init_tla = __esm(() => {
2678
2789
  this.indent--;
2679
2790
  this.indent--;
2680
2791
  this.line("");
2792
+ if (config.capabilities && config.capabilities.length > 0) {
2793
+ this.addCapabilityInvariants(config.capabilities);
2794
+ }
2681
2795
  if (this.extractedInvariants.length > 0) {
2682
2796
  this.line("\\* Extracted invariants from code annotations");
2683
2797
  this.line("");
@@ -2694,6 +2808,15 @@ var init_tla = __esm(() => {
2694
2808
  this.line("\\* State constraint to bound state space");
2695
2809
  this.addStateConstraint(config, _analysis);
2696
2810
  }
2811
+ addCapabilityInvariants(capabilities) {
2812
+ for (const cap of capabilities) {
2813
+ this.extractedInvariants.push({
2814
+ name: `Capability_${cap.name}`,
2815
+ description: cap.message ? `polly#160 capability: ${cap.message}` : `polly#160 capability: ${cap.name} requires its precondition`,
2816
+ expression: `(!(${cap.enabledBy})) || (${cap.requires})`
2817
+ });
2818
+ }
2819
+ }
2697
2820
  addTemporalConstraints(constraints) {
2698
2821
  this.line("\\* Tier 2: Temporal constraint invariants");
2699
2822
  this.line("\\* Enforce ordering requirements between message types");
@@ -3910,6 +4033,85 @@ function generateConfig(analysis, projectType = "chrome-extension") {
3910
4033
  // tools/verify/src/config/parser.ts
3911
4034
  import * as fs from "node:fs";
3912
4035
  import * as path from "node:path";
4036
+
4037
+ // tools/verify/src/config/capability-validation.ts
4038
+ init_expression_validator();
4039
+ function validateCapabilities(capabilities, stateConfig) {
4040
+ if (!capabilities || capabilities.length === 0)
4041
+ return [];
4042
+ const issues = [];
4043
+ const configKeys = new Set(Object.keys(stateConfig));
4044
+ for (const cap of capabilities) {
4045
+ const name = cap.name?.trim();
4046
+ if (!name) {
4047
+ issues.push({
4048
+ type: "invalid_value",
4049
+ severity: "error",
4050
+ field: "capabilities",
4051
+ message: "Capability is missing a name.",
4052
+ suggestion: "Give each capability a unique name; it becomes the TLA+ invariant identifier."
4053
+ });
4054
+ continue;
4055
+ }
4056
+ issues.push(...validateExpression(name, "enabledBy", cap.enabledBy, configKeys, stateConfig), ...validateExpression(name, "requires", cap.requires, configKeys, stateConfig));
4057
+ }
4058
+ return issues;
4059
+ }
4060
+ function validateExpression(name, slot, expr, configKeys, stateConfig) {
4061
+ const field = `capabilities.${name}.${slot}`;
4062
+ if (!expr?.trim()) {
4063
+ return [
4064
+ {
4065
+ type: "capability_empty_expression",
4066
+ severity: "error",
4067
+ field,
4068
+ message: `Capability "${name}" has an empty ${slot} expression.`,
4069
+ suggestion: `Provide a state expression for ${slot}, e.g. "state.authenticated".`
4070
+ }
4071
+ ];
4072
+ }
4073
+ const refs = extractFieldRefs(expr);
4074
+ if (refs.length === 0) {
4075
+ return [
4076
+ {
4077
+ type: "capability_empty_expression",
4078
+ severity: "error",
4079
+ field,
4080
+ message: `Capability "${name}" ${slot} expression "${expr}" references no state field.`,
4081
+ suggestion: 'Reference state via the state./.value form (e.g. "state.authReady"); a bare identifier produces a silently-vacuous invariant.'
4082
+ }
4083
+ ];
4084
+ }
4085
+ return refs.filter((ref) => !fieldInConfig(ref, configKeys, stateConfig)).map((ref) => ({
4086
+ type: "capability_unknown_field",
4087
+ severity: "error",
4088
+ field,
4089
+ message: `Capability "${name}" ${slot} references "${ref}", which is not in the state config.`,
4090
+ suggestion: `Add "${ref}" to state, or correct the expression. Known fields: ${[...configKeys].join(", ")}`
4091
+ }));
4092
+ }
4093
+ function validateCoupledFields(coupledFields, stateConfig) {
4094
+ if (!coupledFields || coupledFields.length === 0)
4095
+ return [];
4096
+ const issues = [];
4097
+ const configKeys = new Set(Object.keys(stateConfig));
4098
+ coupledFields.forEach((group, i) => {
4099
+ for (const field of group) {
4100
+ if (!fieldInConfig(field, configKeys, stateConfig)) {
4101
+ issues.push({
4102
+ type: "coupled_unknown_field",
4103
+ severity: "error",
4104
+ field: `coupledFields[${i}]`,
4105
+ message: `Coupled field "${field}" is not in the state config.`,
4106
+ suggestion: `Use a declared state field. Known fields: ${[...configKeys].join(", ")}`
4107
+ });
4108
+ }
4109
+ }
4110
+ });
4111
+ return issues;
4112
+ }
4113
+
4114
+ // tools/verify/src/config/parser.ts
3913
4115
  class ConfigValidator {
3914
4116
  issues = [];
3915
4117
  validate(configPath) {
@@ -4032,6 +4234,8 @@ class ConfigValidator {
4032
4234
  if (config.subsystems) {
4033
4235
  this.validateSubsystems(config.subsystems, config.state, config.messages);
4034
4236
  }
4237
+ this.issues.push(...validateCapabilities(config.capabilities, config.state));
4238
+ this.issues.push(...validateCoupledFields(config.coupledFields, config.state));
4035
4239
  }
4036
4240
  findNullPlaceholders(obj, path2) {
4037
4241
  if (obj === null || obj === undefined) {
@@ -7858,7 +8062,7 @@ function displayExpressionWarnings(result) {
7858
8062
  function displayMeshOrPeerSignalWarnings(analysis, declaredMeshDocs) {
7859
8063
  const findings = computeMeshOrPeerSignalFindings(analysis, declaredMeshDocs);
7860
8064
  if (findings.length === 0)
7861
- return;
8065
+ return 0;
7862
8066
  const hasUndeclaredMesh = findings.some((f) => f.signalKind === "mesh");
7863
8067
  const hasPeer = findings.some((f) => f.signalKind === "peer");
7864
8068
  console.log(color(`
@@ -7881,6 +8085,61 @@ function displayMeshOrPeerSignalWarnings(analysis, declaredMeshDocs) {
7881
8085
  console.log(color(` at ${f.location.file}:${f.location.line}`, COLORS.gray));
7882
8086
  console.log();
7883
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);
7884
8143
  }
7885
8144
  async function verifyCommand() {
7886
8145
  const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
@@ -7929,6 +8188,14 @@ Stack trace:`, COLORS.gray));
7929
8188
  process.exit(1);
7930
8189
  }
7931
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
+ }
7932
8199
  function getTimeout(config) {
7933
8200
  return config.verification?.timeout ?? 0;
7934
8201
  }
@@ -7941,6 +8208,28 @@ function getMaxDepth(config) {
7941
8208
  }
7942
8209
  return;
7943
8210
  }
8211
+ async function runCoupledFieldsLint(config, analysis) {
8212
+ const groups = config.coupledFields ?? [];
8213
+ if (groups.length === 0)
8214
+ return;
8215
+ const { checkCoupledFields: checkCoupledFields2 } = await Promise.resolve().then(() => exports_coupled_fields);
8216
+ const result = checkCoupledFields2(groups, analysis.handlers);
8217
+ if (result.valid) {
8218
+ console.log(color("✓ Coupled fields: verified (no partial-subset writes)", COLORS.green));
8219
+ console.log();
8220
+ return;
8221
+ }
8222
+ console.log(color(`⚠️ Coupled-field violations detected:
8223
+ `, COLORS.yellow));
8224
+ for (const v of result.violations) {
8225
+ console.log(color(` • Handler "${v.handler}" writes {${v.written.join(", ")}} but not {${v.missing.join(", ")}} of coupled group {${v.group.join(", ")}}`, COLORS.yellow));
8226
+ }
8227
+ console.log();
8228
+ console.log(color(" These fields are declared to move together; a capability granted without its", COLORS.yellow));
8229
+ console.log(color(" precondition is a likely cause. Co-write the full group in each handler, or model", COLORS.yellow));
8230
+ console.log(color(" the relationship with a `capabilities` invariant.", COLORS.yellow));
8231
+ console.log();
8232
+ }
7944
8233
  async function runFullVerification(configPath) {
7945
8234
  const config = await loadVerificationConfig(configPath);
7946
8235
  const analysis = await runCodebaseAnalysis();
@@ -7951,8 +8240,10 @@ async function runFullVerification(configPath) {
7951
8240
  if (exprValidation.warnings.length > 0) {
7952
8241
  displayExpressionWarnings(exprValidation);
7953
8242
  }
7954
- const declaredMeshDocs = new Set(Object.keys(typedConfig.mesh ?? {}));
7955
- displayMeshOrPeerSignalWarnings(typedAnalysis, declaredMeshDocs);
8243
+ const declaredMeshDocs = new Set(getMeshDocIds(typedConfig));
8244
+ const meshFindingCount = displayMeshOrPeerSignalWarnings(typedAnalysis, declaredMeshDocs);
8245
+ await runCoupledFieldsLint(typedConfig, typedAnalysis);
8246
+ await runModelCoverage(typedConfig, typedAnalysis, meshFindingCount);
7956
8247
  if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
7957
8248
  await runSubsystemVerification(typedConfig, typedAnalysis);
7958
8249
  return;
@@ -8222,8 +8513,7 @@ function findMeshSeedSpecDir() {
8222
8513
  return null;
8223
8514
  }
8224
8515
  async function runMeshSeedGuard(docker, specDir, config) {
8225
- const mesh = config.mesh;
8226
- if (!mesh || Object.keys(mesh).length === 0)
8516
+ if (getMeshDocIds(config).length === 0)
8227
8517
  return;
8228
8518
  const sourceDir = findMeshSeedSpecDir();
8229
8519
  if (!sourceDir) {
@@ -8330,6 +8620,11 @@ ${color("Commands:", COLORS.blue)}
8330
8620
  ${color("bun verify", COLORS.green)}
8331
8621
  Run verification (validates config, generates specs, runs TLC)
8332
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
+
8333
8628
  ${color("bun verify --setup", COLORS.green)}
8334
8629
  Analyze codebase and generate configuration file
8335
8630
 
@@ -8393,4 +8688,4 @@ main().catch((error) => {
8393
8688
  process.exit(1);
8394
8689
  });
8395
8690
 
8396
- //# debugId=A59978709BAED66964756E2164756E21
8691
+ //# debugId=9530CFE43867B27164756E2164756E21