@fairfox/polly 0.71.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/src/client/index.js +2 -2
  2. package/dist/src/client/index.js.map +2 -2
  3. package/dist/src/elysia/index.js +464 -4
  4. package/dist/src/elysia/index.js.map +6 -4
  5. package/dist/src/peer.d.ts +2 -0
  6. package/dist/src/peer.js +468 -4
  7. package/dist/src/peer.js.map +8 -5
  8. package/dist/src/polly-ui/ActionInput.d.ts +12 -2
  9. package/dist/src/polly-ui/ActionSelect.d.ts +36 -0
  10. package/dist/src/polly-ui/Button.d.ts +4 -0
  11. package/dist/src/polly-ui/Cluster.d.ts +36 -0
  12. package/dist/src/polly-ui/Code.d.ts +18 -0
  13. package/dist/src/polly-ui/Surface.d.ts +12 -1
  14. package/dist/src/polly-ui/Text.d.ts +43 -0
  15. package/dist/src/polly-ui/index.css +320 -194
  16. package/dist/src/polly-ui/index.d.ts +5 -1
  17. package/dist/src/polly-ui/index.js +533 -284
  18. package/dist/src/polly-ui/index.js.map +14 -8
  19. package/dist/src/polly-ui/internal/dispatch-action.d.ts +13 -0
  20. package/dist/src/polly-ui/internal/passthrough.d.ts +25 -0
  21. package/dist/src/polly-ui/markdown.js +3 -3
  22. package/dist/src/polly-ui/markdown.js.map +2 -2
  23. package/dist/src/polly-ui/styles.css +345 -194
  24. package/dist/src/polly-ui/theme.css +1 -0
  25. package/dist/src/shared/lib/peer-repo-server.d.ts +18 -0
  26. package/dist/src/shared/lib/sweep-sealed.d.ts +111 -0
  27. package/dist/tools/quality/src/cli.js +6 -2
  28. package/dist/tools/quality/src/cli.js.map +3 -3
  29. package/dist/tools/quality/src/index.js +6 -2
  30. package/dist/tools/quality/src/index.js.map +3 -3
  31. package/dist/tools/test/src/browser/run.js +89 -49
  32. package/dist/tools/test/src/browser/run.js.map +6 -5
  33. package/dist/tools/test/src/browser/runner-core.d.ts +32 -0
  34. package/dist/tools/test/src/e2e-mesh/index.js +193 -171
  35. package/dist/tools/test/src/e2e-mesh/index.js.map +4 -4
  36. package/dist/tools/test/src/visual/index.js +270 -251
  37. package/dist/tools/test/src/visual/index.js.map +5 -5
  38. package/dist/tools/verify/specs/tla/MeshSeed.cfg +27 -0
  39. package/dist/tools/verify/specs/tla/MeshSeed.tla +179 -0
  40. package/dist/tools/verify/specs/tla/README.md +11 -1
  41. package/dist/tools/verify/src/cli.js +136 -51
  42. package/dist/tools/verify/src/cli.js.map +9 -7
  43. package/dist/tools/visualize/src/cli.js +72 -2
  44. package/dist/tools/visualize/src/cli.js.map +5 -5
  45. package/package.json +3 -2
@@ -0,0 +1,27 @@
1
+ SPECIFICATION Spec
2
+
3
+ \* Three peers. The concurrent-seed race needs only two seeders; the
4
+ \* third costs nothing and gives margin. The state space is tiny — each
5
+ \* peer's slot moves once, from Unseeded to seeded — so TLC exhausts it
6
+ \* instantly.
7
+ \*
8
+ \* SeedDeterministic = TRUE models the polly#113 fix. The verify CLI
9
+ \* rewrites this constant to FALSE when POLLY_113_DISABLE_FIX is set, to
10
+ \* falsify the guard: under FALSE, TLC must report a SeedConvergence
11
+ \* violation.
12
+ CONSTANTS
13
+ Peers = {peer_a, peer_b, peer_c}
14
+ SeedDeterministic = TRUE
15
+
16
+ \* MeshSeed is a terminating protocol: once every peer has seeded, no
17
+ \* action is enabled. That terminal state is the goal, not a deadlock
18
+ \* bug, so deadlock checking is disabled — EventuallySeeded is the
19
+ \* property that asserts the protocol actually reaches it.
20
+ CHECK_DEADLOCK FALSE
21
+
22
+ INVARIANTS
23
+ TypeOK
24
+ SeedConvergence
25
+
26
+ PROPERTIES
27
+ EventuallySeeded
@@ -0,0 +1,179 @@
1
+ -------------------------- MODULE MeshSeed --------------------------
2
+ (*
3
+ Formal specification of Polly's $meshState concurrent first-time seed.
4
+
5
+ This spec exists to close the polly#114 gap: the polly#113 concurrent-
6
+ seed race lived entirely outside the verify and visualise pipelines, so
7
+ "all green" told a mesh-using consumer nothing about it. MeshSeed makes
8
+ the seed path a first-class, model-checked property.
9
+
10
+ The race
11
+ --------
12
+
13
+ When a device first reads a $meshState document it materialises the
14
+ document against empty local storage — the loadOrSeed path in
15
+ mesh-state.ts. If two devices do this concurrently for the same logical
16
+ key, each writes its own copy of the initial document.
17
+
18
+ - Pre-fix, loadOrSeed used `Automerge.from(initialDoc)`, which stamps
19
+ the seed change with a fresh random actorId and a `Date.now()`
20
+ timestamp. Two devices therefore produce two *distinct* Automerge
21
+ documents — same logical content, different identity. They have no
22
+ common ancestor, so a later sync cannot reconcile them: the two
23
+ seeds coexist as a permanent fork.
24
+
25
+ - The polly#113 fix switched loadOrSeed to
26
+ `Automerge.init({ actor: deriveSeedActor(docId) })` plus
27
+ `Automerge.change(doc, { time: 0 }, …)`. The actor is derived from
28
+ the docId and the change time is fixed, so every device seeding the
29
+ same key produces byte-identical content — literally the same
30
+ document. There is nothing to reconcile.
31
+
32
+ Model
33
+ -----
34
+
35
+ `SeedDeterministic` is the spec's single knob. TRUE models the
36
+ polly#113 fix; FALSE restores the pre-fix behaviour — the same toggle
37
+ the `POLLY_113_DISABLE_FIX` environment variable exposes in
38
+ seedDocumentBytes. The verify CLI flips this constant when the
39
+ environment variable is set, so a regression in the seed path is
40
+ caught here as a TLC counterexample rather than only by a hand-written
41
+ unit test.
42
+
43
+ - `doc[p]` is peer p's copy of the document, or `Unseeded` before it
44
+ has materialised one.
45
+
46
+ - `SeedValue(p)` is the content peer p writes when it seeds. Under the
47
+ fix it is a single shared constant (all peers agree); pre-fix it is
48
+ tagged by the seeding peer (every peer differs). The pre-fix model
49
+ treats every peer's seed as distinct: a specification must withstand
50
+ what is *possible*, and pre-fix divergence is possible, so the worst
51
+ case is the honest one. A nondeterministic seed choice would only
52
+ multiply states without adding insight — the divergent branch is
53
+ what matters and TLC explores it either way.
54
+
55
+ - `Sync` delivers a seeded document to a peer that holds none. A peer
56
+ that has already seeded independently is never overwritten: its
57
+ document and the remote one have no common ancestor, so no merge
58
+ reconciles them. Modelling the fork this way is sound for the safety
59
+ property below — `SeedConvergence` is violated the moment two peers
60
+ hold distinct documents, and no later step can un-violate a safety
61
+ invariant TLC has already reported.
62
+
63
+ One document is enough
64
+ ----------------------
65
+
66
+ The polly#113 race is per-docId. Distinct $meshState documents seed
67
+ through independent loadOrSeed calls, hold independent storage entries,
68
+ and never share state — there is no action by which one document's
69
+ seed influences another's. A model of one document is therefore a
70
+ sound representative of any number of them: the reachable seed/sync
71
+ interleavings of N independent documents are exactly the product of
72
+ N copies of this model, and a safety invariant that holds on one copy
73
+ holds on the product. Modelling one document keeps the state space
74
+ minimal without weakening the result.
75
+
76
+ Properties verified
77
+ -------------------
78
+
79
+ 1. TypeOK — type safety across every transition.
80
+
81
+ 2. SeedConvergence — every peer that holds the document holds the same
82
+ one. Under the fix this is invariant. Pre-fix, the trace
83
+ `Seed(p)` then `Seed(q)` reaches a state where p and q hold
84
+ distinct documents and the invariant fails — that failure is the
85
+ polly#113 race, surfaced by model checking.
86
+
87
+ 3. EventuallySeeded (liveness) — every peer eventually materialises
88
+ the document. Holds under both settings of SeedDeterministic; it
89
+ is the property that earns the WF_vars(Next) fairness conjunct.
90
+ Weak fairness on the whole next-state relation suffices because the
91
+ model is monotone: every step moves one peer from Unseeded to
92
+ seeded and none ever back, so no step can be starved of progress.
93
+ *)
94
+
95
+ CONSTANTS
96
+ Peers, \* Set of mesh peer identifiers
97
+ SeedDeterministic \* TRUE = polly#113 fix; FALSE = pre-fix seed race
98
+
99
+ VARIABLES
100
+ doc \* [Peers -> SeedValues \cup {Unseeded}]
101
+
102
+ vars == <<doc>>
103
+
104
+ \* A peer that has not yet materialised the document.
105
+ Unseeded == "unseeded"
106
+
107
+ \* The content a peer writes when it first seeds the document. Under the
108
+ \* fix every peer produces the same bytes, so the value is one shared
109
+ \* constant; pre-fix each peer produces a distinct document, modelled as
110
+ \* a value tagged by the seeding peer.
111
+ SeedValue(p) == IF SeedDeterministic THEN "seed" ELSE p
112
+
113
+ \* The set of all possible seeded values, for the type invariant.
114
+ SeedValues == IF SeedDeterministic THEN {"seed"} ELSE Peers
115
+
116
+ -----------------------------------------------------------------------------
117
+
118
+ (* Initial state: no peer has materialised the document. *)
119
+
120
+ Init ==
121
+ doc = [p \in Peers |-> Unseeded]
122
+
123
+ -----------------------------------------------------------------------------
124
+
125
+ (* Actions *)
126
+
127
+ (* A peer materialises the document for the first time against empty
128
+ local storage — Polly's $meshState loadOrSeed path. *)
129
+ Seed(p) ==
130
+ /\ doc[p] = Unseeded
131
+ /\ doc' = [doc EXCEPT ![p] = SeedValue(p)]
132
+
133
+ (* Sync delivers a seeded peer's document to a peer that holds none.
134
+ A peer that has already seeded independently is never overwritten —
135
+ see the header note on why that is sound for SeedConvergence. *)
136
+ Sync(src, dst) ==
137
+ /\ src # dst
138
+ /\ doc[src] # Unseeded
139
+ /\ doc[dst] = Unseeded
140
+ /\ doc' = [doc EXCEPT ![dst] = doc[src]]
141
+
142
+ -----------------------------------------------------------------------------
143
+
144
+ (* Next-state relation *)
145
+
146
+ Next ==
147
+ \/ \E p \in Peers : Seed(p)
148
+ \/ \E src, dst \in Peers : Sync(src, dst)
149
+
150
+ Spec == Init /\ [][Next]_vars /\ WF_vars(Next)
151
+
152
+ -----------------------------------------------------------------------------
153
+
154
+ (* Invariants *)
155
+
156
+ (* Type safety: every peer's slot is Unseeded or a valid seeded value. *)
157
+ TypeOK ==
158
+ doc \in [Peers -> SeedValues \cup {Unseeded}]
159
+
160
+ (* Seed convergence: every peer that holds the document holds the same
161
+ one. Under the polly#113 fix (SeedDeterministic = TRUE) this is
162
+ invariant. Pre-fix (FALSE) the trace Seed(p) ; Seed(q) reaches a
163
+ state where p and q hold distinct documents and TLC reports the
164
+ violation — that is the polly#113 race. *)
165
+ SeedConvergence ==
166
+ \A p, q \in Peers :
167
+ (doc[p] # Unseeded /\ doc[q] # Unseeded) => doc[p] = doc[q]
168
+
169
+ -----------------------------------------------------------------------------
170
+
171
+ (* Liveness *)
172
+
173
+ (* Every peer eventually materialises the document. Holds under both
174
+ settings of SeedDeterministic — see the header note on why weak
175
+ fairness on Next suffices for this monotone model. *)
176
+ EventuallySeeded ==
177
+ <>(\A p \in Peers : doc[p] # Unseeded)
178
+
179
+ =============================================================================
@@ -1,7 +1,7 @@
1
1
  # TLA+ Formal Specifications for Polly
2
2
 
3
3
  This directory contains formal specifications for Polly's distributed
4
- protocols using TLA+ (Temporal Logic of Actions). There are three specs:
4
+ protocols using TLA+ (Temporal Logic of Actions). There are four specs:
5
5
 
6
6
  - **MessageRouter.tla** — the original message-routing spec for the web
7
7
  extension's cross-context message bus. Single-writer, routing-focused,
@@ -21,6 +21,16 @@ protocols using TLA+ (Temporal Logic of Actions). There are three specs:
21
21
  peer is revoked, honest peers drop its future ops), and no-fabrication
22
22
  (every op in any replica has a known producer).
23
23
 
24
+ - **MeshSeed.tla** — the `$meshState` concurrent first-time seed
25
+ (polly#114). Model-checks the polly#113 race: two devices materialising
26
+ the same document concurrently against empty storage. Its single
27
+ `SeedDeterministic` constant is TRUE for the shipped fix and FALSE for
28
+ the pre-fix seed; under FALSE, TLC reports a `SeedConvergence`
29
+ violation. The verify CLI runs this spec whenever a project declares
30
+ `mesh:` documents and flips the constant when `POLLY_113_DISABLE_FIX`
31
+ is set, so a regression in `seedDocumentBytes` cannot land green.
32
+ `scripts/e2e-verify-mesh-seed.ts` runs both directions through TLC.
33
+
24
34
  Each spec has a companion `.cfg` file with the small bounded constants
25
35
  TLC uses when model-checking the spec exhaustively.
26
36
 
@@ -89,10 +89,12 @@ function isNullable(configEntry) {
89
89
  }
90
90
  return false;
91
91
  }
92
- function checkUnmodeledFields(expression, configKeys, stateConfig, messageType, conditionType, location) {
92
+ function checkUnmodeledFields(expression, configKeys, stateConfig, messageType, conditionType, location, meshSignalNames) {
93
93
  const warnings = [];
94
94
  const refs = extractFieldRefs(expression);
95
95
  for (const ref of refs) {
96
+ if (meshSignalNames.has(ref.split(".")[0] ?? ref))
97
+ continue;
96
98
  if (!fieldInConfig(ref, configKeys, stateConfig)) {
97
99
  warnings.push({
98
100
  kind: "unmodeled_field",
@@ -195,7 +197,7 @@ function checkWeakPostconditions(expression, handler, messageType, conditionType
195
197
  }
196
198
  return [];
197
199
  }
198
- function validateExpressions(handlers, stateConfig) {
200
+ function validateExpressions(handlers, stateConfig, meshSignalNames = new Set) {
199
201
  const warnings = [];
200
202
  let validCount = 0;
201
203
  const configKeys = new Set(Object.keys(stateConfig));
@@ -220,7 +222,7 @@ function validateExpressions(handlers, stateConfig) {
220
222
  };
221
223
  const condWarnings = [];
222
224
  if (!isPayloadOnly) {
223
- condWarnings.push(...checkUnmodeledFields(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
225
+ condWarnings.push(...checkUnmodeledFields(cond.expression, configKeys, stateConfig, handler.messageType, type, loc, meshSignalNames));
224
226
  }
225
227
  condWarnings.push(...checkUnsupportedMethods(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
226
228
  condWarnings.push(...checkOptionalChaining(cond.expression, handler.messageType, type, loc));
@@ -1216,13 +1218,15 @@ var init_tla = __esm(() => {
1216
1218
  fieldLines.push(`${this.sanitizeFieldName(fieldName)}: ${tlaType}`);
1217
1219
  }
1218
1220
  this.line(`\\* Document type for ${docId}`);
1219
- this.line(`MeshDoc_${this.sanitizeFieldName(docId)} == [${fieldLines.join(", ")}]`);
1221
+ this.line(`MeshDoc_${this.sanitizeIdentifier(docId)} == [${fieldLines.join(", ")}]`);
1220
1222
  this.line("");
1221
1223
  }
1222
- this.line("\\* Initial mesh-document values (one record per declared docId)");
1223
- this.line("InitialMesh == [");
1224
+ this.line("\\* Initial mesh-document values a function keyed by docId");
1225
+ this.line("InitialMesh ==");
1226
+ this.indent++;
1227
+ this.line("[d \\in MeshDocs |->");
1224
1228
  this.indent++;
1225
- const initLines = [];
1229
+ const caseArms = [];
1226
1230
  for (const docId of docIds) {
1227
1231
  const fields = mesh[docId];
1228
1232
  if (!fields)
@@ -1232,15 +1236,19 @@ var init_tla = __esm(() => {
1232
1236
  const initVal = this.fieldConfigInitialValue(`${docId}_${fieldName}`, fieldConfig, config);
1233
1237
  inner.push(`${this.sanitizeFieldName(fieldName)} |-> ${initVal}`);
1234
1238
  }
1235
- initLines.push(`"${docId}" |-> [${inner.join(", ")}]`);
1239
+ caseArms.push(`d = "${docId}" -> [${inner.join(", ")}]`);
1236
1240
  }
1237
- initLines.forEach((line, i) => {
1238
- this.line(line + (i < initLines.length - 1 ? "," : ""));
1241
+ caseArms.forEach((arm, i) => {
1242
+ const prefix = i === 0 ? "CASE " : " [] ";
1243
+ const suffix = i === caseArms.length - 1 ? "]" : "";
1244
+ this.line(prefix + arm + suffix);
1239
1245
  });
1240
- this.indent--;
1241
- this.line("]");
1246
+ this.indent -= 2;
1242
1247
  this.line("");
1243
1248
  }
1249
+ sanitizeIdentifier(key) {
1250
+ return key.replace(/[^A-Za-z0-9_]/g, "_");
1251
+ }
1244
1252
  fieldConfigInitialValue(_path, fieldConfig, _config) {
1245
1253
  const fc = fieldConfig;
1246
1254
  if (fc["type"] === "boolean")
@@ -2049,7 +2057,7 @@ var init_tla = __esm(() => {
2049
2057
  if (typeof expr !== "string")
2050
2058
  return null;
2051
2059
  const trimmed = expr.trim();
2052
- const match = trimmed.match(/^(forAllPeers|somePeer)\s*\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=>\s*([\s\S]+)\)\s*$/);
2060
+ const match = trimmed.match(/^(forAllPeers|somePeer)\s*\(\s*\(?\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)?\s*=>\s*([\s\S]+)\)\s*$/);
2053
2061
  if (!match)
2054
2062
  return null;
2055
2063
  const fn = match[1];
@@ -3217,6 +3225,42 @@ init_expression_validator();
3217
3225
  import * as fs4 from "node:fs";
3218
3226
  import * as path4 from "node:path";
3219
3227
 
3228
+ // tools/verify/src/analysis/mesh-signal-warnings.ts
3229
+ function computeMeshOrPeerSignalFindings(analysis, declaredMeshDocs) {
3230
+ const signals = analysis.meshOrPeerSignals ?? [];
3231
+ if (signals.length === 0)
3232
+ return [];
3233
+ const unverified = signals.filter((s) => !(s.kind === "mesh" && declaredMeshDocs.has(s.key)));
3234
+ if (unverified.length === 0)
3235
+ return [];
3236
+ const findings = [];
3237
+ for (const handler of analysis.handlers) {
3238
+ const scan = (conditions, kind) => {
3239
+ for (const cond of conditions) {
3240
+ for (const sig of unverified) {
3241
+ const pattern = new RegExp(`\\b${sig.variableName}\\.value\\b`);
3242
+ if (pattern.test(cond.expression)) {
3243
+ findings.push({
3244
+ handler: handler.messageType,
3245
+ kind,
3246
+ expression: cond.expression,
3247
+ signalName: sig.variableName,
3248
+ signalKind: sig.kind,
3249
+ location: {
3250
+ file: handler.location?.file ?? sig.filePath,
3251
+ line: cond.location?.line ?? handler.location?.line ?? sig.line
3252
+ }
3253
+ });
3254
+ }
3255
+ }
3256
+ }
3257
+ };
3258
+ scan(handler.preconditions, "precondition");
3259
+ scan(handler.postconditions, "postcondition");
3260
+ }
3261
+ return findings;
3262
+ }
3263
+
3220
3264
  // tools/verify/src/config/types.ts
3221
3265
  function isAdapterConfig(config) {
3222
3266
  return "adapter" in config;
@@ -7491,6 +7535,20 @@ async function analyzeCodebase(options) {
7491
7535
  const extractor = new TypeExtractor(options.tsConfigPath);
7492
7536
  return extractor.analyzeCodebase(options.stateFilePath);
7493
7537
  }
7538
+ // tools/verify/src/runner/mesh-seed.ts
7539
+ function meshSeedCfg(baseCfg, opts) {
7540
+ if (!opts.disableFix)
7541
+ return baseCfg;
7542
+ const declaration = /^([ \t]*)SeedDeterministic = TRUE[ \t]*$/m;
7543
+ if (!declaration.test(baseCfg)) {
7544
+ throw new Error("MeshSeed.cfg no longer declares `SeedDeterministic = TRUE` on its own line — the polly#114 seed-race guard cannot be falsified. Update tools/verify/src/runner/mesh-seed.ts.");
7545
+ }
7546
+ return baseCfg.replace(declaration, "$1SeedDeterministic = FALSE");
7547
+ }
7548
+ function isSeedFixDisabled(env = process.env) {
7549
+ return env["POLLY_113_DISABLE_FIX"] === "1";
7550
+ }
7551
+
7494
7552
  // tools/verify/src/cli.ts
7495
7553
  var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/verify/src";
7496
7554
  var COLORS = {
@@ -7657,7 +7715,8 @@ async function estimateCommand() {
7657
7715
  const analysis = await runCodebaseAnalysis();
7658
7716
  const typedConfig = config;
7659
7717
  const typedAnalysis = analysis;
7660
- const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
7718
+ const meshSignalNames = new Set((typedAnalysis.meshOrPeerSignals ?? []).map((s) => s.variableName));
7719
+ const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state, meshSignalNames);
7661
7720
  if (exprValidation.warnings.length > 0) {
7662
7721
  displayExpressionWarnings(exprValidation);
7663
7722
  }
@@ -7744,44 +7803,28 @@ function displayExpressionWarnings(result) {
7744
7803
  console.log();
7745
7804
  }
7746
7805
  }
7747
- function displayMeshOrPeerSignalWarnings(analysis) {
7748
- const signals = analysis.meshOrPeerSignals ?? [];
7749
- if (signals.length === 0)
7750
- return;
7751
- const findings = [];
7752
- for (const handler of analysis.handlers) {
7753
- const scanConditions = (conditions, kind) => {
7754
- for (const cond of conditions) {
7755
- for (const sig of signals) {
7756
- const pattern = new RegExp(`\\b${sig.variableName}\\.value\\b`);
7757
- if (pattern.test(cond.expression)) {
7758
- findings.push({
7759
- handler: handler.messageType,
7760
- kind,
7761
- expression: cond.expression,
7762
- signalName: sig.variableName,
7763
- signalKind: sig.kind,
7764
- location: {
7765
- file: handler.location.file,
7766
- line: cond.location?.line ?? handler.location.line
7767
- }
7768
- });
7769
- }
7770
- }
7771
- }
7772
- };
7773
- scanConditions(handler.preconditions, "precondition");
7774
- scanConditions(handler.postconditions, "postcondition");
7775
- }
7806
+ function displayMeshOrPeerSignalWarnings(analysis, declaredMeshDocs) {
7807
+ const findings = computeMeshOrPeerSignalFindings(analysis, declaredMeshDocs);
7776
7808
  if (findings.length === 0)
7777
7809
  return;
7810
+ const hasUndeclaredMesh = findings.some((f) => f.signalKind === "mesh");
7811
+ const hasPeer = findings.some((f) => f.signalKind === "peer");
7778
7812
  console.log(color(`
7779
- ⚠️ ${findings.length} predicate(s) reference a $meshState/$peerState signal (polly#117):`, COLORS.yellow));
7780
- console.log(color(" The verifier currently treats these the same as $sharedState — single-context local state.", COLORS.gray));
7781
- console.log(color(` Cross-peer semantics are NOT verified. A green run does not prove the claim across peers.
7782
- `, COLORS.gray));
7813
+ ⚠️ ${findings.length} predicate(s) reference an unverified $meshState/$peerState signal (polly#117):`, COLORS.yellow));
7814
+ console.log(color(" These signals are flattened to single-context local state, so a green run", COLORS.gray));
7815
+ console.log(color(" does NOT prove the claim holds across peers.", COLORS.gray));
7816
+ if (hasUndeclaredMesh) {
7817
+ console.log(color(" Fix ($meshState): declare the document key under `mesh: { ... }` in your", COLORS.gray));
7818
+ console.log(color(" verification config. Declared mesh docs are routed through the meshState", COLORS.gray));
7819
+ console.log(color(" namespace with a PropagateMeshOp action, so `forAllPeers(...)` claims are checked.", COLORS.gray));
7820
+ }
7821
+ if (hasPeer) {
7822
+ console.log(color(" $peerState signals have no cross-peer verification model yet.", COLORS.gray));
7823
+ }
7824
+ console.log();
7783
7825
  for (const f of findings) {
7784
- console.log(color(` ⚠ ${f.handler} ${f.kind}: ${f.signalName} is $${f.signalKind}State, not single-context`, COLORS.yellow));
7826
+ const remedy = f.signalKind === "mesh" ? "not declared in config.mesh" : "$peerState no cross-peer model";
7827
+ console.log(color(` ⚠ ${f.handler} ${f.kind}: ${f.signalName} (${remedy})`, COLORS.yellow));
7785
7828
  console.log(color(` ${f.expression}`, COLORS.gray));
7786
7829
  console.log(color(` at ${f.location.file}:${f.location.line}`, COLORS.gray));
7787
7830
  console.log();
@@ -7851,11 +7894,13 @@ async function runFullVerification(configPath) {
7851
7894
  const analysis = await runCodebaseAnalysis();
7852
7895
  const typedConfig = config;
7853
7896
  const typedAnalysis = analysis;
7854
- const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
7897
+ const meshSignalNames = new Set((typedAnalysis.meshOrPeerSignals ?? []).map((s) => s.variableName));
7898
+ const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state, meshSignalNames);
7855
7899
  if (exprValidation.warnings.length > 0) {
7856
7900
  displayExpressionWarnings(exprValidation);
7857
7901
  }
7858
- displayMeshOrPeerSignalWarnings(typedAnalysis);
7902
+ const declaredMeshDocs = new Set(Object.keys(typedConfig.mesh ?? {}));
7903
+ displayMeshOrPeerSignalWarnings(typedAnalysis, declaredMeshDocs);
7859
7904
  if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
7860
7905
  await runSubsystemVerification(typedConfig, typedAnalysis);
7861
7906
  return;
@@ -7890,6 +7935,17 @@ async function runMonolithicVerification(config, analysis) {
7890
7935
  timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
7891
7936
  maxDepth
7892
7937
  });
7938
+ const meshSeed = await runMeshSeedGuard(docker, specDir, config);
7939
+ if (meshSeed && !meshSeed.success) {
7940
+ console.log(color(`
7941
+ ❌ Mesh seed-race guard failed (polly#113 / polly#114)
7942
+ `, COLORS.red));
7943
+ displayVerificationResults(meshSeed, specDir);
7944
+ }
7945
+ if (meshSeed) {
7946
+ console.log(color(`✓ Mesh seed-race guard passed (MeshSeed.tla)
7947
+ `, COLORS.green));
7948
+ }
7893
7949
  displayVerificationResults(result, specDir);
7894
7950
  }
7895
7951
  async function runSubsystemVerification(config, analysis) {
@@ -8100,6 +8156,35 @@ function findAndCopyBaseSpec(specDir) {
8100
8156
  }
8101
8157
  }
8102
8158
  }
8159
+ function findMeshSeedSpecDir() {
8160
+ const candidates = [
8161
+ path4.join(process.cwd(), "specs", "tla"),
8162
+ path4.join(__dirname, "..", "specs", "tla"),
8163
+ path4.join(__dirname, "..", "..", "specs", "tla"),
8164
+ path4.join(process.cwd(), "node_modules", "@fairfox", "polly-verify", "specs", "tla")
8165
+ ];
8166
+ for (const dir of candidates) {
8167
+ if (fs4.existsSync(path4.join(dir, "MeshSeed.tla")))
8168
+ return dir;
8169
+ }
8170
+ return null;
8171
+ }
8172
+ async function runMeshSeedGuard(docker, specDir, config) {
8173
+ const mesh = config.mesh;
8174
+ if (!mesh || Object.keys(mesh).length === 0)
8175
+ return;
8176
+ const sourceDir = findMeshSeedSpecDir();
8177
+ if (!sourceDir) {
8178
+ console.log(color("⚠️ MeshSeed.tla not found — skipping seed-race guard", COLORS.yellow));
8179
+ return;
8180
+ }
8181
+ const fixDisabled = isSeedFixDisabled();
8182
+ fs4.copyFileSync(path4.join(sourceDir, "MeshSeed.tla"), path4.join(specDir, "MeshSeed.tla"));
8183
+ const baseCfg = fs4.readFileSync(path4.join(sourceDir, "MeshSeed.cfg"), "utf8");
8184
+ fs4.writeFileSync(path4.join(specDir, "MeshSeed.cfg"), meshSeedCfg(baseCfg, { disableFix: fixDisabled }));
8185
+ console.log(color(`⚙️ Running mesh seed-race guard (MeshSeed.tla, SeedDeterministic = ${fixDisabled ? "FALSE" : "TRUE"})...`, COLORS.blue));
8186
+ return docker.runTLC(path4.join(specDir, "MeshSeed.tla"), { workers: 1 });
8187
+ }
8103
8188
  async function setupDocker() {
8104
8189
  const { DockerRunner: DockerRunner2 } = await Promise.resolve().then(() => (init_docker(), exports_docker));
8105
8190
  console.log(color("\uD83D\uDC33 Checking Docker...", COLORS.blue));
@@ -8256,4 +8341,4 @@ main().catch((error) => {
8256
8341
  process.exit(1);
8257
8342
  });
8258
8343
 
8259
- //# debugId=E07763C4D9F04C6264756E2164756E21
8344
+ //# debugId=30F069FEDCAB85F764756E2164756E21