@gigzen/populace 0.1.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 (38) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +258 -0
  3. package/adapters/buzzbuzz.mjs +247 -0
  4. package/adapters/contract.md +164 -0
  5. package/adapters/template-rest.mjs +192 -0
  6. package/adapters/template.mjs +80 -0
  7. package/examples/buzzbuzz/populace-report.html +245 -0
  8. package/examples/buzzbuzz/populace-report.json +280 -0
  9. package/examples/buzzbuzz/populace.config.mjs +51 -0
  10. package/examples/buzzbuzz/run-test.ps1 +61 -0
  11. package/examples/demo/adapters/demo.mjs +90 -0
  12. package/examples/demo/populace-report.html +230 -0
  13. package/examples/demo/populace-report.json +219 -0
  14. package/examples/demo/populace.config.mjs +22 -0
  15. package/examples/rest-api/README.md +85 -0
  16. package/examples/rest-api/adapter.mjs +166 -0
  17. package/examples/rest-api/populace.config.mjs +40 -0
  18. package/examples/rest-api/server.mjs +247 -0
  19. package/examples/token-expiry/expiry-demo.mjs +119 -0
  20. package/package.json +56 -0
  21. package/populace.config.example.mjs +65 -0
  22. package/src/cli.mjs +591 -0
  23. package/src/config.mjs +186 -0
  24. package/src/contract.mjs +130 -0
  25. package/src/diagnose.mjs +40 -0
  26. package/src/engine/agent.mjs +264 -0
  27. package/src/engine/geo.mjs +59 -0
  28. package/src/engine/index.mjs +4 -0
  29. package/src/engine/personas.mjs +115 -0
  30. package/src/engine/world.mjs +120 -0
  31. package/src/html-report.mjs +218 -0
  32. package/src/index.mjs +38 -0
  33. package/src/instrument.mjs +299 -0
  34. package/src/net.mjs +175 -0
  35. package/src/report.mjs +251 -0
  36. package/src/selftest.mjs +1369 -0
  37. package/src/smoke.mjs +274 -0
  38. package/src/version.mjs +24 -0
@@ -0,0 +1,280 @@
1
+ {
2
+ "populace": {
3
+ "version": "0.1.0",
4
+ "generatedAt": "2026-08-15T11:32:20.651Z"
5
+ },
6
+ "run": {
7
+ "app": "Buzz Buzz",
8
+ "adapter": "../../adapters/buzzbuzz.mjs",
9
+ "environment": "test",
10
+ "startedAt": "2026-08-15T11:18:42.515Z",
11
+ "durationMs": 818136,
12
+ "population": {
13
+ "agents": 10,
14
+ "cities": [
15
+ "manila",
16
+ "mumbai"
17
+ ],
18
+ "tickSeconds": 5,
19
+ "minutes": 10
20
+ }
21
+ },
22
+ "verdict": {
23
+ "status": "clean",
24
+ "problems": [],
25
+ "apiFailures": 0,
26
+ "transportFailures": 0,
27
+ "failingMethods": []
28
+ },
29
+ "population": {
30
+ "requested": 10,
31
+ "signedIn": 10,
32
+ "signupFailures": []
33
+ },
34
+ "engineErrors": [],
35
+ "activity": {
36
+ "distanceKm": 36,
37
+ "posts": 139,
38
+ "likes": 296,
39
+ "comments": 123,
40
+ "messages": 110,
41
+ "groupJoins": 16
42
+ },
43
+ "api": {
44
+ "calls": 2030,
45
+ "failures": 0,
46
+ "apiFailures": 0,
47
+ "transportFailures": 0,
48
+ "retries": 0,
49
+ "failureRate": 0,
50
+ "apiFailureRate": 0,
51
+ "network": {
52
+ "retries": 0,
53
+ "transportFailures": 0,
54
+ "retryRate": 0,
55
+ "healthy": true,
56
+ "gaveUp": false,
57
+ "gaveUpAfter": null,
58
+ "outages": 0
59
+ },
60
+ "durationMs": 818132,
61
+ "methods": [
62
+ {
63
+ "method": "reportLocation",
64
+ "calls": 892,
65
+ "failures": 0,
66
+ "apiFailures": 0,
67
+ "transportFailures": 0,
68
+ "retries": 0,
69
+ "failureRate": 0,
70
+ "latencyMs": {
71
+ "p50": 711,
72
+ "p95": 1003,
73
+ "p99": 2371,
74
+ "max": 3178
75
+ },
76
+ "errors": []
77
+ },
78
+ {
79
+ "method": "recentPostsByOthers",
80
+ "calls": 298,
81
+ "failures": 0,
82
+ "apiFailures": 0,
83
+ "transportFailures": 0,
84
+ "retries": 0,
85
+ "failureRate": 0,
86
+ "latencyMs": {
87
+ "p50": 241,
88
+ "p95": 349,
89
+ "p99": 416,
90
+ "max": 622
91
+ },
92
+ "errors": []
93
+ },
94
+ {
95
+ "method": "like",
96
+ "calls": 296,
97
+ "failures": 0,
98
+ "apiFailures": 0,
99
+ "transportFailures": 0,
100
+ "retries": 0,
101
+ "failureRate": 0,
102
+ "latencyMs": {
103
+ "p50": 238,
104
+ "p95": 319,
105
+ "p99": 385,
106
+ "max": 760
107
+ },
108
+ "errors": []
109
+ },
110
+ {
111
+ "method": "post",
112
+ "calls": 139,
113
+ "failures": 0,
114
+ "apiFailures": 0,
115
+ "transportFailures": 0,
116
+ "retries": 0,
117
+ "failureRate": 0,
118
+ "latencyMs": {
119
+ "p50": 240,
120
+ "p95": 313,
121
+ "p99": 383,
122
+ "max": 614
123
+ },
124
+ "errors": []
125
+ },
126
+ {
127
+ "method": "comment",
128
+ "calls": 123,
129
+ "failures": 0,
130
+ "apiFailures": 0,
131
+ "transportFailures": 0,
132
+ "retries": 0,
133
+ "failureRate": 0,
134
+ "latencyMs": {
135
+ "p50": 243,
136
+ "p95": 326,
137
+ "p99": 387,
138
+ "max": 683
139
+ },
140
+ "errors": []
141
+ },
142
+ {
143
+ "method": "openConversation",
144
+ "calls": 110,
145
+ "failures": 0,
146
+ "apiFailures": 0,
147
+ "transportFailures": 0,
148
+ "retries": 0,
149
+ "failureRate": 0,
150
+ "latencyMs": {
151
+ "p50": 245,
152
+ "p95": 336,
153
+ "p99": 375,
154
+ "max": 626
155
+ },
156
+ "errors": []
157
+ },
158
+ {
159
+ "method": "sendMessage",
160
+ "calls": 110,
161
+ "failures": 0,
162
+ "apiFailures": 0,
163
+ "transportFailures": 0,
164
+ "retries": 0,
165
+ "failureRate": 0,
166
+ "latencyMs": {
167
+ "p50": 244,
168
+ "p95": 351,
169
+ "p99": 393,
170
+ "max": 780
171
+ },
172
+ "errors": []
173
+ },
174
+ {
175
+ "method": "listGroups",
176
+ "calls": 16,
177
+ "failures": 0,
178
+ "apiFailures": 0,
179
+ "transportFailures": 0,
180
+ "retries": 0,
181
+ "failureRate": 0,
182
+ "latencyMs": {
183
+ "p50": 250,
184
+ "p95": 312,
185
+ "p99": 312,
186
+ "max": 312
187
+ },
188
+ "errors": []
189
+ },
190
+ {
191
+ "method": "joinGroup",
192
+ "calls": 16,
193
+ "failures": 0,
194
+ "apiFailures": 0,
195
+ "transportFailures": 0,
196
+ "retries": 0,
197
+ "failureRate": 0,
198
+ "latencyMs": {
199
+ "p50": 235,
200
+ "p95": 311,
201
+ "p99": 311,
202
+ "max": 311
203
+ },
204
+ "errors": []
205
+ },
206
+ {
207
+ "method": "createUser",
208
+ "calls": 10,
209
+ "failures": 0,
210
+ "apiFailures": 0,
211
+ "transportFailures": 0,
212
+ "retries": 0,
213
+ "failureRate": 0,
214
+ "latencyMs": {
215
+ "p50": 601,
216
+ "p95": 834,
217
+ "p99": 834,
218
+ "max": 834
219
+ },
220
+ "errors": []
221
+ },
222
+ {
223
+ "method": "setProfile",
224
+ "calls": 10,
225
+ "failures": 0,
226
+ "apiFailures": 0,
227
+ "transportFailures": 0,
228
+ "retries": 0,
229
+ "failureRate": 0,
230
+ "latencyMs": {
231
+ "p50": 249,
232
+ "p95": 392,
233
+ "p99": 392,
234
+ "max": 392
235
+ },
236
+ "errors": []
237
+ },
238
+ {
239
+ "method": "deleteUser",
240
+ "calls": 10,
241
+ "failures": 0,
242
+ "apiFailures": 0,
243
+ "transportFailures": 0,
244
+ "retries": 0,
245
+ "failureRate": 0,
246
+ "latencyMs": {
247
+ "p50": 320,
248
+ "p95": 508,
249
+ "p99": 508,
250
+ "max": 508
251
+ },
252
+ "errors": []
253
+ }
254
+ ]
255
+ },
256
+ "coverage": {
257
+ "label": "13/13",
258
+ "implemented": [
259
+ "createUser",
260
+ "setProfile",
261
+ "refreshSession",
262
+ "reportLocation",
263
+ "post",
264
+ "recentPostsByOthers",
265
+ "like",
266
+ "comment",
267
+ "openConversation",
268
+ "sendMessage",
269
+ "listGroups",
270
+ "joinGroup",
271
+ "deleteUser"
272
+ ],
273
+ "notTested": []
274
+ },
275
+ "cleanup": {
276
+ "removed": 10,
277
+ "notDeleted": [],
278
+ "failed": []
279
+ }
280
+ }
@@ -0,0 +1,51 @@
1
+ // Populace's first customer: Buzz Buzz, a gig-driver tracking app.
2
+ //
3
+ // Run it — PowerShell (Windows):
4
+ // $env:BUZZBUZZ_TEST_URL="https://<your-TEST-project>.supabase.co"
5
+ // $env:BUZZBUZZ_TEST_KEY="<test project publishable key>"
6
+ // node src/cli.mjs run --config examples/buzzbuzz/populace.config.mjs
7
+ //
8
+ // Run it — bash/zsh (macOS, Linux):
9
+ // BUZZBUZZ_TEST_URL=... BUZZBUZZ_TEST_KEY=... \
10
+ // node src/cli.mjs run --config examples/buzzbuzz/populace.config.mjs
11
+ //
12
+ // The test project needs the same schema as production. Run these against it:
13
+ // schema.sql · social_features.sql · groups.sql · direct_messages.sql
14
+ // realtime.sql · fix_chat_rls.sql · user_content_control.sql
15
+ // work_apps_global.sql · privacy_lockdown.sql
16
+ // …and turn OFF Authentication → Providers → Email → "Confirm email",
17
+ // otherwise nobody can sign in after signing up.
18
+
19
+ export default {
20
+ app: "Buzz Buzz",
21
+ adapter: "../../adapters/buzzbuzz.mjs",
22
+ environment: "test",
23
+
24
+ target: {
25
+ url: process.env.BUZZBUZZ_TEST_URL,
26
+ key: process.env.BUZZBUZZ_TEST_KEY,
27
+ },
28
+
29
+ // Buzz Buzz's LIVE project. If BUZZBUZZ_TEST_URL is ever set to this by
30
+ // accident, Populace refuses to start. Real drivers must never see invented
31
+ // people on their map.
32
+ // Both of these are real. rqzuuvlougzhynckvqzd is the original project, which
33
+ // is still running; ypdaetbeexyepswyhbui became production on 2026-08-09.
34
+ // The clean run recorded against the latter happened while it was empty and
35
+ // had no real drivers on it — that window has closed, and it must not be
36
+ // simulated against again.
37
+ neverRunAgainst: [
38
+ "https://rqzuuvlougzhynckvqzd.supabase.co",
39
+ "https://ypdaetbeexyepswyhbui.supabase.co",
40
+ ],
41
+
42
+ population: {
43
+ agents: 8,
44
+ cities: ["manila", "mumbai"],
45
+ minutes: 10,
46
+ tickSeconds: 5,
47
+ },
48
+
49
+ identity: { phonePrefix: "0900" },
50
+ report: { path: "populace-report.json" },
51
+ };
@@ -0,0 +1,61 @@
1
+ # Run the Buzz Buzz simulation from Windows PowerShell.
2
+ #
3
+ # Exists because the bash one-liner does not work here: PowerShell 5.1 has no
4
+ # `&&` statement separator and no `VAR=value command` prefix, so pasting the
5
+ # Unix form fails with "The token '&&' is not a valid statement separator".
6
+ #
7
+ # .\run-test.ps1 # 6 drivers, 5 minutes
8
+ # .\run-test.ps1 -Agents 4 -Minutes 3
9
+ # .\run-test.ps1 -Clean # just remove any leftover accounts
10
+ #
11
+ # Run it from anywhere; it locates itself.
12
+
13
+ param(
14
+ [int]$Agents = 6,
15
+ [int]$Minutes = 5,
16
+ [switch]$Clean
17
+ )
18
+
19
+ $ErrorActionPreference = "Stop"
20
+ Set-Location $PSScriptRoot
21
+
22
+ # Deliberately no NODE_OPTIONS here. An earlier version set
23
+ # --dns-result-order=ipv6first to work around connection failures on a NAT64
24
+ # link. Measuring it afterwards showed 40/40 successful connections both with
25
+ # and without the flag — the failures were the link recovering on its own, not
26
+ # address-family ordering. The workaround was cargo cult, so it is gone.
27
+
28
+ # The isolated test project. Never a production host — populace.config.mjs
29
+ # lists the live ones in neverRunAgainst and refuses them before loading.
30
+ $env:BUZZBUZZ_TEST_URL = "https://jqepegeifmnfofeyebrz.supabase.co"
31
+ $env:BUZZBUZZ_TEST_KEY = "sb_publishable_ZkOhFsiQBrKMB5pc2Ub1Vw_V5oxsBE1"
32
+
33
+ $cli = Join-Path $PSScriptRoot "..\..\src\cli.mjs"
34
+
35
+ if ($Clean) {
36
+ node $cli clean
37
+ exit $LASTEXITCODE
38
+ }
39
+
40
+ Write-Host ""
41
+ Write-Host " Checking the target before running..." -ForegroundColor Cyan
42
+ node $cli doctor
43
+ if ($LASTEXITCODE -ne 0) {
44
+ Write-Host ""
45
+ Write-Host " doctor says not ready — stopping here rather than burning a run." -ForegroundColor Yellow
46
+ exit 1
47
+ }
48
+
49
+ node $cli run --agents $Agents --minutes $Minutes
50
+ $code = $LASTEXITCODE
51
+
52
+ # A run exits non-zero when it FINDS something. That is the tool working, not
53
+ # the tool failing, so say so rather than leaving a bare red exit code.
54
+ Write-Host ""
55
+ if ($code -eq 0) {
56
+ Write-Host " Run finished with no failures." -ForegroundColor Green
57
+ } else {
58
+ Write-Host " Run finished and found problems — see the report above." -ForegroundColor Yellow
59
+ Write-Host " That is the expected outcome when there is something to find." -ForegroundColor DarkGray
60
+ }
61
+ exit $code
@@ -0,0 +1,90 @@
1
+ // A pretend app, so Populace can be evaluated in ten seconds with no backend.
2
+ //
3
+ // It stores everything in memory and fakes plausible network latency. One
4
+ // endpoint is deliberately slow and one deliberately flaky, so the report has
5
+ // something real to say — which is the point of the demo. A demo where
6
+ // everything passes teaches you nothing about what the tool is for.
7
+
8
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
+ const latency = (base, spread) => sleep(base + Math.random() * spread);
10
+
11
+ export function createAdapter() {
12
+ const db = { users: new Map(), posts: [], likes: 0, comments: 0, messages: 0, groups: [{ id: "riders" }, { id: "couriers" }] };
13
+ let likeCalls = 0;
14
+
15
+ return {
16
+ name: "demo",
17
+
18
+ async healthCheck() {
19
+ await latency(20, 30);
20
+ },
21
+
22
+ async createUser({ name, phone }) {
23
+ await latency(80, 120);
24
+ const id = `u_${phone}`;
25
+ db.users.set(id, { id, name });
26
+ return { id, name };
27
+ },
28
+
29
+ async setProfile() {
30
+ await latency(30, 40);
31
+ },
32
+
33
+ async reportLocation() {
34
+ await latency(15, 25);
35
+ },
36
+
37
+ async post(user, text) {
38
+ await latency(40, 60);
39
+ const post = { id: `p${db.posts.length}`, userId: user.id, text };
40
+ db.posts.push(post);
41
+ return post.id;
42
+ },
43
+
44
+ // Deliberately the slow one: an unindexed feed query is the single most
45
+ // common thing a simulation like this uncovers.
46
+ async recentPostsByOthers(user, limit = 10) {
47
+ await latency(180, 320);
48
+ return db.posts.filter((p) => p.userId !== user.id).slice(-limit);
49
+ },
50
+
51
+ // Deliberately the broken one: a permission rule that rejects some writes.
52
+ async like() {
53
+ await latency(25, 35);
54
+ likeCalls += 1;
55
+ if (likeCalls % 4 === 0) {
56
+ throw new Error(`new row violates row-level security policy "post_likes_insert"`);
57
+ }
58
+ db.likes += 1;
59
+ },
60
+
61
+ async comment() {
62
+ await latency(35, 45);
63
+ db.comments += 1;
64
+ },
65
+
66
+ async openConversation() {
67
+ await latency(50, 70);
68
+ return "thread_1";
69
+ },
70
+
71
+ async sendMessage() {
72
+ await latency(30, 40);
73
+ db.messages += 1;
74
+ },
75
+
76
+ async listGroups() {
77
+ await latency(25, 30);
78
+ return db.groups;
79
+ },
80
+
81
+ async joinGroup() {
82
+ await latency(40, 50);
83
+ },
84
+
85
+ async deleteUser(user) {
86
+ await latency(60, 80);
87
+ db.users.delete(user.id);
88
+ },
89
+ };
90
+ }