@fairfox/polly 0.72.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 (35) hide show
  1. package/dist/src/elysia/index.js +464 -4
  2. package/dist/src/elysia/index.js.map +6 -4
  3. package/dist/src/peer.d.ts +2 -0
  4. package/dist/src/peer.js +468 -4
  5. package/dist/src/peer.js.map +8 -5
  6. package/dist/src/polly-ui/ActionInput.d.ts +2 -1
  7. package/dist/src/polly-ui/ActionSelect.d.ts +2 -1
  8. package/dist/src/polly-ui/Button.d.ts +4 -0
  9. package/dist/src/polly-ui/Cluster.d.ts +2 -1
  10. package/dist/src/polly-ui/Code.d.ts +2 -1
  11. package/dist/src/polly-ui/Surface.d.ts +12 -1
  12. package/dist/src/polly-ui/Text.d.ts +23 -11
  13. package/dist/src/polly-ui/index.css +42 -9
  14. package/dist/src/polly-ui/index.js +59 -6
  15. package/dist/src/polly-ui/index.js.map +11 -10
  16. package/dist/src/polly-ui/internal/passthrough.d.ts +25 -0
  17. package/dist/src/polly-ui/styles.css +57 -9
  18. package/dist/src/polly-ui/theme.css +1 -0
  19. package/dist/src/shared/lib/peer-repo-server.d.ts +18 -0
  20. package/dist/src/shared/lib/sweep-sealed.d.ts +111 -0
  21. package/dist/tools/test/src/browser/run.js +42 -33
  22. package/dist/tools/test/src/browser/run.js.map +6 -5
  23. package/dist/tools/test/src/browser/runner-core.d.ts +32 -0
  24. package/dist/tools/test/src/e2e-mesh/index.js +193 -171
  25. package/dist/tools/test/src/e2e-mesh/index.js.map +4 -4
  26. package/dist/tools/test/src/visual/index.js +248 -229
  27. package/dist/tools/test/src/visual/index.js.map +5 -5
  28. package/dist/tools/verify/specs/tla/MeshSeed.cfg +27 -0
  29. package/dist/tools/verify/specs/tla/MeshSeed.tla +179 -0
  30. package/dist/tools/verify/specs/tla/README.md +11 -1
  31. package/dist/tools/verify/src/cli.js +55 -1
  32. package/dist/tools/verify/src/cli.js.map +6 -5
  33. package/dist/tools/visualize/src/cli.js +72 -2
  34. package/dist/tools/visualize/src/cli.js.map +5 -5
  35. 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
 
@@ -7535,6 +7535,20 @@ async function analyzeCodebase(options) {
7535
7535
  const extractor = new TypeExtractor(options.tsConfigPath);
7536
7536
  return extractor.analyzeCodebase(options.stateFilePath);
7537
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
+
7538
7552
  // tools/verify/src/cli.ts
7539
7553
  var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/verify/src";
7540
7554
  var COLORS = {
@@ -7921,6 +7935,17 @@ async function runMonolithicVerification(config, analysis) {
7921
7935
  timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
7922
7936
  maxDepth
7923
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
+ }
7924
7949
  displayVerificationResults(result, specDir);
7925
7950
  }
7926
7951
  async function runSubsystemVerification(config, analysis) {
@@ -8131,6 +8156,35 @@ function findAndCopyBaseSpec(specDir) {
8131
8156
  }
8132
8157
  }
8133
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
+ }
8134
8188
  async function setupDocker() {
8135
8189
  const { DockerRunner: DockerRunner2 } = await Promise.resolve().then(() => (init_docker(), exports_docker));
8136
8190
  console.log(color("\uD83D\uDC33 Checking Docker...", COLORS.blue));
@@ -8287,4 +8341,4 @@ main().catch((error) => {
8287
8341
  process.exit(1);
8288
8342
  });
8289
8343
 
8290
- //# debugId=15266168B7768F5064756E2164756E21
8344
+ //# debugId=30F069FEDCAB85F764756E2164756E21