@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.
- package/dist/cli/polly.js +76 -3
- package/dist/cli/polly.js.map +3 -3
- package/dist/src/background/index.js.map +3 -3
- package/dist/src/background/message-router.js.map +3 -3
- package/dist/src/client/index.js +137 -32
- package/dist/src/client/index.js.map +6 -5
- package/dist/src/client/wrapper.d.ts +39 -2
- package/dist/src/elysia/index.js +22 -3
- package/dist/src/elysia/index.js.map +5 -5
- package/dist/src/elysia/route-match.d.ts +13 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +12 -2
- package/dist/src/index.js.map +7 -7
- package/dist/src/mesh.js +28 -9
- package/dist/src/mesh.js.map +10 -9
- package/dist/src/peer.js +6 -2
- package/dist/src/peer.js.map +5 -5
- package/dist/src/polly-ui/Badge.d.ts +5 -0
- package/dist/src/polly-ui/Button.d.ts +31 -6
- package/dist/src/polly-ui/Dropdown.d.ts +6 -0
- package/dist/src/polly-ui/Select.d.ts +11 -1
- package/dist/src/polly-ui/TextInput.d.ts +30 -0
- package/dist/src/polly-ui/index.css +10 -0
- package/dist/src/polly-ui/index.js +81 -32
- package/dist/src/polly-ui/index.js.map +10 -10
- package/dist/src/polly-ui/styles.css +10 -0
- package/dist/src/shared/adapters/index.js.map +3 -3
- package/dist/src/shared/lib/context-helpers.js.map +3 -3
- package/dist/src/shared/lib/message-bus.js.map +3 -3
- package/dist/src/shared/lib/resource.js +11 -2
- package/dist/src/shared/lib/resource.js.map +6 -6
- package/dist/src/shared/lib/state.d.ts +20 -0
- package/dist/src/shared/lib/state.js +11 -1
- package/dist/src/shared/lib/state.js.map +5 -5
- package/dist/src/shared/state/app-state.js +10 -1
- package/dist/src/shared/state/app-state.js.map +5 -5
- package/dist/tools/init/src/cli.js +23 -2
- package/dist/tools/init/src/cli.js.map +4 -4
- package/dist/tools/init/templates/pwa/package.json.template +1 -1
- package/dist/tools/init/templates/pwa/src/service-worker.ts.template +26 -15
- package/dist/tools/init/templates/pwa/src/shared-worker.ts.template +13 -3
- package/dist/tools/init/templates/pwa/tsconfig.json.template +2 -2
- package/dist/tools/init/templates/pwa/tsconfig.worker.json.template +17 -0
- package/dist/tools/test/src/browser/index.js +5 -2
- package/dist/tools/test/src/browser/index.js.map +3 -3
- package/dist/tools/test/src/contrast/index.js +20 -15
- package/dist/tools/test/src/contrast/index.js.map +3 -3
- package/dist/tools/test/src/coverage-policy/cli.d.ts +19 -0
- package/dist/tools/test/src/coverage-policy/cli.js +339 -0
- package/dist/tools/test/src/coverage-policy/cli.js.map +13 -0
- package/dist/tools/test/src/coverage-policy/discover.d.ts +23 -0
- package/dist/tools/test/src/coverage-policy/enforce.d.ts +54 -0
- package/dist/tools/test/src/coverage-policy/index.d.ts +10 -0
- package/dist/tools/test/src/coverage-policy/index.js +242 -0
- package/dist/tools/test/src/coverage-policy/index.js.map +13 -0
- package/dist/tools/test/src/coverage-policy/mutate-targets.d.ts +30 -0
- package/dist/tools/test/src/coverage-policy/types.d.ts +35 -0
- package/dist/tools/test/src/e2e-cli/index.d.ts +10 -0
- package/dist/tools/test/src/e2e-cli/run-cli.d.ts +25 -0
- package/dist/tools/test/src/e2e-cli/with-temp-dir.d.ts +15 -0
- package/dist/tools/test/src/e2e-mesh/index.js +29 -8
- package/dist/tools/test/src/e2e-mesh/index.js.map +7 -6
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +7 -1
- package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +8 -0
- package/dist/tools/test/src/e2e-relay/index.d.ts +12 -0
- package/dist/tools/test/src/e2e-relay/index.js +1421 -0
- package/dist/tools/test/src/e2e-relay/index.js.map +30 -0
- package/dist/tools/test/src/e2e-relay/wait-for-relay-convergence.d.ts +35 -0
- package/dist/tools/test/src/e2e-relay/with-repo-server.d.ts +33 -0
- package/dist/tools/test/src/e2e-shared/assert.d.ts +18 -0
- package/dist/tools/test/src/e2e-shared/contract.d.ts +40 -0
- package/dist/tools/test/src/e2e-shared/index.d.ts +3 -0
- package/dist/tools/test/src/e2e-shared/timeout-context.d.ts +17 -0
- package/dist/tools/test/src/index.d.ts +1 -0
- package/dist/tools/test/src/index.js +16 -1
- package/dist/tools/test/src/index.js.map +5 -4
- package/dist/tools/test/src/tiers/args.d.ts +23 -0
- package/dist/tools/test/src/tiers/cli.d.ts +2 -0
- package/dist/tools/test/src/tiers/cli.js +490 -0
- package/dist/tools/test/src/tiers/cli.js.map +16 -0
- package/dist/tools/test/src/tiers/detect.d.ts +12 -0
- package/dist/tools/test/src/tiers/discover.d.ts +2 -0
- package/dist/tools/test/src/tiers/engine.d.ts +3 -0
- package/dist/tools/test/src/tiers/index.d.ts +14 -0
- package/dist/tools/test/src/tiers/protocol.d.ts +10 -0
- package/dist/tools/test/src/tiers/reporter.d.ts +12 -0
- package/dist/tools/test/src/tiers/types.d.ts +94 -0
- package/dist/tools/test/src/tiers/worker.d.ts +2 -0
- package/dist/tools/test/src/tiers/worker.js +60 -0
- package/dist/tools/test/src/tiers/worker.js.map +12 -0
- package/dist/tools/verify/src/cli.js +165 -30
- package/dist/tools/verify/src/cli.js.map +7 -6
- package/dist/tools/verify/src/stryker/index.js +20 -11
- package/dist/tools/verify/src/stryker/index.js.map +3 -3
- package/dist/tools/visualize/src/cli.js +8 -5
- package/dist/tools/visualize/src/cli.js.map +4 -4
- 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,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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
if (
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
return
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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=
|
|
8691
|
+
//# debugId=9530CFE43867B27164756E2164756E21
|