@bongos/core 1.19.347 → 1.19.348

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/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.347",
6
- "core_contract": "1.19.347",
7
- "source_commit": "e27bff09b16adf42c58b54ead68924751dbc2351",
5
+ "core_version": "1.19.348",
6
+ "core_contract": "1.19.348",
7
+ "source_commit": "d42db56154d01be276f13e04e364e7ced8d8c400",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-25T02:40:24.281Z",
9
+ "built_at": "2026-08-25T02:48:07.802Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 328,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1497,
14
+ "functional_verbatim": 1499,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1847,
20
- "tree_sha256": "2e117a040ff4372ef27c7c654e2e827f99f84acd347da0b416682cc5a66a1eb2",
19
+ "file_count": 1849,
20
+ "tree_sha256": "3004531bfb70cd33dbca752e2d945c4687d6ea1148f54548b6fcf921f953681f",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1472,12 +1472,12 @@
1472
1472
  {
1473
1473
  "path": "docs/copy-inventory.md",
1474
1474
  "mode": "0000644",
1475
- "sha256": "360d2e6fd65c436cc402f94fede50e2ab59ff275c1bd894c99f599576a804984"
1475
+ "sha256": "0caaef6980475bc4abbb51df79ebe4b14b2a3ad7b97f3e2da13f08d8e1ae1b11"
1476
1476
  },
1477
1477
  {
1478
1478
  "path": "docs/copy-registry.json",
1479
1479
  "mode": "0000644",
1480
- "sha256": "9c6c9b7271759fff6f4a76857f7d98ebef55785c474158276d28d7f6db9092d9"
1480
+ "sha256": "d5410ce76bb98ee335363f00e483fd66539b9ac35ffb4ec8a7bd3e0af928faf0"
1481
1481
  },
1482
1482
  {
1483
1483
  "path": "docs/design-contract.md",
@@ -3577,7 +3577,7 @@
3577
3577
  {
3578
3578
  "path": "modules/hall-ui/public/idea.js",
3579
3579
  "mode": "0000644",
3580
- "sha256": "abda218838cb126485f717c9c1c945cdb8bff5140323ebe8c8f5aa29bbcc6150"
3580
+ "sha256": "b742657979f37993db85efbf05d0983e8ef295dcde7b0058a826909bfa352074"
3581
3581
  },
3582
3582
  {
3583
3583
  "path": "modules/hall-ui/public/ideas.css",
@@ -3592,7 +3592,7 @@
3592
3592
  {
3593
3593
  "path": "modules/hall-ui/public/ideas.js",
3594
3594
  "mode": "0000644",
3595
- "sha256": "6ec39a94e314f37ebf610b6f42899b68a994974024e2cd0578e5d15d812b040f"
3595
+ "sha256": "96796c1d7192ed2f6c4ac2ef185ade5e6001ecb828feb1b99a123a457bfdc8bf"
3596
3596
  },
3597
3597
  {
3598
3598
  "path": "modules/hall-ui/public/index.html",
@@ -4762,12 +4762,12 @@
4762
4762
  {
4763
4763
  "path": "package-lock.json",
4764
4764
  "mode": "0000644",
4765
- "sha256": "ab72d709096f258ac4fb7df05ba0e32127d2e470b9bba8d724894ea883e14094"
4765
+ "sha256": "6745e4e181b20731968d02f8e4dac7cb864acbf3fbbd6ec6054c799a41487618"
4766
4766
  },
4767
4767
  {
4768
4768
  "path": "package.json",
4769
4769
  "mode": "0000644",
4770
- "sha256": "3f40232829ee5093d6b7a64808013abb68ac04fe25b38d3fa174fc5fecff11a2"
4770
+ "sha256": "65ce6bcba41342833cd48800e028fd1e3420a4d9d7fb82c1250cfca724baa0ad"
4771
4771
  },
4772
4772
  {
4773
4773
  "path": "public-docs/index.html",
@@ -5279,6 +5279,11 @@
5279
5279
  "mode": "0000644",
5280
5280
  "sha256": "5aa658b099b2acda06cdb3aecfe905da7de79f2b12c844faca091ff061454017"
5281
5281
  },
5282
+ {
5283
+ "path": "scripts/gds/client-baseurl-guard.js",
5284
+ "mode": "0000644",
5285
+ "sha256": "8ac4c8909b75aa5e2022a967ba9ac0cbcdac60da9d75757603827b86964c825b"
5286
+ },
5282
5287
  {
5283
5288
  "path": "scripts/gds/code.js",
5284
5289
  "mode": "0000644",
@@ -5342,7 +5347,7 @@
5342
5347
  {
5343
5348
  "path": "scripts/gds/deploy-divergence.js",
5344
5349
  "mode": "0000644",
5345
- "sha256": "ba2184ba713fb1accdc86a14fbd3c8802b45808e5269800644a6ef166d8b249b"
5350
+ "sha256": "f9ade0941ad0e49cb6412936ad5d657561da0ab591508a2844a3645e25debdcb"
5346
5351
  },
5347
5352
  {
5348
5353
  "path": "scripts/gds/design-sync.js",
@@ -5447,7 +5452,7 @@
5447
5452
  {
5448
5453
  "path": "scripts/gds/fitness.js",
5449
5454
  "mode": "0000644",
5450
- "sha256": "b07c9489b073233d5e3a83bb59a7ebda0fc1a2bd0defcd3d5a69efc77ff24b86"
5455
+ "sha256": "91b125de588406e6b93f8cc29d4d53891932a52744a6e92824459c734f747f16"
5451
5456
  },
5452
5457
  {
5453
5458
  "path": "scripts/gds/gate-review.js",
@@ -6217,7 +6222,7 @@
6217
6222
  {
6218
6223
  "path": "scripts/gds/update-channel.js",
6219
6224
  "mode": "0000644",
6220
- "sha256": "065095ccdcb104ede0312375484f62b23742fa1bc446f9749505c22b71babb5d"
6225
+ "sha256": "db54fba6065c8b662f574aa9443e686df4e5a152865342999b1f20d50ece01ed"
6221
6226
  },
6222
6227
  {
6223
6228
  "path": "scripts/gds/upgrade.js",
@@ -6617,7 +6622,7 @@
6617
6622
  {
6618
6623
  "path": "src/module-api.js",
6619
6624
  "mode": "0000644",
6620
- "sha256": "0caf921877b91ef138e5ad71215e7a2b05a5ad837dc1fa6cb8d8d2b4ccbb63f2"
6625
+ "sha256": "7eca632fc6880711d60fad718a743301e62e99ca990a8a4cf07767726f86f0cf"
6621
6626
  },
6622
6627
  {
6623
6628
  "path": "src/module-loader/catalog.js",
@@ -7124,6 +7129,11 @@
7124
7129
  "mode": "0000644",
7125
7130
  "sha256": "fae365f4c0cbc41a9077cfa19387a2f016f39d14fad37cf2867230eaf9196b11"
7126
7131
  },
7132
+ {
7133
+ "path": "tests/client_baseurl_guard.mjs",
7134
+ "mode": "0000644",
7135
+ "sha256": "541407601a480408b725768a1a1b0ba8b71e0969f82d6f4747f862abcb62a5d0"
7136
+ },
7127
7137
  {
7128
7138
  "path": "tests/client_bundle_route.mjs",
7129
7139
  "mode": "0000644",
@@ -7972,7 +7982,7 @@
7972
7982
  {
7973
7983
  "path": "tests/idea_hall_composer.mjs",
7974
7984
  "mode": "0000644",
7975
- "sha256": "1e6d0784ecb37e613561cc8ee45fa23e178ecf112d5a8f218b9795dca0e7d0fc"
7985
+ "sha256": "dbe8f91e4f3acc566a36547ce42d30328ba9e651a622fe1f25092d49d4e3a955"
7976
7986
  },
7977
7987
  {
7978
7988
  "path": "tests/idea_ideate_full.mjs",
@@ -9187,7 +9197,7 @@
9187
9197
  {
9188
9198
  "path": "tests/update_channel.mjs",
9189
9199
  "mode": "0000644",
9190
- "sha256": "3b0912dcda6888b4daa2cd13bbef0cddddafc425c39a509dc4955ce77ddd720e"
9200
+ "sha256": "9debf9ecdb02b986c882f3d29bdf4b5fe2154cb4e7639db0fd0a0336cd4bdc3b"
9191
9201
  },
9192
9202
  {
9193
9203
  "path": "tests/upgrade.mjs",
@@ -125,7 +125,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
125
125
  | `634738746b81` | certain | A fresh box has a | `modules/hall-ui/public/harbor.js:122` |
126
126
  | `df5eb70b5a05` | certain | A higher rank. Its details are hidden until you reach the rank directly below it. | `modules/hall-ui/public/ranks.js:125` |
127
127
  | `a07c7b335a66` | certain | A protected-scope goal requires a Metic+ member. | `modules/hall-ui/public/goals-render.js:903` |
128
- | `5723baf26b2d` | certain | A quick idea is a thought filed fast. It is not an unfinished Full Idea — only its author can offer it up for | `modules/hall-ui/public/idea.js:237` |
128
+ | `5723baf26b2d` | certain | A quick idea is a thought filed fast. It is not an unfinished Full Idea — only its author can offer it up for | `modules/hall-ui/public/idea.js:245` |
129
129
  | `6359b5afbc6b` | certain | A short path to finding your feet. Tick the steps you’ve done; the rest are tracked for you. | `modules/hall-ui/public/hall-widgets.js:60` |
130
130
  | `58f7f6f0d8bd` | certain | A small app for your menu bar (Mac) or system tray (Windows): one switch to start and stop your dev box, with | `modules/hall-ui/public/settings.html:170` |
131
131
  | `a3185a43c230` | certain | A sound plays at each turn of your work — a gentle chime when Claude finishes, a flourish when a task ships or | `modules/hall-ui/public/settings.html:265` |
@@ -147,7 +147,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
147
147
  | `200a83529fa6` | certain | Already acknowledged — you're all set. | `modules/hall-ui/public/primer.html:68` |
148
148
  | `d11477f287b7` | certain | An objection returns the idea to its author with your reason and the section it faults \u2014 the feedback the | `modules/hall-ui/public/government.js:741` |
149
149
  | `69f0ca4a4d84` | certain | Analytics scope | `modules/hall-ui/public/hall-render.js:995` |
150
- | `14bdceaac078` | certain | Answer the five questions and this spark becomes a Full Idea. | `modules/hall-ui/public/ideas.js:415` |
150
+ | `14bdceaac078` | certain | Answer the five questions and this spark becomes a Full Idea. | `modules/hall-ui/public/ideas.js:423` |
151
151
  | `52ed0e2e3470` | certain | Any rank | `modules/hall-ui/public/task-lib.js:62` |
152
152
  | `c89833aa985a` | certain | Approval failed — try again. | `modules/hall-ui/public/pair.js:92` |
153
153
  | `0832ba017c90` | certain | Approve & re-run | `modules/hall-ui/public/gate.js:100` |
@@ -165,7 +165,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
165
165
  | `4195d0d114e4` | certain | Archons only. | `modules/hall-ui/public/sessions.js:97` |
166
166
  | `4195d0d114e4` | certain | Archons only. | `modules/hall-ui/public/watch.js:838` |
167
167
  | `5fcfdf8975cf` | certain | As configuration: | `modules/hall-ui/public/government.js:633` |
168
- | `01a0f29ff406` | certain | As first filed | `modules/hall-ui/public/idea.js:229` |
168
+ | `01a0f29ff406` | certain | As first filed | `modules/hall-ui/public/idea.js:237` |
169
169
  | `688cc6dd38ee` | certain | Asked of you | `modules/hall-ui/public/collab.html:32` |
170
170
  | `b2656d0b2402` | certain | Asked {…} | `modules/hall-ui/public/collab.js:36` |
171
171
  | `b2656d0b2402` | certain | Asked {…} | `modules/hall-ui/public/watch.js:205` |
@@ -280,7 +280,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
280
280
  | `dec4b11f316d` | certain | Could not load dev boxes just now. | `modules/hall-ui/public/harbor.js:288` |
281
281
  | `0f7c244312b1` | certain | Could not load government data just now. | `modules/hall-ui/public/government.js:883` |
282
282
  | `cd00bf198661` | certain | Could not load held pull requests just now. | `modules/hall-ui/public/gate.js:180` |
283
- | `589a4cfc5442` | certain | Could not load idea {…}. | `modules/hall-ui/public/ideas.js:444` |
283
+ | `589a4cfc5442` | certain | Could not load idea {…}. | `modules/hall-ui/public/ideas.js:456` |
284
284
  | `192c893fc64e` | certain | Could not load modules. | `modules/hall-ui/public/modules.js:226` |
285
285
  | `4e09b612c6b7` | certain | Could not load modules. Return to the hall and try again. | `modules/hall-ui/public/modules.js:225` |
286
286
  | `8a4b590fa1a0` | certain | Could not load requests. | `modules/hall-ui/public/collab.js:137` |
@@ -289,11 +289,11 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
289
289
  | `5420bb0b8475` | certain | Could not load the Board Room just now. | `modules/hall-ui/public/government.js:804` |
290
290
  | `3f82d4f29e5b` | certain | Could not load the constitution just now. | `modules/hall-ui/public/government.js:628` |
291
291
  | `8f597eabf4ac` | certain | Could not load the drachmae guide. | `modules/hall-ui/public/drachmae.js:162` |
292
- | `b2147cc90e36` | certain | Could not load the idea template. Reload, or file from the terminal with capture.js. | `modules/hall-ui/public/ideas.js:422` |
292
+ | `b2147cc90e36` | certain | Could not load the idea template. Reload, or file from the terminal with capture.js. | `modules/hall-ui/public/ideas.js:438` |
293
293
  | `6264ebc9ede0` | certain | Could not load the primer. | `modules/hall-ui/public/primer.js:620` |
294
294
  | `7604af13c40e` | certain | Could not load the profile. | `modules/hall-ui/public/profile.js:330` |
295
- | `8b69e445592a` | certain | Could not load the question labels; showing the answers as stored. | `modules/hall-ui/public/idea.js:221` |
296
- | `edd5bafbe075` | certain | Could not load the queue just now. | `modules/hall-ui/public/ideas.js:379` |
295
+ | `8b69e445592a` | certain | Could not load the question labels; showing the answers as stored. | `modules/hall-ui/public/idea.js:229` |
296
+ | `edd5bafbe075` | certain | Could not load the queue just now. | `modules/hall-ui/public/ideas.js:387` |
297
297
  | `02bb0570996a` | certain | Could not load the ranks. | `modules/hall-ui/public/ranks.js:197` |
298
298
  | `2878778cf9f3` | certain | Could not load this page. | `modules/hall-ui/public/watch.js:884` |
299
299
  | `4b0c8d94541c` | certain | Could not load your account. Reload the page to try again. | `modules/hall-ui/public/settings.js:1270` |
@@ -353,7 +353,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
353
353
  | `cc681021a140` | certain | Description (optional) | `modules/hall-ui/public/government.html:61` |
354
354
  | `8cac6be8afdf` | certain | Dev box app | `modules/hall-ui/public/settings.html:169` |
355
355
  | `9c37356c3a04` | certain | Dev-box costs | `modules/hall-ui/public/watch.html:86` |
356
- | `0a4fe7150971` | certain | Develop this idea | `modules/hall-ui/public/ideas.js:414` |
356
+ | `0a4fe7150971` | certain | Develop this idea | `modules/hall-ui/public/ideas.js:422` |
357
357
  | `11b8266d6aab` | certain | Developing a Big Idea Spark | `modules/hall-ui/public/ideas.html:56` |
358
358
  | `457f4967541b` | certain | Device pairing | `modules/hall-ui/public/pair.html:15` |
359
359
  | `457f4967541b` | certain | Device pairing | `modules/hall-ui/public/pair.html:15` |
@@ -374,7 +374,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
374
374
  | `9c728558d6e7` | certain | Drachmae & karma | `modules/hall-ui/public/drachmae.html:15` |
375
375
  | `9d9cc4ef8557` | certain | Drachmae & karma — {{worldName}} | `modules/hall-ui/public/drachmae.html:6` |
376
376
  | `9d9cc4ef8557` | certain | Drachmae & karma — {{worldName}} | `modules/hall-ui/public/drachmae.html:6` |
377
- | `2f8d0404039e` | certain | Draft discarded. | `modules/hall-ui/public/ideas.js:339` |
377
+ | `2f8d0404039e` | certain | Draft discarded. | `modules/hall-ui/public/ideas.js:347` |
378
378
  | `d59beed97d58` | certain | Drag to pan, scroll to zoom | `modules/hall-ui/public/diagrams.html:42` |
379
379
  | `67f09099c2d3` | certain | Duplicate — should be said once, in one place | `modules/hall-ui/public/copy-desk.html:95` |
380
380
  | `db7f370c0d38` | certain | Each builder’s dev box at a glance. | `modules/hall-ui/public/harbor.js:335` |
@@ -404,7 +404,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
404
404
  | `86ef86c5c647` | certain | Fit to view | `modules/hall-ui/public/diagram-viewer.js:97` |
405
405
  | `86ef86c5c647` | certain | Fit to view | `modules/hall-ui/public/diagram-viewer.js:100` |
406
406
  | `78107a76a755` | certain | Five diagrams that show, at a glance, how work flows through {{worldName}}'s development system. | `modules/hall-ui/public/diagrams.html:42` |
407
- | `18ab1be9d3ad` | certain | Five questions, and a score that counts how much of each you actually answered. | `modules/hall-ui/public/ideas.js:432` |
407
+ | `18ab1be9d3ad` | certain | Five questions, and a score that counts how much of each you actually answered. | `modules/hall-ui/public/ideas.js:448` |
408
408
  | `b9267033b1f1` | certain | Flag this copy | `modules/hall-ui/public/copy-desk.html:87` |
409
409
  | `448d00771dc8` | certain | Flagged. It is in the queue. | `modules/hall-ui/public/copy-desk.js:281` |
410
410
  | `617f13fa3fdb` | certain | Flip the switch. | `modules/hall-ui/public/hall-render.js:609` |
@@ -457,7 +457,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
457
457
  | `298ff22576ee` | certain | How your output has changed over time — work shipped, cost and time per work, and how reviews held up. | `modules/hall-ui/public/hall-widgets.js:182` |
458
458
  | `08d3bf741a2c` | certain | I have read it | `modules/hall-ui/public/primer.html:67` |
459
459
  | `08d3bf741a2c` | certain | I have read it | `modules/hall-ui/public/primer.js:496` |
460
- | `a4ff1e9c34e3` | certain | Idea {…} | `modules/hall-ui/public/idea.js:244` |
460
+ | `a4ff1e9c34e3` | certain | Idea {…} | `modules/hall-ui/public/idea.js:252` |
461
461
  | `ba82ec56f1e3` | certain | Idea — {{worldName}} | `modules/hall-ui/public/idea.html:6` |
462
462
  | `ba82ec56f1e3` | certain | Idea — {{worldName}} | `modules/hall-ui/public/idea.html:6` |
463
463
  | `e181637c5a5e` | certain | Ideas awaiting triage | `modules/hall-ui/public/builders.js:843` |
@@ -473,7 +473,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
473
473
  | `d176de7857f3` | certain | Invited. | `modules/hall-ui/public/goals-render.js:895` |
474
474
  | `2170b74c6526` | certain | Isometric map of the repository | `modules/hall-ui/public/atlas.html:39` |
475
475
  | `79791be35018` | certain | It opens this hall in your browser — click one | `modules/hall-ui/public/hall-render.js:609` |
476
- | `b0bcb3f844ec` | certain | Its author marked this as needing development — anyone can turn it into a Full Idea, and they keep the credit | `modules/hall-ui/public/idea.js:235` |
476
+ | `b0bcb3f844ec` | certain | Its author marked this as needing development — anyone can turn it into a Full Idea, and they keep the credit | `modules/hall-ui/public/idea.js:243` |
477
477
  | `34dc2af8664f` | certain | Join a project | `modules/hall-ui/public/my-projects.html:44` |
478
478
  | `b7719e855009` | certain | Join goal | `modules/hall-ui/public/goals-render.js:758` |
479
479
  | `9f9424a5100b` | certain | Join our Discord | `modules/hall-ui/public/hall-render.js:607` |
@@ -565,7 +565,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
565
565
  | `6d6fd2314248` | certain | No cross-project activity reported yet. | `modules/hall-ui/public/profile.js:233` |
566
566
  | `6d6fd2314248` | certain | No cross-project activity reported yet. | `modules/hall-ui/public/scouting.js:29` |
567
567
  | `b1ada579483c` | certain | No custom ranks yet | `modules/hall-ui/public/government.js:303` |
568
- | `6e2d2e11f84a` | certain | No description. | `modules/hall-ui/public/idea.js:106` |
568
+ | `6e2d2e11f84a` | certain | No description. | `modules/hall-ui/public/idea.js:114` |
569
569
  | `cb1a2f7e92bf` | certain | No dev boxes yet — no builder has opened one. | `modules/hall-ui/public/harbor.js:304` |
570
570
  | `36fc20a2a55e` | certain | No duplicated strings in the registry. | `modules/hall-ui/public/copy-desk.js:181` |
571
571
  | `93c00cb1423a` | certain | No goal | `modules/hall-ui/public/work.js:421` |
@@ -591,7 +591,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
591
591
  | `c72e0bdb834a` | certain | Not declared in module.json — this is who ships the module. | `modules/hall-ui/public/modules.js:49` |
592
592
  | `361b353ded5e` | certain | Not enough shipped work yet to chart. | `modules/hall-ui/public/hall-widgets.js:184` |
593
593
  | `90230572d017` | certain | Not found | `modules/hall-ui/public/task.js:304` |
594
- | `61250b49a90b` | certain | Not scored. | `modules/hall-ui/public/idea.js:70` |
594
+ | `61250b49a90b` | certain | Not scored. | `modules/hall-ui/public/idea.js:78` |
595
595
  | `8d6afe6def47` | certain | Not signed in | `modules/hall-ui/public/settings.html:33` |
596
596
  | `19adac0efed1` | certain | Not yet available | `modules/hall-ui/public/not-ready.html:24` |
597
597
  | `2c19f41704b9` | certain | Not yet available — {{worldName}} | `modules/hall-ui/public/not-ready.html:6` |
@@ -602,7 +602,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
602
602
  | `fec5d78497e8` | certain | Nothing in the backlog right now. | `modules/hall-ui/public/work.js:953` |
603
603
  | `2c6e0dfbd781` | certain | Nothing is waiting on you. Requests reach you by your craft — set yours in | `modules/hall-ui/public/collab.html:38` |
604
604
  | `18ce0723be0d` | certain | Nothing is waiting — no held changes need your approval. | `modules/hall-ui/public/gate.js:188` |
605
- | `2ec0fbb20a42` | certain | Nothing is waiting. A spark appears here when its author offers a quick idea up for development. | `modules/hall-ui/public/ideas.js:357` |
605
+ | `2ec0fbb20a42` | certain | Nothing is waiting. A spark appears here when its author offers a quick idea up for development. | `modules/hall-ui/public/ideas.js:365` |
606
606
  | `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/profile.js:144` |
607
607
  | `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/work.js:914` |
608
608
  | `42909e1460c4` | certain | Nothing to submit — every module here already ships with the core. | `modules/hall-ui/public/modules.js:166` |
@@ -769,7 +769,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
769
769
  | `6374d6172678` | certain | Shipped {…}{…}. | `modules/hall-ui/public/task.js:234` |
770
770
  | `14d32a35c15f` | certain | Show my cross-project record | `modules/hall-ui/public/settings.js:579` |
771
771
  | `dc0f9fe50a65` | certain | Show the setup guide again | `modules/hall-ui/public/index.html:55` |
772
- | `2af4c0f2b4b9` | certain | Showing {…} of {…}. | `modules/hall-ui/public/ideas.js:361` |
772
+ | `2af4c0f2b4b9` | certain | Showing {…} of {…}. | `modules/hall-ui/public/ideas.js:369` |
773
773
  | `2f12d46f96ca` | certain | Sign in from the app. | `modules/hall-ui/public/hall-render.js:609` |
774
774
  | `19ab56cc3932` | certain | Sign in to filter to your goals | `modules/hall-ui/public/goals-page.js:793` |
775
775
  | `b057dfbf49c5` | certain | Sign in when Claude prompts you. | `modules/hall-ui/public/hall-render.js:609` |
@@ -926,7 +926,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
926
926
  | `c5fd76a9e03d` | certain | Won {…} | `modules/hall-ui/public/profile.js:115` |
927
927
  | `520d139df9eb` | certain | Work is complete but the version is held — no tasks are being claimed here right now. | `modules/hall-ui/public/roadmap.js:223` |
928
928
  | `52e5fe3c6075` | certain | Write a Full Idea | `modules/hall-ui/public/ideas.html:43` |
929
- | `7664f4373d1f` | certain | Write one | `modules/hall-ui/public/idea.js:235` |
929
+ | `7664f4373d1f` | certain | Write one | `modules/hall-ui/public/idea.js:243` |
930
930
  | `c368b56ecf40` | certain | Xenos builders start with a focused set of pages — ship your first | `modules/hall-ui/public/not-ready.html:26` |
931
931
  | `b6b0fcb2bcee` | certain | You already have a pending request or invitation for this goal. | `modules/hall-ui/public/goals-render.js:947` |
932
932
  | `a674836fec16` | certain | You already have access ✓ | `modules/hall-ui/public/hall-render.js:355` |
@@ -1020,8 +1020,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1020
1020
  | `17ab8db79e1f` | certain | holds the worker, the code, and your local memory; the | `modules/hall-ui/public/diagrams.html:83` |
1021
1021
  | `7d5cbe3d81e4` | certain | how long a vote stays open | `modules/hall-ui/public/government.html:40` |
1022
1022
  | `dacb901c4f10` | certain | idea number | `modules/hall-ui/public/task-detail.js:458` |
1023
- | `7df22e74c87a` | certain | idea {…} | `modules/hall-ui/public/idea.js:190` |
1024
- | `7df22e74c87a` | certain | idea {…} | `modules/hall-ui/public/ideas.js:326` |
1023
+ | `7df22e74c87a` | certain | idea {…} | `modules/hall-ui/public/idea.js:198` |
1024
+ | `7df22e74c87a` | certain | idea {…} | `modules/hall-ui/public/ideas.js:334` |
1025
1025
  | `2123a62d69e3` | certain | if this stays empty. | `modules/hall-ui/public/collab.html:40` |
1026
1026
  | `b2b399fdd6a4` | certain | in progress | `modules/hall-ui/public/goals-render.js:188` |
1027
1027
  | `b2b399fdd6a4` | certain | in progress | `modules/hall-ui/public/roadmap.js:108` |
@@ -1040,7 +1040,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1040
1040
  | `754bdec368ed` | certain | lowest credits | `modules/hall-ui/public/hall-widgets.js:136` |
1041
1041
  | `cffba8409070` | certain | manage its visibility in Settings → Cross-project record | `modules/hall-ui/public/profile.js:200` |
1042
1042
  | `5c3fdd34cd02` | certain | means expressible through the real configuration — a rank-based membership plus a pass rule — with no new code | `modules/hall-ui/public/government.js:610` |
1043
- | `0eefd625d954` | certain | merged into | `modules/hall-ui/public/idea.js:190` |
1043
+ | `0eefd625d954` | certain | merged into | `modules/hall-ui/public/idea.js:198` |
1044
1044
  | `70c234985d42` | certain | most recent first | `modules/hall-ui/public/hall-widgets.js:134` |
1045
1045
  | `7421250dc155` | certain | name, author, origin project, or licence… | `modules/hall-ui/public/modules.html:30` |
1046
1046
  | `f61d8fc1516d` | certain | new SSH identity | `modules/hall-ui/public/harbor.js:122` |
@@ -1084,7 +1084,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1084
1084
  | `cfc61d8cabe6` | certain | tally: {…} yes · {…} no — sitting under | `modules/hall-ui/public/government.js:786` |
1085
1085
  | `129ed842096b` | certain | target gone | `modules/hall-ui/public/copy-desk.js:63` |
1086
1086
  | `4d8ee7d68974` | certain | task {…} | `modules/hall-ui/public/gate.js:123` |
1087
- | `4d8ee7d68974` | certain | task {…} | `modules/hall-ui/public/idea.js:187` |
1087
+ | `4d8ee7d68974` | certain | task {…} | `modules/hall-ui/public/idea.js:195` |
1088
1088
  | `a41659dc2bee` | certain | tasks remaining | `modules/hall-ui/public/roadmap.js:273` |
1089
1089
  | `bd3402efa59e` | certain | task{…} shipped | `modules/hall-ui/public/goals-render.js:656` |
1090
1090
  | `ee431f143549` | certain | three tasks | `modules/hall-ui/public/not-ready.html:28` |
@@ -1147,7 +1147,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1147
1147
  | `085f93f3f775` | certain | {…} — see all standings | `modules/hall-ui/public/hall-render.js:80` |
1148
1148
  | `f75ee96a7511` | certain | {…} — total payout from shipped work. | `modules/hall-ui/public/profile.js:58` |
1149
1149
  | `2a2728a4ec4f` | certain | {…}% of tasks complete | `modules/hall-ui/public/roadmap.js:70` |
1150
- | `c43050b62297` | certain | {…}/100 complete | `modules/hall-ui/public/idea.js:73` |
1150
+ | `c43050b62297` | certain | {…}/100 complete | `modules/hall-ui/public/idea.js:81` |
1151
1151
  | `499916045b19` | certain | {…}{…} asks to join | `modules/hall-ui/public/goal-inbox.js:69` |
1152
1152
  | `99d107c17070` | certain | · by {…} · | `modules/hall-ui/public/gate.js:138` |
1153
1153
  | `4188b6f8997e` | certain | · default (on) | `modules/hall-ui/public/settings.js:500` |
@@ -1224,8 +1224,8 @@
1224
1224
  "text": "idea {…}",
1225
1225
  "occurrences": 2,
1226
1226
  "at": [
1227
- "modules/hall-ui/public/idea.js:190",
1228
- "modules/hall-ui/public/ideas.js:326"
1227
+ "modules/hall-ui/public/idea.js:198",
1228
+ "modules/hall-ui/public/ideas.js:334"
1229
1229
  ]
1230
1230
  },
1231
1231
  {
@@ -1265,7 +1265,7 @@
1265
1265
  "occurrences": 2,
1266
1266
  "at": [
1267
1267
  "modules/hall-ui/public/gate.js:123",
1268
- "modules/hall-ui/public/idea.js:187"
1268
+ "modules/hall-ui/public/idea.js:195"
1269
1269
  ]
1270
1270
  },
1271
1271
  {
@@ -1752,7 +1752,7 @@
1752
1752
  "surface": "builders-hall",
1753
1753
  "text": "A quick idea is a thought filed fast. It is not an unfinished Full Idea — only its author can offer it up for development.",
1754
1754
  "file": "modules/hall-ui/public/idea.js",
1755
- "line": 237,
1755
+ "line": 245,
1756
1756
  "origin": "js-markup",
1757
1757
  "confidence": "certain"
1758
1758
  },
@@ -1950,7 +1950,7 @@
1950
1950
  "surface": "builders-hall",
1951
1951
  "text": "Answer the five questions and this spark becomes a Full Idea.",
1952
1952
  "file": "modules/hall-ui/public/ideas.js",
1953
- "line": 415,
1953
+ "line": 423,
1954
1954
  "origin": "js-text-assign",
1955
1955
  "confidence": "certain"
1956
1956
  },
@@ -2112,7 +2112,7 @@
2112
2112
  "surface": "builders-hall",
2113
2113
  "text": "As first filed",
2114
2114
  "file": "modules/hall-ui/public/idea.js",
2115
- "line": 229,
2115
+ "line": 237,
2116
2116
  "origin": "js-markup",
2117
2117
  "confidence": "certain"
2118
2118
  },
@@ -3147,7 +3147,7 @@
3147
3147
  "surface": "builders-hall",
3148
3148
  "text": "Could not load idea {…}.",
3149
3149
  "file": "modules/hall-ui/public/ideas.js",
3150
- "line": 444,
3150
+ "line": 456,
3151
3151
  "origin": "js-text-assign",
3152
3152
  "confidence": "certain"
3153
3153
  },
@@ -3228,7 +3228,7 @@
3228
3228
  "surface": "builders-hall",
3229
3229
  "text": "Could not load the idea template. Reload, or file from the terminal with capture.js.",
3230
3230
  "file": "modules/hall-ui/public/ideas.js",
3231
- "line": 422,
3231
+ "line": 438,
3232
3232
  "origin": "js-text-assign",
3233
3233
  "confidence": "certain"
3234
3234
  },
@@ -3255,7 +3255,7 @@
3255
3255
  "surface": "builders-hall",
3256
3256
  "text": "Could not load the question labels; showing the answers as stored.",
3257
3257
  "file": "modules/hall-ui/public/idea.js",
3258
- "line": 221,
3258
+ "line": 229,
3259
3259
  "origin": "js-markup",
3260
3260
  "confidence": "certain"
3261
3261
  },
@@ -3264,7 +3264,7 @@
3264
3264
  "surface": "builders-hall",
3265
3265
  "text": "Could not load the queue just now.",
3266
3266
  "file": "modules/hall-ui/public/ideas.js",
3267
- "line": 379,
3267
+ "line": 387,
3268
3268
  "origin": "js-markup",
3269
3269
  "confidence": "certain"
3270
3270
  },
@@ -3804,7 +3804,7 @@
3804
3804
  "surface": "builders-hall",
3805
3805
  "text": "Develop this idea",
3806
3806
  "file": "modules/hall-ui/public/ideas.js",
3807
- "line": 414,
3807
+ "line": 422,
3808
3808
  "origin": "js-text-assign",
3809
3809
  "confidence": "certain"
3810
3810
  },
@@ -3993,7 +3993,7 @@
3993
3993
  "surface": "builders-hall",
3994
3994
  "text": "Draft discarded.",
3995
3995
  "file": "modules/hall-ui/public/ideas.js",
3996
- "line": 339,
3996
+ "line": 347,
3997
3997
  "origin": "js-notice",
3998
3998
  "confidence": "certain"
3999
3999
  },
@@ -4263,7 +4263,7 @@
4263
4263
  "surface": "builders-hall",
4264
4264
  "text": "Five questions, and a score that counts how much of each you actually answered.",
4265
4265
  "file": "modules/hall-ui/public/ideas.js",
4266
- "line": 432,
4266
+ "line": 448,
4267
4267
  "origin": "js-text-assign",
4268
4268
  "confidence": "certain"
4269
4269
  },
@@ -4740,7 +4740,7 @@
4740
4740
  "surface": "builders-hall",
4741
4741
  "text": "Idea {…}",
4742
4742
  "file": "modules/hall-ui/public/idea.js",
4743
- "line": 244,
4743
+ "line": 252,
4744
4744
  "origin": "js-text-assign",
4745
4745
  "confidence": "certain"
4746
4746
  },
@@ -4884,7 +4884,7 @@
4884
4884
  "surface": "builders-hall",
4885
4885
  "text": "Its author marked this as needing development — anyone can turn it into a Full Idea, and they keep the credit for having had it.",
4886
4886
  "file": "modules/hall-ui/public/idea.js",
4887
- "line": 235,
4887
+ "line": 243,
4888
4888
  "origin": "js-markup",
4889
4889
  "confidence": "certain"
4890
4890
  },
@@ -5712,7 +5712,7 @@
5712
5712
  "surface": "builders-hall",
5713
5713
  "text": "No description.",
5714
5714
  "file": "modules/hall-ui/public/idea.js",
5715
- "line": 106,
5715
+ "line": 114,
5716
5716
  "origin": "js-markup",
5717
5717
  "confidence": "certain"
5718
5718
  },
@@ -5946,7 +5946,7 @@
5946
5946
  "surface": "builders-hall",
5947
5947
  "text": "Not scored.",
5948
5948
  "file": "modules/hall-ui/public/idea.js",
5949
- "line": 70,
5949
+ "line": 78,
5950
5950
  "origin": "js-markup",
5951
5951
  "confidence": "certain"
5952
5952
  },
@@ -6045,7 +6045,7 @@
6045
6045
  "surface": "builders-hall",
6046
6046
  "text": "Nothing is waiting. A spark appears here when its author offers a quick idea up for development.",
6047
6047
  "file": "modules/hall-ui/public/ideas.js",
6048
- "line": 357,
6048
+ "line": 365,
6049
6049
  "origin": "js-markup",
6050
6050
  "confidence": "certain"
6051
6051
  },
@@ -7548,7 +7548,7 @@
7548
7548
  "surface": "builders-hall",
7549
7549
  "text": "Showing {…} of {…}.",
7550
7550
  "file": "modules/hall-ui/public/ideas.js",
7551
- "line": 361,
7551
+ "line": 369,
7552
7552
  "origin": "js-markup",
7553
7553
  "confidence": "certain"
7554
7554
  },
@@ -8961,7 +8961,7 @@
8961
8961
  "surface": "builders-hall",
8962
8962
  "text": "Write one",
8963
8963
  "file": "modules/hall-ui/public/idea.js",
8964
- "line": 235,
8964
+ "line": 243,
8965
8965
  "origin": "js-markup",
8966
8966
  "confidence": "certain"
8967
8967
  },
@@ -9807,7 +9807,7 @@
9807
9807
  "surface": "builders-hall",
9808
9808
  "text": "idea {…}",
9809
9809
  "file": "modules/hall-ui/public/idea.js",
9810
- "line": 190,
9810
+ "line": 198,
9811
9811
  "origin": "js-markup",
9812
9812
  "confidence": "certain"
9813
9813
  },
@@ -9816,7 +9816,7 @@
9816
9816
  "surface": "builders-hall",
9817
9817
  "text": "idea {…}",
9818
9818
  "file": "modules/hall-ui/public/ideas.js",
9819
- "line": 326,
9819
+ "line": 334,
9820
9820
  "origin": "js-markup",
9821
9821
  "confidence": "certain"
9822
9822
  },
@@ -9987,7 +9987,7 @@
9987
9987
  "surface": "builders-hall",
9988
9988
  "text": "merged into",
9989
9989
  "file": "modules/hall-ui/public/idea.js",
9990
- "line": 190,
9990
+ "line": 198,
9991
9991
  "origin": "js-markup",
9992
9992
  "confidence": "certain"
9993
9993
  },
@@ -10383,7 +10383,7 @@
10383
10383
  "surface": "builders-hall",
10384
10384
  "text": "task {…}",
10385
10385
  "file": "modules/hall-ui/public/idea.js",
10386
- "line": 187,
10386
+ "line": 195,
10387
10387
  "origin": "js-markup",
10388
10388
  "confidence": "certain"
10389
10389
  },
@@ -10950,7 +10950,7 @@
10950
10950
  "surface": "builders-hall",
10951
10951
  "text": "{…}/100 complete",
10952
10952
  "file": "modules/hall-ui/public/idea.js",
10953
- "line": 73,
10953
+ "line": 81,
10954
10954
  "origin": "js-markup",
10955
10955
  "confidence": "certain"
10956
10956
  },
@@ -39,7 +39,15 @@
39
39
  'use strict';
40
40
 
41
41
  const API = '/api/gds';
42
- const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
42
+ // NO baseUrl OVERRIDE — this page calls the GENERATED namespaced methods
43
+ // (api.inbox.*, api.me.*), whose path templates are API-RELATIVE ('/inbox/template').
44
+ // baseUrl:'' means "these paths are already absolute" and passes them through
45
+ // unchanged, which sent every request to the SITE ROOT and 404'd — this page was
46
+ // dead in production from the day it shipped. Letting it default resolves them
47
+ // against '/api/bongos/v1' (api-prefix.js VERSIONED_API_PREFIX), where the router
48
+ // actually mounts. A page that DOES pass baseUrl:'' must build full paths itself
49
+ // via api.request('GET', `${API}/...`) as work.js/copy-desk.js do — never mix the two.
50
+ const api = window.BongosClient.createClient({ credentials: 'same-origin', throwOnError: false });
43
51
  const { escapeHtml, $, fmtDate } = window.OTB;
44
52
 
45
53
  const ideaId = () => {
@@ -44,7 +44,15 @@
44
44
  (() => {
45
45
  'use strict';
46
46
 
47
- const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
47
+ // NO baseUrl OVERRIDE — this page calls the GENERATED namespaced methods
48
+ // (api.inbox.*, api.me.*), whose path templates are API-RELATIVE ('/inbox/template').
49
+ // baseUrl:'' means "these paths are already absolute" and passes them through
50
+ // unchanged, which sent every request to the SITE ROOT and 404'd — this page was
51
+ // dead in production from the day it shipped. Letting it default resolves them
52
+ // against '/api/bongos/v1' (api-prefix.js VERSIONED_API_PREFIX), where the router
53
+ // actually mounts. A page that DOES pass baseUrl:'' must build full paths itself
54
+ // via api.request('GET', `${API}/...`) as work.js/copy-desk.js do — never mix the two.
55
+ const api = window.BongosClient.createClient({ credentials: 'same-origin', throwOnError: false });
48
56
  const { escapeHtml, $, toast } = window.OTB;
49
57
 
50
58
  // One key per THING BEING WRITTEN. A new idea and a spark being developed are
@@ -417,6 +425,14 @@
417
425
  }
418
426
 
419
427
  async function main() {
428
+ // THE QUEUE IS LOADED FIRST, AND UNCONDITIONALLY. The composer and the spark
429
+ // queue are independent surfaces that happen to share a page: the queue needs
430
+ // no template. Loading it after the template check meant one failed fetch took
431
+ // BOTH down — the reported symptom was "Could not load the idea template" AND a
432
+ // queue stuck on "Loading…" forever, which read as two bugs and was one.
433
+ // A surface that does not depend on the failure must not be hostage to it.
434
+ loadQueue();
435
+
420
436
  const r = await api.inbox.getInboxTemplate({});
421
437
  if (!r.ok || !r.data || !r.data.template) {
422
438
  $('#fi-sub').textContent = 'Could not load the idea template. Reload, or file from the terminal with capture.js.';
@@ -434,10 +450,6 @@
434
450
  $('#fi-form').addEventListener('submit', file);
435
451
  $('#fi-clear').addEventListener('click', discard);
436
452
 
437
- // The queue loads on every visit, develop mode or not: having just picked one
438
- // up, seeing what else is waiting is the natural next thing.
439
- loadQueue();
440
-
441
453
  if (DEVELOP_ID) {
442
454
  const d = await api.inbox.getInboxId({ id: DEVELOP_ID });
443
455
  if (!d.ok || !d.data || !d.data.idea) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.347",
3
+ "version": "1.19.348",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.347",
9
+ "version": "1.19.348",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.347",
3
+ "version": "1.19.348",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/client-baseurl-guard.js — a web page must not send its API calls to
3
+ // the site root (task 1003263).
4
+ //
5
+ // THE BUG THIS EXISTS TO STOP. The generated client (clients/bongos-client) exposes
6
+ // two call styles, and mixing them is silent and fatal:
7
+ //
8
+ // NAMESPACED api.inbox.getInboxTemplate({}) → path template '/inbox/template',
9
+ // API-RELATIVE, needs the API base
10
+ // ESCAPE HATCH api.request('GET', `${API}/me`) → caller supplies the FULL path
11
+ //
12
+ // createClient documents `baseUrl: ''` as "paths are already absolute" and passes
13
+ // them through unchanged. So a page that sets baseUrl:'' AND calls a namespaced
14
+ // method requests '/inbox/template' — the SITE ROOT, where nothing is mounted — and
15
+ // 404s on every call. The router mounts at '/api/bongos/v1' (api-prefix.js
16
+ // VERSIONED_API_PREFIX), which is exactly the client's own default.
17
+ //
18
+ // WHY A FITNESS CHECK AND NOT A TEST. Three pages shipped this defect — ideas.js and
19
+ // idea.js were dead in production from the day they shipped, and collab.js was caught
20
+ // in review one commit before it would have been. Every page test stubs the client
21
+ // method directly, so URL construction is never exercised; tests/idea_hall_composer.mjs
22
+ // even asserted with a regex against the source text, which passes happily on a broken
23
+ // URL. 54 green tests over pages that 404 on every call. The defect is invisible to the
24
+ // test style this repo uses for hall pages, so it needs a guard at a different altitude.
25
+ //
26
+ // STATIC AND CONSERVATIVE. It flags only the unambiguous combination — an explicit
27
+ // empty baseUrl in a createClient call, in a file that also makes a namespaced call.
28
+ // A page using the escape hatch with baseUrl:'' is CORRECT and stays silent (work.js,
29
+ // copy-desk.js, goal-inbox.js and most of the hall do exactly that).
30
+
31
+ 'use strict';
32
+
33
+ const fs = require('node:fs');
34
+ const path = require('node:path');
35
+ const { execFileSync } = require('node:child_process');
36
+
37
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
38
+ const NAME = 'a web page sends its API calls to the API, not the site root (task 1003263)';
39
+
40
+ // createClient({ ... baseUrl: '' ... }) — an EXPLICIT empty base. Omitting baseUrl is
41
+ // the correct fix and must not match; only the literal empty string is the defect.
42
+ const EMPTY_BASEURL = /createClient\(\s*\{[^}]*baseUrl\s*:\s*(''|""|`\s*`)/;
43
+
44
+ // api.<namespace>.<method>( — the generated namespaced style. `api.request(` is the
45
+ // escape hatch and is deliberately NOT matched: with it, baseUrl:'' is correct.
46
+ const NAMESPACED_CALL = /\bapi\s*\.\s*(?!request\b)[A-Za-z_$][\w$]*\s*\.\s*[A-Za-z_$][\w$]*\s*\(/;
47
+
48
+ // Every tracked browser-served script. Server code never calls createClient this way.
49
+ function trackedPageScripts() {
50
+ try {
51
+ const out = execFileSync('git', ['ls-files', 'modules/*/public/*.js', 'public/*.js'],
52
+ { cwd: REPO_ROOT, encoding: 'utf8' });
53
+ return out.split('\n').map((s) => s.trim()).filter(Boolean);
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ function scanPages(files, read) {
60
+ const violations = [];
61
+ let scanned = 0;
62
+ let usingClient = 0;
63
+ for (const rel of files) {
64
+ const src = read(rel);
65
+ if (src == null) continue;
66
+ scanned += 1;
67
+ if (!/createClient\(/.test(src)) continue;
68
+ usingClient += 1;
69
+ if (!EMPTY_BASEURL.test(src)) continue;
70
+ if (!NAMESPACED_CALL.test(src)) continue; // escape-hatch page — baseUrl:'' is right
71
+ const m = NAMESPACED_CALL.exec(src);
72
+ violations.push(
73
+ `${rel} sets baseUrl:'' AND calls the generated client (${m[0].replace(/\s+/g, '')}…). `
74
+ + "Those paths are API-relative, so baseUrl:'' sends them to the site root and every "
75
+ + 'call 404s. Drop the baseUrl override, or switch every call to '
76
+ + "api.request('METHOD', `${API}/…`) with a full path.",
77
+ );
78
+ }
79
+ return { violations, scanned, usingClient };
80
+ }
81
+
82
+ function checkClientBaseUrl({ files, read } = {}) {
83
+ const list = files || trackedPageScripts();
84
+ if (!list.length) {
85
+ return {
86
+ name: NAME, ok: false, hardFail: true,
87
+ violations: ['scan defect — enumerated 0 tracked page scripts; a broken enumeration, not a clean result.'],
88
+ warnings: [], note: 'static scan of browser-served scripts for a root-bound API client.',
89
+ };
90
+ }
91
+ const reader = read || ((rel) => {
92
+ try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
93
+ });
94
+ const { violations, scanned, usingClient } = scanPages(list, reader);
95
+ return {
96
+ name: NAME,
97
+ ok: violations.length === 0,
98
+ hardFail: violations.length > 0,
99
+ violations,
100
+ warnings: [],
101
+ note: `${scanned} browser-served script(s) scanned, ${usingClient} building a client: none may combine `
102
+ + "baseUrl:'' with a generated namespaced call (api.<ns>.<method>()) — that pairing 404s every request. "
103
+ + "baseUrl:'' with api.request() and a full path is the correct escape-hatch style and is not flagged.",
104
+ };
105
+ }
106
+
107
+ function main() {
108
+ const r = checkClientBaseUrl();
109
+ for (const v of r.violations) console.error(`✗ ${v}`);
110
+ console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
111
+ process.exit(r.hardFail ? 1 : 0);
112
+ }
113
+
114
+ if (require.main === module) main();
115
+
116
+ module.exports = { checkClientBaseUrl, scanPages, trackedPageScripts, EMPTY_BASEURL, NAMESPACED_CALL };
@@ -160,6 +160,28 @@ async function fetchLive(base, timeoutMs = DEFAULT_TIMEOUT_MS) {
160
160
  }
161
161
  }
162
162
 
163
+ // Is the automation that should have PREVENTED this divergence itself alive? (task 1003158)
164
+ //
165
+ // This script detects the SYMPTOM — live is behind main — and only ever at a moment a human is
166
+ // already looking. What went unwatched is the auto-upgrade subscription itself: when it stopped,
167
+ // nothing said so, and cloudbongos.com sat 36 releases behind for days. Reading the roster here means
168
+ // a dead upgrader is reported in the same breath as the staleness it was supposed to prevent.
169
+ //
170
+ // Speaks ONLY when a subscription manifest is actually present. An ordinary core checkout has none by
171
+ // design (ADR 0136 — it ships empty), and alarming about that every session is exactly the noise that
172
+ // gets a real warning scrolled past. Present-but-broken, or present-and-subscribing-nothing, is the
173
+ // signal worth having: someone meant to enroll instances here and none are being upgraded.
174
+ function subscriptionHealth(dir = REPO_ROOT) {
175
+ try {
176
+ const uc = require('./update-channel');
177
+ const roster = uc.loadSubscriptions({ instanceDir: dir });
178
+ if (!roster.present) return null;
179
+ return uc.describeRoster(roster, { controlPlane: true });
180
+ } catch {
181
+ return null; // never let the health probe break the divergence check it rides along with
182
+ }
183
+ }
184
+
163
185
  async function check({ base, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
164
186
  const target = base || arg('--url') || API_BASE;
165
187
  const local = coreVersionAt(null);
@@ -173,7 +195,9 @@ async function check({ base, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
173
195
  const unreleased = unreleasedSince(mainRef);
174
196
  const live = await fetchLive(target, timeoutMs);
175
197
 
176
- if (live.error) return { ok: false, undetermined: true, reason: `could not read ${live.url}: ${live.error}`, target, local, onMain, unreleased };
198
+ const subscriptions = subscriptionHealth();
199
+
200
+ if (live.error) return { ok: false, undetermined: true, reason: `could not read ${live.url}: ${live.error}`, target, local, onMain, unreleased, subscriptions };
177
201
 
178
202
  const behind = cmpSemver(live.coreVersion, onMain) < 0;
179
203
  const ahead = cmpSemver(live.coreVersion, onMain) > 0;
@@ -181,7 +205,7 @@ async function check({ base, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
181
205
 
182
206
  return {
183
207
  ok: !behind && !hasUnreleased,
184
- target, local, onMain, mainRef, unreleased, live, behind, ahead, hasUnreleased,
208
+ target, local, onMain, mainRef, unreleased, live, behind, ahead, hasUnreleased, subscriptions,
185
209
  };
186
210
  }
187
211
 
@@ -232,6 +256,13 @@ function report(r, { quiet = false } = {}) {
232
256
  if (r.ahead) {
233
257
  say(` note: live (${r.live.coreVersion}) is AHEAD of ${r.mainRef} (${r.onMain}) — this checkout is stale; fetch.`);
234
258
  }
259
+ // The watcher's watcher: if this checkout carries a subscription roster and that roster is not
260
+ // healthy, the unattended upgrade is not going to close the gap above on its own.
261
+ if (r.subscriptions && r.subscriptions.level !== 'ok') {
262
+ say('');
263
+ say(` ${r.subscriptions.level === 'alarm' ? '⚠ AUTO-UPGRADE' : 'note: auto-upgrade'} — ${r.subscriptions.message}`);
264
+ if (r.behind) say(' So the gap above will NOT close by itself.');
265
+ }
235
266
  say('');
236
267
  say(' Merging to main deploys nothing by design. "shipped" means "merged" —');
237
268
  say(' it does not mean users can see it.');
@@ -317,4 +348,4 @@ if (require.main === module) {
317
348
  });
318
349
  }
319
350
 
320
- module.exports = { check, report, line, coreVersionAt, unreleasedSince, cmpSemver };
351
+ module.exports = { check, report, line, coreVersionAt, unreleasedSince, cmpSemver, subscriptionHealth };
@@ -1307,6 +1307,7 @@ const CHECKS = [
1307
1307
  require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
1308
1308
  require('./routine-schedule.js').checkRoutineCadencesScheduled, // Check 25 — task 1003209: a routine that declares a cadence must have a scheduler firing it AT that period (ADR 0188)
1309
1309
  require('./baseline-staleness.js').checkBaselineFloors, // Check 26 — task 1003210: a shrink-only baseline that never shrinks is unmanaged debt; a floor exempting one must say why
1310
+ require('./client-baseurl-guard.js').checkClientBaseUrl, // Check 27 — task 1003263: a page must not combine baseUrl:'' with a generated namespaced call (that pairing 404s every request; three pages shipped it, two of them dead in prod)
1310
1311
  ];
1311
1312
 
1312
1313
  function runAll() {
@@ -32,6 +32,23 @@ const DEFAULT_CHANNEL = 'patch';
32
32
  // core template — a fresh instance subscribes nothing until the operator lists a target here.
33
33
  const SUBSCRIPTIONS_REL = path.join('config', 'update-subscriptions.json');
34
34
 
35
+ // Why a roster came back the way it did. The routine reports this EVERY run: an upgrader whose only
36
+ // job is keeping instances current must never be indistinguishable from one that has stopped. Before
37
+ // task 1003158 an absent manifest, a malformed manifest and a deliberately empty roster all produced
38
+ // the same silent `instances: []`, which is exactly how cloudbongos.com sat 36 releases behind for
39
+ // days with nothing complaining (the same failure SHAPE as blocker 1000097 / task 1002521).
40
+ const ROSTER_OK = 'ok'; // at least one usable entry
41
+ const ROSTER_ABSENT = 'manifest-absent'; // no file at all
42
+ const ROSTER_MALFORMED = 'manifest-malformed';// file present, JSON unparseable
43
+ const ROSTER_EMPTY = 'roster-empty'; // file parsed, nothing subscribable in it
44
+ const ROSTER_REASONS = [ROSTER_OK, ROSTER_ABSENT, ROSTER_MALFORMED, ROSTER_EMPTY];
45
+
46
+ // How many releases a subscribed instance may fall behind before "the upgrader stopped" is the only
47
+ // remaining explanation. A successful sweep takes the HIGHEST permitted version in one jump, so a
48
+ // healthy roster returns to 0 behind after every run — a backlog that keeps growing means no run is
49
+ // landing. 5 leaves room for a busy publish-on-merge day between two daily sweeps without crying wolf.
50
+ const DEFAULT_STALENESS_THRESHOLD = 5;
51
+
35
52
  // ---- semver (stable x.y.z, optional prerelease tag) -------------------------
36
53
  // A deliberately small parser — the repo avoids an npm-semver dependency in the CLI path (see
37
54
  // upgrade.js versionFromDep). We only need compare + the major/minor/patch fields.
@@ -143,16 +160,23 @@ function parseNpmViewVersions(stdout) {
143
160
  // this control plane keeps up to date. Never throws: a missing/malformed file ⇒ an empty roster
144
161
  // (subscribe nothing), because an unreadable manifest must not upgrade anything. Each entry is
145
162
  // normalized to { slug, dir, service, healthUrl, channel } with the channel validated + defaulted.
163
+ //
164
+ // Still fails CLOSED, no longer fails QUIET (task 1003158): every return carries a `reason` from
165
+ // ROSTER_REASONS and a `dropped` count of entries that were unusable, so a caller can say WHY it is
166
+ // about to do nothing. Pair it with describeRoster() for the line to print.
146
167
  function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SUBSCRIPTIONS_REL } = {}) {
147
168
  const abs = path.isAbsolute(file) ? file : path.join(instanceDir, file);
148
169
  let raw;
149
- try { raw = fsImpl.readFileSync(abs, 'utf8'); } catch (_) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: false }; }
170
+ try { raw = fsImpl.readFileSync(abs, 'utf8'); } catch (_) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: false, dropped: 0, reason: ROSTER_ABSENT }; }
150
171
  let json;
151
- try { json = JSON.parse(raw); } catch (e) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: true, error: `malformed JSON: ${e.message}` }; }
172
+ try { json = JSON.parse(raw); } catch (e) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: true, dropped: 0, error: `malformed JSON: ${e.message}`, reason: ROSTER_MALFORMED }; }
152
173
  const defaultChannel = normalizeChannel(json.defaultChannel);
153
174
  const rows = Array.isArray(json.instances) ? json.instances : [];
154
- const instances = rows
155
- .filter((r) => r && typeof r === 'object' && r.dir) // an entry without a dir can't be upgraded — drop it
175
+ // An entry without a dir can't be upgraded — drop it, but COUNT the drops. Silently discarding an
176
+ // operator's malformed row is how an instance believes it is enrolled while nothing ever touches it.
177
+ const usable = rows.filter((r) => r && typeof r === 'object' && r.dir);
178
+ const dropped = rows.length - usable.length;
179
+ const instances = usable
156
180
  .map((r) => ({
157
181
  slug: String(r.slug || r.dir),
158
182
  dir: String(r.dir),
@@ -178,7 +202,90 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
178
202
  ? Object.fromEntries(Object.entries(r.env).filter(([, v]) => typeof v === 'string'))
179
203
  : {},
180
204
  }));
181
- return { defaultChannel, instances, source: abs, present: true };
205
+ return {
206
+ defaultChannel, instances, source: abs, present: true, dropped,
207
+ reason: instances.length ? ROSTER_OK : ROSTER_EMPTY,
208
+ };
209
+ }
210
+
211
+ // ---- reporting: make "nothing happened" audible -----------------------------
212
+
213
+ // Turn a loadSubscriptions() result into the line a routine prints EVERY run, plus a level a caller
214
+ // can act on ('ok' | 'warn' | 'alarm'). The judgement that matters is `controlPlane`: a fresh
215
+ // instance subscribing nothing is the shipped default (ADR 0136 — the manifest ships empty), but a
216
+ // CONTROL PLANE that subscribes nothing has silently stopped doing its only job, and that is an alarm.
217
+ // A malformed manifest is an alarm everywhere — an operator wrote something and it does not parse.
218
+ function describeRoster(roster, { controlPlane = false } = {}) {
219
+ const r = roster || {};
220
+ const reason = ROSTER_REASONS.includes(r.reason) ? r.reason : ROSTER_EMPTY;
221
+ const where = r.source || SUBSCRIPTIONS_REL;
222
+ const dropped = Number(r.dropped) || 0;
223
+ const n = Array.isArray(r.instances) ? r.instances.length : 0;
224
+
225
+ if (reason === ROSTER_MALFORMED) {
226
+ return { level: 'alarm', reason, count: n, dropped,
227
+ message: `update-subscriptions: MALFORMED manifest at ${where} (${r.error || 'unparseable'}) — subscribing NOTHING. No instance will be upgraded until this parses.` };
228
+ }
229
+ if (reason === ROSTER_ABSENT) {
230
+ return { level: controlPlane ? 'alarm' : 'warn', reason, count: n, dropped,
231
+ message: `update-subscriptions: no manifest at ${where} — subscribing NOTHING.${controlPlane ? ' This is a CONTROL PLANE: it is supposed to keep instances current and currently keeps none.' : ' That is the shipped default; add an instance to enroll it.'}` };
232
+ }
233
+ if (reason === ROSTER_EMPTY) {
234
+ return { level: controlPlane ? 'alarm' : 'warn', reason, count: n, dropped,
235
+ message: `update-subscriptions: manifest at ${where} lists no upgradable instance${dropped ? ` (${dropped} entr${dropped === 1 ? 'y' : 'ies'} dropped for a missing \`dir\`)` : ''} — subscribing NOTHING.${controlPlane ? ' This is a CONTROL PLANE: it is supposed to keep instances current and currently keeps none.' : ''}` };
236
+ }
237
+ // Usable entries — still surface partial loss, which is the quiet half of an enrolled-but-untouched
238
+ // instance: the sweep looks healthy while the operator's broken row is never upgraded.
239
+ if (dropped > 0) {
240
+ return { level: 'warn', reason, count: n, dropped,
241
+ message: `update-subscriptions: ${n} instance${n === 1 ? '' : 's'} subscribed, but ${dropped} entr${dropped === 1 ? 'y was' : 'ies were'} DROPPED for a missing \`dir\` in ${where} — those instances are NOT being upgraded.` };
242
+ }
243
+ return { level: 'ok', reason, count: n, dropped,
244
+ message: `update-subscriptions: ${n} instance${n === 1 ? '' : 's'} subscribed from ${where}.` };
245
+ }
246
+
247
+ // ---- watching the watcher ---------------------------------------------------
248
+
249
+ // How far behind is this instance in releases its OWN channel is entitled to take? Not raw version
250
+ // distance: majors, prereleases and (on the patch channel) minors are releases the subscription is
251
+ // correctly declining, and counting them would alarm on instances that are behaving exactly as
252
+ // configured. `behind` is therefore "stable releases this channel WOULD have taken and has not".
253
+ //
254
+ // This is the check that was missing (task 1003158). deploy-divergence.js detects the SYMPTOM — live
255
+ // is behind main — at ship time, when a human is already looking. Nothing watched whether the
256
+ // upgrader itself was still alive, so its death was only ever discovered by someone noticing stale UI
257
+ // days later. A healthy sweep drives `behind` to 0 every run, so a persistent backlog IS the alarm.
258
+ //
259
+ // A `pinned` instance is never stale: channelAllows declines everything, so behind === 0 by
260
+ // construction. That is correct — a pinned instance is not neglected, it is pinned.
261
+ function subscriptionStaleness({ installed, channel, available, threshold = DEFAULT_STALENESS_THRESHOLD } = {}) {
262
+ const ch = normalizeChannel(channel);
263
+ const list = Array.isArray(available) ? available : [];
264
+ // `installed` is parsed and the channel re-normalized ONCE, not per candidate:
265
+ // channelAllows re-does both on every element, and this filter is the only place
266
+ // that calls it in a loop. Cheap either way at registry scale — hoisted because
267
+ // the invariant genuinely is invariant, not because the arithmetic hurts.
268
+ const from = parseSemver(installed);
269
+ const behind = (ch === 'pinned' || !from) ? 0
270
+ : list.filter((v) => channelAllows(installed, v, ch)).length;
271
+ const t = Number(threshold);
272
+ const limit = Number.isFinite(t) && t >= 0 ? t : DEFAULT_STALENESS_THRESHOLD;
273
+ return {
274
+ channel: ch,
275
+ installed: installed == null ? null : String(installed),
276
+ target: resolveChannelTarget({ installed, channel: ch, available: list }),
277
+ behind,
278
+ threshold: limit,
279
+ stale: behind > limit,
280
+ };
281
+ }
282
+
283
+ // The alarm line for one subscribed instance, or null when it is healthy (nothing to say).
284
+ function describeStaleness(st, { slug = 'instance' } = {}) {
285
+ if (!st || !st.stale) return null;
286
+ return `update-subscriptions: ${slug} is ${st.behind} ${st.channel} release${st.behind === 1 ? '' : 's'} behind `
287
+ + `(installed ${st.installed || 'unknown'}, entitled to ${st.target}) — over the ${st.threshold} threshold. `
288
+ + 'A healthy sweep clears this to zero every run, so the upgrader has most likely stopped.';
182
289
  }
183
290
 
184
291
  module.exports = {
@@ -195,4 +302,13 @@ module.exports = {
195
302
  listAvailableVersions,
196
303
  parseNpmViewVersions,
197
304
  loadSubscriptions,
305
+ ROSTER_OK,
306
+ ROSTER_ABSENT,
307
+ ROSTER_MALFORMED,
308
+ ROSTER_EMPTY,
309
+ ROSTER_REASONS,
310
+ DEFAULT_STALENESS_THRESHOLD,
311
+ describeRoster,
312
+ subscriptionStaleness,
313
+ describeStaleness,
198
314
  };
package/src/module-api.js CHANGED
@@ -1124,8 +1124,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1124
1124
  // landed since 1.19.345 with no explicit bump. run 32801147433. (task 1002620)
1125
1125
  // 1.19.347 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1126
1126
  // landed since 1.19.346 with no explicit bump. run 32802265337. (task 1002620)
1127
+ // 1.19.348 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1128
+ // landed since 1.19.347 with no explicit bump. run 32802746363. (task 1002620)
1127
1129
  // ---------------------------------------------------------------------------
1128
- const CORE_VERSION = '1.19.347'; // CI auto-patch carrier (ADR 0161); see changelog above
1130
+ const CORE_VERSION = '1.19.348'; // CI auto-patch carrier (ADR 0161); see changelog above
1129
1131
 
1130
1132
  // A namespaced logger so a module's log lines are attributable + consistent.
1131
1133
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,122 @@
1
+ // tests/client_baseurl_guard.mjs — the guard that stops a page 404ing every call
2
+ // (task 1003263).
3
+ //
4
+ // Three pages shipped the defect this guard exists to catch: ideas.js and idea.js
5
+ // were dead in production from the day they shipped, and collab.js was caught one
6
+ // commit before it would have joined them. So the guard itself is worth testing on
7
+ // synthetic sources rather than only on the live tree — a scan that passes because
8
+ // the tree happens to be clean today proves nothing about what it would REFUSE.
9
+ //
10
+ // Every case below is an in-memory file: no fixtures on disk, no dependence on the
11
+ // current state of modules/hall-ui/public/.
12
+ //
13
+ // Run: node tests/client_baseurl_guard.mjs
14
+
15
+ import { strict as assert } from 'node:assert';
16
+ import { createRequire } from 'node:module';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const { checkClientBaseUrl, scanPages } = require('../scripts/gds/client-baseurl-guard.js');
20
+
21
+ let passed = 0;
22
+ let failed = 0;
23
+ function test(name, fn) {
24
+ try { fn(); passed++; console.log(` ok ${name}`); }
25
+ catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
26
+ }
27
+
28
+ const from = (map) => ({
29
+ files: Object.keys(map),
30
+ read: (rel) => (rel in map ? map[rel] : null),
31
+ });
32
+
33
+ // --- THE DEFECT ------------------------------------------------------------
34
+
35
+ test('the exact shipped defect is refused: baseUrl:\'\' plus a namespaced call', () => {
36
+ const { violations } = scanPages(...Object.values(from({
37
+ 'modules/hall-ui/public/bad.js':
38
+ "const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin' });\n"
39
+ + 'const r = await api.inbox.getInboxTemplate({});\n',
40
+ })));
41
+ assert.equal(violations.length, 1);
42
+ assert.match(violations[0], /bad\.js/);
43
+ assert.match(violations[0], /site root/, 'the message says WHY it is wrong, not just that it is');
44
+ assert.match(violations[0], /Drop the baseUrl override/, 'and what to do about it');
45
+ });
46
+
47
+ test('double-quoted and backtick empty bases are the same defect', () => {
48
+ for (const empty of ['""', '``']) {
49
+ const { violations } = scanPages(...Object.values(from({
50
+ 'public/p.js': `const api = createClient({ baseUrl: ${empty} });\napi.me.getMe({});\n`,
51
+ })));
52
+ assert.equal(violations.length, 1, `baseUrl: ${empty} must be caught`);
53
+ }
54
+ });
55
+
56
+ // --- WHAT IT MUST NOT FLAG -------------------------------------------------
57
+ // A guard that fires on correct code gets suppressed, and then it protects nothing.
58
+
59
+ test("the escape-hatch style is CORRECT with baseUrl:'' — never flagged", () => {
60
+ // work.js, copy-desk.js and most of the hall do exactly this: empty base, but
61
+ // every path written out in full. Flagging it would be a false positive on the
62
+ // majority of the hall.
63
+ const { violations } = scanPages(...Object.values(from({
64
+ 'modules/hall-ui/public/work.js':
65
+ "const API = '/api/bongos';\n"
66
+ + "const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin' });\n"
67
+ + "const res = await api.request('GET', `${API}/tasks`);\n",
68
+ })));
69
+ assert.deepEqual(violations, []);
70
+ });
71
+
72
+ test('the fix itself passes — omitting baseUrl with namespaced calls is correct', () => {
73
+ const { violations } = scanPages(...Object.values(from({
74
+ 'modules/hall-ui/public/ideas.js':
75
+ "const api = window.BongosClient.createClient({ credentials: 'same-origin', throwOnError: false });\n"
76
+ + 'await api.inbox.getInboxTemplate({});\n',
77
+ })));
78
+ assert.deepEqual(violations, [], 'the shipped fix must not trip the guard that motivated it');
79
+ });
80
+
81
+ test('a non-empty baseUrl is a deliberate choice, not the defect', () => {
82
+ const { violations } = scanPages(...Object.values(from({
83
+ 'public/ext.js':
84
+ "const api = createClient({ baseUrl: 'https://other.example/api/bongos/v1' });\n"
85
+ + 'api.me.getMe({});\n',
86
+ })));
87
+ assert.deepEqual(violations, []);
88
+ });
89
+
90
+ test('a page that builds no client at all is ignored', () => {
91
+ const { violations, usingClient } = scanPages(...Object.values(from({
92
+ 'public/static.js': "document.querySelector('#x').textContent = 'hi';\n",
93
+ })));
94
+ assert.deepEqual(violations, []);
95
+ assert.equal(usingClient, 0);
96
+ });
97
+
98
+ // --- THE SCAN MUST NOT SILENTLY SUCCEED ------------------------------------
99
+
100
+ test('an empty enumeration is a hard FAIL, not a clean pass', () => {
101
+ // The failure mode that makes a guard worthless: git ls-files returns nothing,
102
+ // zero files are scanned, and the check reports green forever.
103
+ const r = checkClientBaseUrl({ files: [], read: () => null });
104
+ assert.equal(r.hardFail, true);
105
+ assert.match(r.violations[0], /scan defect/);
106
+ });
107
+
108
+ test('an unreadable file is skipped without being counted as scanned', () => {
109
+ const { scanned } = scanPages(['gone.js'], () => null);
110
+ assert.equal(scanned, 0);
111
+ });
112
+
113
+ test('the check reports hardFail so CI actually stops', () => {
114
+ const r = checkClientBaseUrl(from({
115
+ 'public/bad.js': "createClient({ baseUrl: '' });\napi.inbox.postInbox({});\n",
116
+ }));
117
+ assert.equal(r.ok, false);
118
+ assert.equal(r.hardFail, true, 'a page dead in prod is not a warning');
119
+ });
120
+
121
+ console.log(`\nclient_baseurl_guard: ${passed} passed, ${failed} failed`);
122
+ process.exit(failed ? 1 : 0);
@@ -112,6 +112,17 @@ await test('the page gets both the FORM and the METER from the server', () => {
112
112
  assert.match(IDEAS_JS, /getInboxTemplate/, 'the form is rendered from GET /inbox/template');
113
113
  assert.match(IDEAS_JS, /postInboxScore/, 'the meter is filled from POST /inbox/score');
114
114
  assert.match(IDEAS_JS, /postInbox\b/, 'and it files through POST /inbox');
115
+
116
+ // ...AND THOSE CALLS MUST REACH THE API. The three assertions above are regexes
117
+ // over the SOURCE TEXT, so they passed for months while the page 404'd on every
118
+ // one of them: baseUrl:'' told the client the API-relative templates were already
119
+ // absolute, and each request went to the site root. Naming a method proves the
120
+ // call is written, never that it is addressed — task 1003263.
121
+ assert.doesNotMatch(
122
+ IDEAS_JS,
123
+ /createClient\(\s*\{[^}]*baseUrl\s*:\s*(''|"")/,
124
+ "baseUrl:'' sends the generated client's API-relative paths to the site root",
125
+ );
115
126
  });
116
127
 
117
128
  await test('the page does not print a pass bar — that rule lives in economy', () => {
@@ -8,6 +8,8 @@
8
8
  // 4. parseNpmViewVersions — the array / bare-string / empty shapes npm view emits.
9
9
  // 5. listAvailableVersions — ok + fail-soft with an injected runner.
10
10
  // 6. loadSubscriptions — missing/malformed → empty; channel default + validate; env kept; dropless.
11
+ // 7. describeRoster — an empty roster REPORTS WHY, and is an alarm on a control plane (task 1003158).
12
+ // 8. subscriptionStaleness — releases the channel was entitled to and did not take = "the upgrader stopped".
11
13
  //
12
14
  // DB-free + network-free (every external is injected). Run: node tests/update_channel.mjs
13
15
 
@@ -182,6 +184,129 @@ t('loadSubscriptions: an entry without a dir is dropped (cannot be upgraded)', (
182
184
  assert.equal(r.instances[0].slug, 'real');
183
185
  });
184
186
 
187
+ // ---- describeRoster: fails closed, no longer fails quiet (task 1003158) ----
188
+ //
189
+ // The regression these pin: before this, a missing manifest, a malformed manifest and a deliberately
190
+ // empty roster were INDISTINGUISHABLE — all three returned a bare `instances: []` and the routine
191
+ // shrugged. cloudbongos.com sat 36 releases behind for days because "nothing happened" looked
192
+ // identical to "nothing needed to happen". Every empty roster must now say WHY, every run.
193
+
194
+ t('loadSubscriptions: missing file reports reason manifest-absent (not just an empty list)', () => {
195
+ const dir = mkdtempSync(join(tmpdir(), 'otb-subs-none2-'));
196
+ const r = uc.loadSubscriptions({ instanceDir: dir });
197
+ assert.deepEqual(r.instances, []);
198
+ assert.equal(r.reason, uc.ROSTER_ABSENT);
199
+ assert.equal(r.dropped, 0);
200
+ });
201
+
202
+ t('loadSubscriptions: malformed JSON reports reason manifest-malformed + keeps the error', () => {
203
+ const dir = scratchWith('{ not valid');
204
+ const r = uc.loadSubscriptions({ instanceDir: dir });
205
+ assert.equal(r.reason, uc.ROSTER_MALFORMED);
206
+ assert.match(r.error, /malformed JSON/);
207
+ });
208
+
209
+ t('loadSubscriptions: parsed-but-unsubscribable reports roster-empty and COUNTS the drops', () => {
210
+ const dir = scratchWith({ instances: [{ slug: 'ghost' }, { slug: 'alsoghost' }] });
211
+ const r = uc.loadSubscriptions({ instanceDir: dir });
212
+ assert.deepEqual(r.instances, []);
213
+ assert.equal(r.reason, uc.ROSTER_EMPTY);
214
+ assert.equal(r.dropped, 2);
215
+ });
216
+
217
+ t('loadSubscriptions: a usable roster reports ok', () => {
218
+ const dir = scratchWith({ instances: [{ slug: 'cb', dir: '/srv/cb' }] });
219
+ const r = uc.loadSubscriptions({ instanceDir: dir });
220
+ assert.equal(r.reason, uc.ROSTER_OK);
221
+ assert.equal(r.dropped, 0);
222
+ });
223
+
224
+ t('describeRoster: an empty roster on a CONTROL PLANE is an alarm, not a shrug', () => {
225
+ const dir = mkdtempSync(join(tmpdir(), 'otb-subs-cp-'));
226
+ const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: dir }), { controlPlane: true });
227
+ assert.equal(d.level, 'alarm');
228
+ assert.match(d.message, /CONTROL PLANE/);
229
+ assert.match(d.message, /subscribing NOTHING/);
230
+ });
231
+
232
+ t('describeRoster: the same empty roster on a plain instance is only a warn (shipped default)', () => {
233
+ const dir = mkdtempSync(join(tmpdir(), 'otb-subs-plain-'));
234
+ const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: dir }));
235
+ assert.equal(d.level, 'warn');
236
+ assert.match(d.message, /shipped default/);
237
+ });
238
+
239
+ t('describeRoster: a malformed manifest is an alarm everywhere', () => {
240
+ const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: scratchWith('{ nope') }));
241
+ assert.equal(d.level, 'alarm');
242
+ assert.match(d.message, /MALFORMED/);
243
+ });
244
+
245
+ t('describeRoster: partial loss is surfaced — a healthy-looking sweep that silently skips a row', () => {
246
+ const dir = scratchWith({ instances: [{ slug: 'cb', dir: '/srv/cb' }, { slug: 'ghost' }] });
247
+ const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: dir }));
248
+ assert.equal(d.level, 'warn');
249
+ assert.equal(d.dropped, 1);
250
+ assert.match(d.message, /DROPPED/);
251
+ });
252
+
253
+ t('describeRoster: a clean roster is ok and says how many it holds', () => {
254
+ const dir = scratchWith({ instances: [{ slug: 'cb', dir: '/srv/cb' }] });
255
+ const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: dir }), { controlPlane: true });
256
+ assert.equal(d.level, 'ok');
257
+ assert.equal(d.count, 1);
258
+ });
259
+
260
+ // ---- subscriptionStaleness: watching the watcher ---------------------------
261
+
262
+ const RELEASES = Array.from({ length: 41 }, (_, i) => `1.19.${250 + i}`); // 1.19.250 … 1.19.290
263
+
264
+ t('subscriptionStaleness: reproduces the incident — 1.19.254 on patch is 36 releases behind, stale', () => {
265
+ const st = uc.subscriptionStaleness({ installed: '1.19.254', channel: 'patch', available: RELEASES });
266
+ assert.equal(st.behind, 36);
267
+ assert.equal(st.target, '1.19.290');
268
+ assert.equal(st.stale, true);
269
+ });
270
+
271
+ t('subscriptionStaleness: a swept instance is 0 behind and not stale', () => {
272
+ const st = uc.subscriptionStaleness({ installed: '1.19.290', channel: 'patch', available: RELEASES });
273
+ assert.equal(st.behind, 0);
274
+ assert.equal(st.target, null);
275
+ assert.equal(st.stale, false);
276
+ });
277
+
278
+ t('subscriptionStaleness: a busy publish-on-merge day under the threshold does NOT cry wolf', () => {
279
+ const st = uc.subscriptionStaleness({ installed: '1.19.287', channel: 'patch', available: RELEASES });
280
+ assert.equal(st.behind, 3);
281
+ assert.equal(st.stale, false);
282
+ });
283
+
284
+ t('subscriptionStaleness: a pinned instance is never stale (declining is not neglect)', () => {
285
+ const st = uc.subscriptionStaleness({ installed: '1.19.254', channel: 'pinned', available: RELEASES });
286
+ assert.equal(st.behind, 0);
287
+ assert.equal(st.stale, false);
288
+ });
289
+
290
+ t('subscriptionStaleness: counts only what the CHANNEL may take — minors/majors are not a backlog', () => {
291
+ const st = uc.subscriptionStaleness({ installed: '1.19.254', channel: 'patch', available: ['1.20.0', '1.20.7', '2.0.0'] });
292
+ assert.equal(st.behind, 0);
293
+ assert.equal(st.stale, false);
294
+ // the same list on the minor channel IS a backlog
295
+ assert.equal(uc.subscriptionStaleness({ installed: '1.19.254', channel: 'minor', available: ['1.20.0', '1.20.7', '2.0.0'] }).behind, 2);
296
+ });
297
+
298
+ t('subscriptionStaleness: threshold is overridable; a bad threshold falls back to the default', () => {
299
+ assert.equal(uc.subscriptionStaleness({ installed: '1.19.287', channel: 'patch', available: RELEASES, threshold: 1 }).stale, true);
300
+ assert.equal(uc.subscriptionStaleness({ installed: '1.19.287', channel: 'patch', available: RELEASES, threshold: 'nonsense' }).threshold, uc.DEFAULT_STALENESS_THRESHOLD);
301
+ });
302
+
303
+ t('describeStaleness: silent when healthy, explicit about "the upgrader stopped" when not', () => {
304
+ assert.equal(uc.describeStaleness(uc.subscriptionStaleness({ installed: '1.19.290', channel: 'patch', available: RELEASES })), null);
305
+ const msg = uc.describeStaleness(uc.subscriptionStaleness({ installed: '1.19.254', channel: 'patch', available: RELEASES }), { slug: 'cloudbongos.com' });
306
+ assert.match(msg, /cloudbongos\.com is 36 patch releases behind/);
307
+ assert.match(msg, /upgrader has most likely stopped/);
308
+ });
309
+
185
310
  console.log(`\nupdate_channel: ${passed} passed, ${failed} failed`);
186
311
  process.exit(failed ? 1 : 0);
187
312