@bongos/core 1.19.674 → 1.19.676

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.674",
6
- "core_contract": "1.19.674",
7
- "source_commit": "0da6434caa66f679eeb8c34bc2be3826f2a1bfe8",
5
+ "core_version": "1.19.676",
6
+ "core_contract": "1.19.676",
7
+ "source_commit": "9aad4d0005991e8ba38f4443b4e5611a13aa3af8",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-11T18:40:06.671Z",
9
+ "built_at": "2026-09-11T20:44:28.469Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 474,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2122,
14
+ "functional_verbatim": 2123,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2620,
20
- "tree_sha256": "984de9fdd6b42c1d4cf8728faad1a458ab08d8491957851e2874992610d4ae01",
19
+ "file_count": 2621,
20
+ "tree_sha256": "b556a6e9644d99b65ffdf9ac64384eee16b20ecfb7e31ed60c875d76297310f3",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -367,12 +367,12 @@
367
367
  {
368
368
  "path": "clients/bongos-client/README.md",
369
369
  "mode": "0000644",
370
- "sha256": "03379a98721207feab9a9b4c03862051ee6d52a29a0bc467ed73ae1d36bf49cd"
370
+ "sha256": "b15290057c38e4b80db4136d7fdd32549718e31f5975484e77098c66050c4366"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/bongos-client.global.js",
374
374
  "mode": "0000644",
375
- "sha256": "ec75333abb0ee49b89320a86e73b56f2f3872c13c596b1e67f36e705b8ba1210"
375
+ "sha256": "bdf715d3196df99d675570748f7ac054522046cbe89d78b84c27eef89a52d18f"
376
376
  },
377
377
  {
378
378
  "path": "clients/bongos-client/examples/hello-world.mjs",
@@ -382,17 +382,17 @@
382
382
  {
383
383
  "path": "clients/bongos-client/index.cjs",
384
384
  "mode": "0000644",
385
- "sha256": "bce3dcdbe37505e0fa526e02b7b379ca9ab378f6c34b41345621d55170d9f19d"
385
+ "sha256": "2fbbab84a89021945a707fd21d72acbad7f03b77c5fb6eb39abce44a64d4cc1b"
386
386
  },
387
387
  {
388
388
  "path": "clients/bongos-client/index.d.ts",
389
389
  "mode": "0000644",
390
- "sha256": "d692cd6b80b1c286f6990a0ebcb3c109d51213b6cdc58cc7dc97fa52ff6fbe46"
390
+ "sha256": "22523f2527d8849445122c5a03332d777a63d849a5806e33d387cbd83c54fb2f"
391
391
  },
392
392
  {
393
393
  "path": "clients/bongos-client/index.mjs",
394
394
  "mode": "0000644",
395
- "sha256": "3fbcb50de89350e95b86180e828a16b897be32a1f8b4cdf78cc060e1c5afff86"
395
+ "sha256": "eeb72bda7a89900279647d6d999d2a920a986bf2f4ed3a63782cd4e36b1b3dfc"
396
396
  },
397
397
  {
398
398
  "path": "clients/bongos-client/package.json",
@@ -1912,12 +1912,12 @@
1912
1912
  {
1913
1913
  "path": "docs/api-reference.md",
1914
1914
  "mode": "0000644",
1915
- "sha256": "4b396cff8505cdec44f4cb471c79c5e5af8e5446a04a3ecaad0cba5435677f9a"
1915
+ "sha256": "6b681ad5de20a67efa1d6d024d5cb5dbe5313add4ec51d94a30a0025fa567ba2"
1916
1916
  },
1917
1917
  {
1918
1918
  "path": "docs/api/openapi.json",
1919
1919
  "mode": "0000644",
1920
- "sha256": "8292a0f17190f803436bd3ae60f7ca36631d57b552c3553b6f5f2cdcd4eaffd9"
1920
+ "sha256": "301cf1db00b9f18b84cb765bbaa359329a665b1ef754dff4c21c1550ec60cae5"
1921
1921
  },
1922
1922
  {
1923
1923
  "path": "docs/architecture.md",
@@ -2792,7 +2792,7 @@
2792
2792
  {
2793
2793
  "path": "docs/module-api-changelog.md",
2794
2794
  "mode": "0000644",
2795
- "sha256": "0e8b8ccaf7913c7d40a59f5662da9dac4cbd8ec419df1e7ada549a26d1824f67"
2795
+ "sha256": "b8786c58575f93eb93661ee1d7e0745f0993f5666e5237ca073177df2d3ee9f4"
2796
2796
  },
2797
2797
  {
2798
2798
  "path": "docs/modules-contract.md",
@@ -2912,7 +2912,7 @@
2912
2912
  {
2913
2913
  "path": "docs/recipes/core-release-pipeline.md",
2914
2914
  "mode": "0000644",
2915
- "sha256": "06b938c2ad38915d3ed92b20a94c57621ae835f617b01ab02fc01129cd154e2c"
2915
+ "sha256": "86d266be2c1833484225886c1ed961b8f19d511e84226032dc65c9ccf18f5550"
2916
2916
  },
2917
2917
  {
2918
2918
  "path": "docs/recipes/devbox-app-release.md",
@@ -4242,7 +4242,7 @@
4242
4242
  {
4243
4243
  "path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
4244
4244
  "mode": "0000644",
4245
- "sha256": "bce3dcdbe37505e0fa526e02b7b379ca9ab378f6c34b41345621d55170d9f19d"
4245
+ "sha256": "2fbbab84a89021945a707fd21d72acbad7f03b77c5fb6eb39abce44a64d4cc1b"
4246
4246
  },
4247
4247
  {
4248
4248
  "path": "modules/dev-box/app/test/unit.mjs",
@@ -4332,7 +4332,7 @@
4332
4332
  {
4333
4333
  "path": "modules/discord/discord-channels.js",
4334
4334
  "mode": "0000644",
4335
- "sha256": "e910fb46a97b5d25338477a609eed045862020f1634e7ba4312a5b3857605d04"
4335
+ "sha256": "aeeb86c5d61b4a06ae5429fb69bb4720d54f4782ead5d23cdce640344a568246"
4336
4336
  },
4337
4337
  {
4338
4338
  "path": "modules/discord/discord-inbound.js",
@@ -4382,7 +4382,7 @@
4382
4382
  {
4383
4383
  "path": "modules/discord/routes/discord.js",
4384
4384
  "mode": "0000644",
4385
- "sha256": "eeb7c7165f18e33216bff0bb8b846a1674f537a95e8853c0b88fc101c336fb53"
4385
+ "sha256": "2229a563e11bd05f0eb047f0afc0e1d66eaddb6a7fa2fb07c429e162684246f0"
4386
4386
  },
4387
4387
  {
4388
4388
  "path": "modules/discord/ship-broadcast.js",
@@ -7767,12 +7767,12 @@
7767
7767
  {
7768
7768
  "path": "package-lock.json",
7769
7769
  "mode": "0000644",
7770
- "sha256": "747c1f7d390c913bc71f93fa96ecc76f67aea4409a3265fdf6555ca91aac5c11"
7770
+ "sha256": "3b0ebf2f1fb870085a40531295fb3c5989be5ab399e2493e43a1e3e844b0633c"
7771
7771
  },
7772
7772
  {
7773
7773
  "path": "package.json",
7774
7774
  "mode": "0000644",
7775
- "sha256": "59c1c75a1f396810212ad4654ac2dfd0f100e4c63fd3b2b7f5e11be22ce1a6fc"
7775
+ "sha256": "b4c1e3c5189ac7220af056624bdc0476a0d751c00aa76aba2e84416128280bc7"
7776
7776
  },
7777
7777
  {
7778
7778
  "path": "public-docs/index.html",
@@ -8152,7 +8152,7 @@
8152
8152
  {
8153
8153
  "path": "scripts/gds/discord-channels.js",
8154
8154
  "mode": "0000644",
8155
- "sha256": "395ad654afa348b1b00e40aa1f685ac66e9ce5961213a4d4c35c132759527ecb"
8155
+ "sha256": "e142b2d64e919adc4781ea6a55796113ebde3a3645bd4c4ed113ff9f13cd7f6a"
8156
8156
  },
8157
8157
  {
8158
8158
  "path": "scripts/gds/do-api.js",
@@ -8327,7 +8327,7 @@
8327
8327
  {
8328
8328
  "path": "scripts/gds/go-live.js",
8329
8329
  "mode": "0000644",
8330
- "sha256": "1e14a452c0ae53384facd6a20526a90106cb479c414937ebb7fe180f11c5fa31"
8330
+ "sha256": "d1383b714ba0d8e4d0f5ceb097e45c1ed1af09caf648f7a43a6c6431c38fb422"
8331
8331
  },
8332
8332
  {
8333
8333
  "path": "scripts/gds/goal-names.js",
@@ -9102,7 +9102,7 @@
9102
9102
  {
9103
9103
  "path": "scripts/gds/upgrade.js",
9104
9104
  "mode": "0000644",
9105
- "sha256": "430d1f55212254d75144fb15a438ffc61aad1ea7d5176b36f39e87c30efeb2df"
9105
+ "sha256": "7a173ecf47b25007bba28f68787893352f429a52b34b7f7b4eae0d3791a5cdcf"
9106
9106
  },
9107
9107
  {
9108
9108
  "path": "scripts/gds/validate-design.js",
@@ -9532,7 +9532,7 @@
9532
9532
  {
9533
9533
  "path": "src/module-api.js",
9534
9534
  "mode": "0000644",
9535
- "sha256": "b2adf8fa055095fb344dad2ce76e3e0710ba0c23e3b5ccd68105b5daeded0390"
9535
+ "sha256": "406c218b56b011b4560887f2ae4e13f20e227fa4404e6763649e2eac2b0f5292"
9536
9536
  },
9537
9537
  {
9538
9538
  "path": "src/module-loader/catalog.js",
@@ -10439,6 +10439,11 @@
10439
10439
  "mode": "0000644",
10440
10440
  "sha256": "981d34c2fb5ecb6e9b9f38ece947c6c7a400dceadf8bbcf09546c8b3a92eb899"
10441
10441
  },
10442
+ {
10443
+ "path": "tests/discord_channels_snapshot.mjs",
10444
+ "mode": "0000644",
10445
+ "sha256": "d4e43501ac29ea920c0c064afe601bd9fc24f87808f10986a51d981b3111946b"
10446
+ },
10442
10447
  {
10443
10448
  "path": "tests/discord_foundation.mjs",
10444
10449
  "mode": "0000644",
@@ -10662,7 +10667,7 @@
10662
10667
  {
10663
10668
  "path": "tests/go_live.mjs",
10664
10669
  "mode": "0000644",
10665
- "sha256": "ce97c74d749ec3574c0683160b3ce5dad8a7af2dcf1ce53fd23e95f5e0ad480d"
10670
+ "sha256": "7528da9c6cd81d9876f0ffae0bf836844cb726a9bbb24cda44f0675d6f6ac0ea"
10666
10671
  },
10667
10672
  {
10668
10673
  "path": "tests/goal_abuse_matrix.mjs",
@@ -12977,7 +12982,7 @@
12977
12982
  {
12978
12983
  "path": "tests/upgrade_persist_pin.mjs",
12979
12984
  "mode": "0000644",
12980
- "sha256": "07fd48b255bbd2370580b62a8f77d7fc981b0dfdefcc5be6973a9c4d6cb49add"
12985
+ "sha256": "c0a96fbdc6a9f851925babfb0857c9a168374c4165c84884f5c4c56c7177dd58"
12981
12986
  },
12982
12987
  {
12983
12988
  "path": "tests/verify_task_guard.mjs",
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
5
5
  by hand; it regenerates when the spec changes, so it can never drift from the routes.
6
6
 
7
7
  - API version: **v1** (served at `/api/bongos/v1`)
8
- - 362 operations across 55 resource groups
8
+ - 363 operations across 55 resource groups
9
9
 
10
10
  ## Use it from your project
11
11
 
@@ -313,6 +313,8 @@ function createClient(opts = {}) {
313
313
  getDiscordChannelsPlan: (args) => request("GET", "/discord/channels/plan", { hasBody: false }, args),
314
314
  // POST /discord/channels/reconcile — rank: metic+archon — POST /discord/channels/reconcile
315
315
  postDiscordChannelsReconcile: (args) => request("POST", "/discord/channels/reconcile", { hasBody: true }, args),
316
+ // GET /discord/channels/snapshot — rank: metic+archon — GET /discord/channels/snapshot
317
+ getDiscordChannelsSnapshot: (args) => request("GET", "/discord/channels/snapshot", { hasBody: false }, args),
316
318
  },
317
319
  "doneWhen": {
318
320
  // PATCH /done-when/{criterionId} — rank: metic+archon — PATCH /done-when/:criterionId
@@ -312,6 +312,8 @@ function createClient(opts = {}) {
312
312
  getDiscordChannelsPlan: (args) => request("GET", "/discord/channels/plan", { hasBody: false }, args),
313
313
  // POST /discord/channels/reconcile — rank: metic+archon — POST /discord/channels/reconcile
314
314
  postDiscordChannelsReconcile: (args) => request("POST", "/discord/channels/reconcile", { hasBody: true }, args),
315
+ // GET /discord/channels/snapshot — rank: metic+archon — GET /discord/channels/snapshot
316
+ getDiscordChannelsSnapshot: (args) => request("GET", "/discord/channels/snapshot", { hasBody: false }, args),
315
317
  },
316
318
  "doneWhen": {
317
319
  // PATCH /done-when/{criterionId} — rank: metic+archon — PATCH /done-when/:criterionId
@@ -45,6 +45,7 @@ export interface GetConnectionsDegreeHandleResponse { handle: unknown; degree: u
45
45
  export interface GetConnectionsRequestsResponse { incoming: unknown; outgoing: unknown; incoming_count: unknown; outgoing_count: unknown }
46
46
  export interface GetConnectionsResponse { connections: unknown; count: unknown }
47
47
  export interface GetDiscordChannelsPlanResponse { ok: boolean; guild_id: unknown; op_count: unknown; ops: unknown; warnings: unknown }
48
+ export interface GetDiscordChannelsSnapshotResponse { ok: boolean; guild_id: unknown; config: unknown; warnings: unknown }
48
49
  export interface GetDoneWhenPendingReviewResponse { criteria: unknown; count: unknown }
49
50
  export interface GetGithubRepoResponse { ok: boolean; repo: unknown; private_deployable: boolean }
50
51
  export interface GetGithubReposResponse { repos: unknown }
@@ -647,6 +648,8 @@ export interface BongosClient {
647
648
  getDiscordChannelsPlan(args?: RequestArgs): Promise<GetDiscordChannelsPlanResponse>;
648
649
  /** POST /discord/channels/reconcile — rank: metic+archon */
649
650
  postDiscordChannelsReconcile(args?: RequestArgs): Promise<PostDiscordChannelsReconcileResponse>;
651
+ /** GET /discord/channels/snapshot — rank: metic+archon */
652
+ getDiscordChannelsSnapshot(args?: RequestArgs): Promise<GetDiscordChannelsSnapshotResponse>;
650
653
  };
651
654
  "doneWhen": {
652
655
  /** PATCH /done-when/{criterionId} — rank: metic+archon */
@@ -309,6 +309,8 @@ export function createClient(opts = {}) {
309
309
  getDiscordChannelsPlan: (args) => request("GET", "/discord/channels/plan", { hasBody: false }, args),
310
310
  // POST /discord/channels/reconcile — rank: metic+archon — POST /discord/channels/reconcile
311
311
  postDiscordChannelsReconcile: (args) => request("POST", "/discord/channels/reconcile", { hasBody: true }, args),
312
+ // GET /discord/channels/snapshot — rank: metic+archon — GET /discord/channels/snapshot
313
+ getDiscordChannelsSnapshot: (args) => request("GET", "/discord/channels/snapshot", { hasBody: false }, args),
312
314
  },
313
315
  "doneWhen": {
314
316
  // PATCH /done-when/{criterionId} — rank: metic+archon — PATCH /done-when/:criterionId
@@ -4338,6 +4338,47 @@
4338
4338
  ]
4339
4339
  }
4340
4340
  },
4341
+ "/discord/channels/snapshot": {
4342
+ "get": {
4343
+ "operationId": "get_discord_channels_snapshot",
4344
+ "tags": [
4345
+ "discord"
4346
+ ],
4347
+ "summary": "GET /discord/channels/snapshot",
4348
+ "description": "GET /api/bongos/discord/channels/snapshot — what docs/discord/channels.json WOULD say if it described the guild exactly as it is right now (task 1003510). Read-only in both directions: it writes nothing to Discord and nothing to the repo — the operator reviews the output and commits it, so the layout stays Archon-gated by merge, which is the property ADR 0037 §1 rests on. 503 when the bot is disabled / not connected to a guild. rank: archon — channel administration command.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `discord.channels.manage` (all required).",
4349
+ "x-rank": "metic+archon",
4350
+ "x-source": "modules/discord/routes/discord.js",
4351
+ "x-permissions": [
4352
+ "discord.channels.manage"
4353
+ ],
4354
+ "responses": {
4355
+ "200": {
4356
+ "description": "Success.",
4357
+ "content": {
4358
+ "application/json": {
4359
+ "schema": {
4360
+ "$ref": "#/components/schemas/GetDiscordChannelsSnapshotResponse"
4361
+ }
4362
+ }
4363
+ }
4364
+ },
4365
+ "400": {
4366
+ "$ref": "#/components/responses/BadRequest"
4367
+ },
4368
+ "401": {
4369
+ "$ref": "#/components/responses/Unauthorized"
4370
+ },
4371
+ "403": {
4372
+ "$ref": "#/components/responses/Forbidden"
4373
+ }
4374
+ },
4375
+ "security": [
4376
+ {
4377
+ "builderSession": []
4378
+ }
4379
+ ]
4380
+ }
4381
+ },
4341
4382
  "/done-when/pending-review": {
4342
4383
  "get": {
4343
4384
  "operationId": "get_done_when_pending_review",
@@ -17101,6 +17142,23 @@
17101
17142
  "warnings"
17102
17143
  ]
17103
17144
  },
17145
+ "GetDiscordChannelsSnapshotResponse": {
17146
+ "type": "object",
17147
+ "properties": {
17148
+ "ok": {
17149
+ "type": "boolean"
17150
+ },
17151
+ "guild_id": {},
17152
+ "config": {},
17153
+ "warnings": {}
17154
+ },
17155
+ "required": [
17156
+ "ok",
17157
+ "guild_id",
17158
+ "config",
17159
+ "warnings"
17160
+ ]
17161
+ },
17104
17162
  "GetDoneWhenPendingReviewResponse": {
17105
17163
  "type": "object",
17106
17164
  "properties": {
@@ -22955,9 +23013,9 @@
22955
23013
  "description": "A required dependency/feature is not configured or is temporarily down."
22956
23014
  }
22957
23015
  },
22958
- "x-endpoint-count": 362,
22959
- "x-schema-count": 401,
23016
+ "x-endpoint-count": 363,
23017
+ "x-schema-count": 402,
22960
23018
  "x-undocumented-bodies": 9,
22961
- "x-response-schemas": 270,
23019
+ "x-response-schemas": 271,
22962
23020
  "x-generated-by": "scripts/gds/gen-api-docs.js"
22963
23021
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Bongos API reference
4
4
 
5
- > **Generated from the live route files** — the route file is authoritative. 362 endpoints across 58 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 363 endpoints across 58 route files.
6
6
  > Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
7
7
 
8
8
  Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -191,12 +191,13 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
191
191
  | POST | `/api/bongos/dependencies` | `archon` | — | POST /dependencies — create an edge "from is blocked until to is done". |
192
192
  | DELETE | `/api/bongos/dependencies` | `archon` | — | DELETE /dependencies — remove an edge (body carries the 4-tuple). |
193
193
 
194
- ## `discord` (2)
194
+ ## `discord` (3)
195
195
 
196
196
  | Method | Path | Rank | Body | Description |
197
197
  |---|---|---|---|---|
198
198
  | GET | `/api/bongos/discord/channels/plan` | `metic+archon` | — | GET /api/bongos/discord/channels/plan — dry run. |
199
199
  | POST | `/api/bongos/discord/channels/reconcile` | `metic+archon` | — | POST /api/bongos/discord/channels/reconcile — apply the config to Discord now. |
200
+ | GET | `/api/bongos/discord/channels/snapshot` | `metic+archon` | — | GET /api/bongos/discord/channels/snapshot — what docs/discord/channels.json WOULD say if it described the guild exactly as it is right no… |
200
201
 
201
202
  ## `done-when` (4)
202
203
 
@@ -1807,5 +1807,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1807
1807
  landed since 1.19.672 with no explicit bump. run 34632328471. (task 1002620)
1808
1808
  1.19.674 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1809
1809
  landed since 1.19.673 with no explicit bump. run 34634515595. (task 1002620)
1810
+ 1.19.675 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1811
+ landed since 1.19.674 with no explicit bump. run 34639371461. (task 1002620)
1812
+ 1.19.676 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1813
+ landed since 1.19.675 with no explicit bump. run 34645751352. (task 1002620)
1810
1814
  ---------------------------------------------------------------------------
1811
1815
  ```
@@ -168,6 +168,18 @@ Host topology (ssh alias, box paths, service names, health URL) lives in a host-
168
168
  `config/go-live.json`, never in the core — run `node scripts/gds/go-live.js --help` for the
169
169
  schema. A core that named one instance's box would not be a core.
170
170
 
171
+ **Upgrading a provisioned co-tenant** (task 1003521). A co-tenant under `/srv/<host>/<slug>` is
172
+ the same engine with two fields different, because two of this instance's assumptions do not hold
173
+ there: it has no pull-deploy timer to stop (only its `<slug>-db-backup.timer`), and its repo of
174
+ record is the **customer's** own GitHub repo, so pushing the pin there is both wrong and
175
+ uncredentialed. So omit `deployTimer` — the stop and the restart are skipped, and there is no
176
+ `reset --hard` to race — and set `"pinMode": "local-commit"`, which commits the pin without
177
+ pushing it. (`"leave-dirty"` leaves the bump in the working tree; `"push"` is the default and is
178
+ what the platform instance needs.) Everything else — the registry proof, `--verify-marker`, the
179
+ `pg_dump`, the health-gated upgrade with auto-rollback, the box-side version read-back — is
180
+ unchanged. The co-tenant's own scaffold gap is separate and still open:
181
+ [#1003520](https://cloudbongos.com/builders#/task/1003520).
182
+
171
183
  ### Step 1 — pick the version (your machine, main checkout)
172
184
 
173
185
  ```bash
@@ -312,6 +312,8 @@ function createClient(opts = {}) {
312
312
  getDiscordChannelsPlan: (args) => request("GET", "/discord/channels/plan", { hasBody: false }, args),
313
313
  // POST /discord/channels/reconcile — rank: metic+archon — POST /discord/channels/reconcile
314
314
  postDiscordChannelsReconcile: (args) => request("POST", "/discord/channels/reconcile", { hasBody: true }, args),
315
+ // GET /discord/channels/snapshot — rank: metic+archon — GET /discord/channels/snapshot
316
+ getDiscordChannelsSnapshot: (args) => request("GET", "/discord/channels/snapshot", { hasBody: false }, args),
315
317
  },
316
318
  "doneWhen": {
317
319
  // PATCH /done-when/{criterionId} — rank: metic+archon — PATCH /done-when/:criterionId
@@ -178,6 +178,98 @@ function viewOverwritesEqual(a, b, ctx) {
178
178
  // categories: [{ id, name }],
179
179
  // channels: [{ id, name, type, parentId, topic, overwrites:[{id,type,allow,deny}] }]
180
180
  // }
181
+ // viewFromOverwrites — the INVERSE of resolveViewRoles + mergeViewOverwrites:
182
+ // read a channel's real permission overwrites and say which `view` value would
183
+ // produce them. Pure (task 1003510).
184
+ //
185
+ // `view: R` grants R and every rank ABOVE it, so the answer is the LOWEST rank
186
+ // on the ladder holding an explicit VIEW allow. @everyone allowed outranks all
187
+ // of it and means "everyone".
188
+ //
189
+ // Returns null for "cannot tell" — no @everyone overwrite AND no rank role
190
+ // allowed, which is a channel the bot has never touched and which simply
191
+ // inherits its category. Null is the honest answer there, and the caller emits
192
+ // no `view` key so the channel inherits in the config exactly as it does in the
193
+ // guild. Do NOT collapse null to 'everyone': that would write a claim the guild
194
+ // never made, and a later reconcile would act on it.
195
+ function viewFromOverwrites(overwrites, ctx) {
196
+ const { everyoneId, roleMap = {} } = ctx || {};
197
+ const has = (o, field) => (BigInt((o && o[field]) || 0) & VIEW_CHANNEL) !== 0n;
198
+ const byId = new Map((overwrites || []).map((o) => [String(o.id), o]));
199
+
200
+ const everyone = byId.get(String(everyoneId));
201
+ if (everyone && has(everyone, 'allow')) return 'everyone';
202
+
203
+ for (const rank of RANK_LADDER) { // low -> high: first allow wins
204
+ const rid = roleMap[rank] ? String(roleMap[rank]) : null;
205
+ if (rid && has(byId.get(rid), 'allow')) return rank;
206
+ }
207
+ // @everyone explicitly DENIED but no rank allowed: restricted to nobody we
208
+ // manage. resolveViewRoles fails safe to archon, so mirror that rather than
209
+ // inventing wider access.
210
+ if (everyone && has(everyone, 'deny')) return 'archon';
211
+ return null;
212
+ }
213
+
214
+ // configFromSnapshot — turn a LIVE guild snapshot into channels.json shape, so
215
+ // the file can be seeded from what the guild really is instead of from a guess
216
+ // (task 1003510; ADR 0037 §1 calls channels.json the single source of truth, and
217
+ // that is only true once the channels actually in the guild are written down).
218
+ //
219
+ // THE CORRECTNESS PROPERTY, and the reason this is worth having as a pure
220
+ // function: feeding the result straight back to planChannelReconcile against the
221
+ // SAME snapshot must produce ZERO ops. If any field is inverted wrongly — a view,
222
+ // a topic, a name, a parent — the planner emits an op for it, so the round trip
223
+ // is a total test of this function rather than a field-by-field restatement of it.
224
+ //
225
+ // That zero-ops guarantee holds for a guild the bot has already reconciled. On a
226
+ // guild it has NOT, a channel may carry no @everyone overwrite at all; the config
227
+ // then omits its `view`, it inherits the category, and the planner may rightly
228
+ // emit an edit. That difference is real and belongs in the operator's review, not
229
+ // hidden here — which is why `warnings` is returned rather than swallowed.
230
+ //
231
+ // Channels with no parent category are REPORTED, never invented: channels.json
232
+ // can only express a channel nested in a category, and the omitted-is-untouched
233
+ // rule (see the file's own header) makes leaving them out the safe default.
234
+ function configFromSnapshot(snapshot, ctx) {
235
+ const warnings = [];
236
+ const cats = (snapshot.categories || []).map((c) => ({
237
+ id: String(c.id),
238
+ name: c.name,
239
+ view: viewFromOverwrites(c.overwrites, ctx),
240
+ channels: [],
241
+ }));
242
+ const byId = new Map(cats.map((c) => [c.id, c]));
243
+
244
+ for (const ch of snapshot.channels || []) {
245
+ if (ch.type !== 0) continue; // text channels only, as snapshotFromGuild already filters
246
+ const parent = ch.parentId ? byId.get(String(ch.parentId)) : null;
247
+ if (!parent) {
248
+ warnings.push(`channel "${ch.name}" has no category — channels.json cannot express an uncategorised channel, so it is omitted (and therefore left untouched).`);
249
+ continue;
250
+ }
251
+ const view = viewFromOverwrites(ch.overwrites, ctx);
252
+ const entry = { key: slugify(ch.name), name: ch.name, topic: ch.topic || '' };
253
+ // Emit `view` only when it DIFFERS from the category's — the planner reads
254
+ // `ch.view || cat.view`, so an identical value is noise that drifts later.
255
+ if (view !== null && view !== parent.view) entry.view = view;
256
+ if (view === null) {
257
+ warnings.push(`channel "${ch.name}" carries no VIEW overwrite the bot manages — it inherits category "${parent.name}"; confirm that is intended before applying.`);
258
+ }
259
+ parent.channels.push(entry);
260
+ }
261
+
262
+ const categories = cats.map((c) => ({
263
+ name: c.name,
264
+ view: c.view === null ? 'everyone' : c.view,
265
+ channels: c.channels,
266
+ }));
267
+ for (const c of cats) {
268
+ if (c.view === null) warnings.push(`category "${c.name}" carries no VIEW overwrite the bot manages — recorded as "everyone"; confirm before applying.`);
269
+ }
270
+ return { config: { categories }, warnings };
271
+ }
272
+
181
273
  // Ops never include deletions. Categories are planned before their channels so
182
274
  // the apply step can resolve a freshly-created parent id.
183
275
  function planChannelReconcile(snapshot, config, ctx) {
@@ -323,6 +415,18 @@ function computePlan(deps = realDeps) {
323
415
  return { ok: true, ops, warnings, guildId: String(guild.id) };
324
416
  }
325
417
 
418
+ // computeSnapshotConfig — what channels.json WOULD say if it described the guild
419
+ // as it is right now (task 1003510). Read-only: it touches Discord only through
420
+ // the same getGuild() the planner uses, and writes nothing anywhere.
421
+ function computeSnapshotConfig(deps = realDeps) {
422
+ const guild = deps.getGuild();
423
+ if (!guild) return { ok: false, reason: 'no_guild', config: null, warnings: [] };
424
+ const roleMap = deps.loadRoleMap();
425
+ const ctx = { everyoneId: String(guild.id), roleMap, botId: botIdFromGuild(guild) };
426
+ const { config, warnings } = configFromSnapshot(snapshotFromGuild(guild), ctx);
427
+ return { ok: true, config, warnings, guildId: String(guild.id) };
428
+ }
429
+
326
430
  // Apply the plan to Discord. Best-effort, never throws. Returns a summary.
327
431
  // Categories are created first so child channels can resolve their parent id.
328
432
  async function reconcileChannels(deps = realDeps) {
@@ -398,6 +502,8 @@ module.exports = {
398
502
  // pure (tested)
399
503
  slugify,
400
504
  resolveViewRoles,
505
+ viewFromOverwrites,
506
+ configFromSnapshot,
401
507
  mergeViewOverwrites,
402
508
  viewOverwritesEqual,
403
509
  planChannelReconcile,
@@ -405,6 +511,7 @@ module.exports = {
405
511
  loadConfig,
406
512
  snapshotFromGuild,
407
513
  computePlan,
514
+ computeSnapshotConfig,
408
515
  reconcileChannels,
409
516
  CONFIG_PATH,
410
517
  VIEW_CHANNEL,
@@ -14,6 +14,9 @@
14
14
 
15
15
  const express = require('express');
16
16
  const api = require('../../../src/module-api');
17
+ // Structured + attributable, and the console ratchet counts raw console calls —
18
+ // the api.logger(<module key>) precedent every other module's routes follow.
19
+ const log = api.logger('discord');
17
20
  const auth = require('../auth');
18
21
  const db = require('../db');
19
22
  const shipBroadcast = require('../ship-broadcast');
@@ -93,7 +96,28 @@ module.exports = function buildDiscordRouter() {
93
96
  }
94
97
  return res.json({ ok: true, guild_id: plan.guildId, op_count: plan.ops.length, ops: plan.ops, warnings: plan.warnings });
95
98
  } catch (err) {
96
- console.error('[gds] GET /discord/channels/plan', err);
99
+ log.error({ err: err && err.message }, 'GET /discord/channels/plan failed');
100
+ return res.fail('internal_error', 500);
101
+ }
102
+ });
103
+
104
+ // GET /api/bongos/discord/channels/snapshot — what docs/discord/channels.json
105
+ // WOULD say if it described the guild exactly as it is right now (task 1003510).
106
+ // Read-only in both directions: it writes nothing to Discord and nothing to the
107
+ // repo — the operator reviews the output and commits it, so the layout stays
108
+ // Archon-gated by merge, which is the property ADR 0037 §1 rests on.
109
+ // 503 when the bot is disabled / not connected to a guild.
110
+ // rank: archon — channel administration command.
111
+ router.get('/discord/channels/snapshot', auth.requireBuilder, auth.requirePermission('discord.channels.manage'), async (req, res) => {
112
+ try {
113
+ const channels = require('../discord-channels');
114
+ const snap = channels.computeSnapshotConfig();
115
+ if (!snap.ok) {
116
+ return res.fail('bot_unavailable', 503, { reason: snap.reason });
117
+ }
118
+ return res.json({ ok: true, guild_id: snap.guildId, config: snap.config, warnings: snap.warnings });
119
+ } catch (err) {
120
+ log.error({ err: err && err.message }, 'GET /discord/channels/snapshot failed');
97
121
  return res.fail('internal_error', 500);
98
122
  }
99
123
  });
@@ -111,7 +135,7 @@ module.exports = function buildDiscordRouter() {
111
135
  }
112
136
  return res.json({ ok: true, applied: result.applied, failed: result.failed, total: result.total, warnings: result.warnings });
113
137
  } catch (err) {
114
- console.error('[gds] POST /discord/channels/reconcile', err);
138
+ log.error({ err: err && err.message }, 'POST /discord/channels/reconcile failed');
115
139
  return res.fail('internal_error', 500);
116
140
  }
117
141
  });
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.674",
3
+ "version": "1.19.676",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.674",
9
+ "version": "1.19.676",
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.674",
3
+ "version": "1.19.676",
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",