@bongos/core 1.19.956 → 1.19.958

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.956",
6
- "core_contract": "1.19.956",
7
- "source_commit": "b5f9ec6f9c84cf59195511dea0bef9cd2f607e00",
5
+ "core_version": "1.19.958",
6
+ "core_contract": "1.19.958",
7
+ "source_commit": "8b7c56c98aae1497c594219c7ae761ce54c60f8b",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-22T05:46:01.341Z",
9
+ "built_at": "2026-09-22T13:42:54.493Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 514,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2357,
14
+ "functional_verbatim": 2358,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2897,
20
- "tree_sha256": "37422b79ea02e637ca3a28a909c2d299c5f309190fbda25845bcb1ad6d2765c1",
19
+ "file_count": 2898,
20
+ "tree_sha256": "1dd23da0216fa29e9f848ba037a788cf0cfa9d0a55fea04ffb7eba833b142541",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2117,12 +2117,12 @@
2117
2117
  {
2118
2118
  "path": "docs/adr/0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md",
2119
2119
  "mode": "0000644",
2120
- "sha256": "cf6e701b8791462057fa5bc53dc716ef8f80b8ee2efa12a07581305f788a5e48"
2120
+ "sha256": "6ae902e7626feb915300f4b21b6d20ae3369116a86bf624bd614ac9616c3ac11"
2121
2121
  },
2122
2122
  {
2123
2123
  "path": "docs/adr/README.md",
2124
2124
  "mode": "0000644",
2125
- "sha256": "8d2b9a793fed7214d50fa667848b6c3037f129bff3e33113071bdbcd5acc34cf"
2125
+ "sha256": "5db407e3565216666055365b11a5ff05b8901ff9c7fbaee011c93784c40762e6"
2126
2126
  },
2127
2127
  {
2128
2128
  "path": "docs/api-reference.md",
@@ -2157,12 +2157,12 @@
2157
2157
  {
2158
2158
  "path": "docs/copy-inventory.md",
2159
2159
  "mode": "0000644",
2160
- "sha256": "ea08a06744310c312420c4af1e0f67154e324fe2be552b6d5c2fc683e18a63e8"
2160
+ "sha256": "77cb83609d2a80e2c7520746181ef858839d37e8c0997b5dce42127d0c4c119c"
2161
2161
  },
2162
2162
  {
2163
2163
  "path": "docs/copy-registry.json",
2164
2164
  "mode": "0000644",
2165
- "sha256": "45091c4acfe82fa8968be566f8b14f414ee3df68772b13617cba55735de93d1e"
2165
+ "sha256": "8d83ca0d16a1f9d0efc89f0331f6940592ce34e0dd6f92c448575497bce96548"
2166
2166
  },
2167
2167
  {
2168
2168
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2692,7 +2692,7 @@
2692
2692
  {
2693
2693
  "path": "docs/module-api-changelog.md",
2694
2694
  "mode": "0000644",
2695
- "sha256": "e5152e6018b9378631ce08e502ef244efca8db514fdb5acca5f6da21a6fca155"
2695
+ "sha256": "25f1d655a540764066d47dcd30fc53451e253377c57ac1647d3dc3128928a385"
2696
2696
  },
2697
2697
  {
2698
2698
  "path": "docs/modules-contract.md",
@@ -4822,7 +4822,7 @@
4822
4822
  {
4823
4823
  "path": "modules/hall-ui/public/deploy.js",
4824
4824
  "mode": "0000644",
4825
- "sha256": "fca8216900a7ff1d0b48f87e3fd223ae15e61346980ba68e7721801fb971157a"
4825
+ "sha256": "e3a4fa62e761dddfb54a1f13680eb64ff87c4d37e83fa41a846ebfe1f41d44c8"
4826
4826
  },
4827
4827
  {
4828
4828
  "path": "modules/hall-ui/public/diagram-viewer.css",
@@ -6542,7 +6542,7 @@
6542
6542
  {
6543
6543
  "path": "modules/provisioning/core-upgrade.js",
6544
6544
  "mode": "0000644",
6545
- "sha256": "d6ba41f0bdb775191f94a1b57903a3f22cee68a8e7552be92f6112d86b4090fd"
6545
+ "sha256": "96c6b73411a489e5c41fd567123fa8ef35f7a757db0e1df0c0cafd720674b05f"
6546
6546
  },
6547
6547
  {
6548
6548
  "path": "modules/provisioning/credential-preflight.js",
@@ -6674,6 +6674,11 @@
6674
6674
  "mode": "0000644",
6675
6675
  "sha256": "1fb60b17c411d9529f4c0ecd0b3d1efc9b2061567a1b31bf74fa706e2e83b3d7"
6676
6676
  },
6677
+ {
6678
+ "path": "modules/provisioning/migrations/provisioning_024_snapshot_schema_pending.sql",
6679
+ "mode": "0000644",
6680
+ "sha256": "1287440b0088a6451dd4df234e578e06fc6f3e892915e64cd6c6e1f09dadd9d5"
6681
+ },
6677
6682
  {
6678
6683
  "path": "modules/provisioning/module.json",
6679
6684
  "mode": "0000644",
@@ -8077,12 +8082,12 @@
8077
8082
  {
8078
8083
  "path": "package-lock.json",
8079
8084
  "mode": "0000644",
8080
- "sha256": "984830848a268de5d21b596133f328532da98025312eb19c55bab1f59bcefd3d"
8085
+ "sha256": "83b00099cd3853c2bf7ca568043852caec477bace90cb26921122a525ed0bfc8"
8081
8086
  },
8082
8087
  {
8083
8088
  "path": "package.json",
8084
8089
  "mode": "0000644",
8085
- "sha256": "3b5369b5abd11bc15832bd5f20582f76fb70e492e9cf2c79060e01ffd5f288df"
8090
+ "sha256": "8f3e2b77a4b6f11f1eb2ffe270ea5fe212d959a1dd0db7eab501808423df8ef4"
8086
8091
  },
8087
8092
  {
8088
8093
  "path": "public-docs/index.html",
@@ -9027,7 +9032,7 @@
9027
9032
  {
9028
9033
  "path": "scripts/gds/provision-core-upgrade.js",
9029
9034
  "mode": "0000644",
9030
- "sha256": "4a84f48518a6427e95bf23e273865b6b2753489b40737b9256191fd2a65562e1"
9035
+ "sha256": "f22c31d00a50a312d7228922cbe70fedc503e704cc7f2cb33a8686a4954c1c1e"
9031
9036
  },
9032
9037
  {
9033
9038
  "path": "scripts/gds/provision-disconnect.js",
@@ -10087,7 +10092,7 @@
10087
10092
  {
10088
10093
  "path": "src/module-api.js",
10089
10094
  "mode": "0000644",
10090
- "sha256": "77129f6f2f9471813d98976e7fcac4b9f219507c10960a7b3c6aaa219e862421"
10095
+ "sha256": "afe264ed7d639f350d12b51e28f13d9a5a604bac89b110dcad7b38a415334df3"
10091
10096
  },
10092
10097
  {
10093
10098
  "path": "src/module-loader/catalog.js",
@@ -10992,12 +10997,12 @@
10992
10997
  {
10993
10998
  "path": "tests/core_upgrade_door.mjs",
10994
10999
  "mode": "0000644",
10995
- "sha256": "7333e05011a080e5ac3a54bb1b07fb421db848ee2eb54c786f36c43058b9e587"
11000
+ "sha256": "c4238e2f2e2c52f23fb6948f5e6545bc4880efe75eb9af229a455db6017db05c"
10996
11001
  },
10997
11002
  {
10998
11003
  "path": "tests/core_upgrade_runner.mjs",
10999
11004
  "mode": "0000644",
11000
- "sha256": "f8bc5b5329c4de27b41a9172e6b5023c4854d864f532aac3ed062e0d35633f00"
11005
+ "sha256": "85bc665f5d80ff1c7e8134e6be7dd8fa336b4fb50344164b034b0e72a963f04d"
11001
11006
  },
11002
11007
  {
11003
11008
  "path": "tests/cosmos_token_contrast.mjs",
@@ -11142,7 +11147,7 @@
11142
11147
  {
11143
11148
  "path": "tests/deploy_page_projects.mjs",
11144
11149
  "mode": "0000644",
11145
- "sha256": "cec0141f75e41dc268d9377db4925d8b8f0f6e9eb9b6b3c6b6c7c1eb07e75ac8"
11150
+ "sha256": "5c582192ddd83ef289be692b12d898c8837b54312348751082478d036987a647"
11146
11151
  },
11147
11152
  {
11148
11153
  "path": "tests/design_tokens_sync.mjs",
@@ -7,7 +7,7 @@
7
7
  - **Supersedes in part:** [ADR 0111](0111-instance-hosting-provisioning-module.md) §5/§7 (the "managed layer is greenfield, deferred, spec-first" sequencing)
8
8
  - **Depends on:** [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) (the shapes and the platform this operates)
9
9
  - **Gates:** [task 1004142](https://cloudbongos.com/builders#/task/1004142) (the platform leg) · [task 1001949](https://cloudbongos.com/builders#/task/1001949) (metering) · [task 1001621](https://cloudbongos.com/builders#/task/1001621) (the spend rollup)
10
- - **Filed by this ADR:** [task 1004149](https://cloudbongos.com/builders#/task/1004149) (the $0 cost ledger — decision 6) · [task 1004150](https://cloudbongos.com/builders#/task/1004150) (the subscription roster — decision 5)
10
+ - **Filed by this ADR:** [task 1004148](https://cloudbongos.com/builders#/task/1004148) (the $0 cost ledger — decision 6) · [task 1004150](https://cloudbongos.com/builders#/task/1004150) (the subscription roster — decision 5)
11
11
 
12
12
  ## Context
13
13
 
@@ -63,7 +63,7 @@ This ADR records the direction and files it as [task 1004150](https://cloudbongo
63
63
 
64
64
  `costEstimateUsd` returns **0** for `standalone` — correct when a project was a process on a box we already paid for, and wrong the moment it becomes a ~$14/month Render service. Until it is fixed the fleet cost ledger reports \$0 for every project the platform is about to be billed for, and it reports it with no error.
65
65
 
66
- That is a defect, not a deferral, and it is filed as [task 1004149](https://cloudbongos.com/builders#/task/1004149). The metering and invoicing track ([task 1001949](https://cloudbongos.com/builders#/task/1001949)) and the spend rollup ([task 1001621](https://cloudbongos.com/builders#/task/1001621)) stay separate work, as [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) already says.
66
+ That is a defect, not a deferral, and it is filed as [task 1004148](https://cloudbongos.com/builders#/task/1004148). The metering and invoicing track ([task 1001949](https://cloudbongos.com/builders#/task/1001949)) and the spend rollup ([task 1001621](https://cloudbongos.com/builders#/task/1001621)) stay separate work, as [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) already says.
67
67
 
68
68
  ## Consequences
69
69
 
@@ -416,4 +416,4 @@ This keeps the decision history honest and traceable.
416
416
  | 0322 | [**The five states are derived once, and a remnant needs evidence** ([task 1004103](https://cloudbongos.com/builders#/task/1004103), goal 1000095 — BONGOS-V2). The ideator surface draws a thought as one of five objects — star, protostar, satellite, remnant, cold body — and three surfaces are about to draw them (the room, the descent, the sky). Every fact already existed: `idea_inbox.status` (`core_195`), [ADR 0321](0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md)’s `hasWork` / `share` / `attributable`, and `tasks.goal_id` + `tasks.status`. What was missing was ONE place that reads them: a state inferred in three renderers is three states, and the sky can say a thought went out while the room beside it still calls it alive, with nothing erroring. **Decision:** one pure resolver (`modules/ideas/idea-state.js`), brightness exposed on the `reward` port (ideas may not reach `credit_log` any other way — [ADR 0083](0083-modular-architecture-strangler-build-plan.md) rule 1 — and already declares `reward` in `consumes`), and the ORDER of the tests IS the decision. **MERGED wins over everything:** a folded thought keeps its own money (`trail-size.js` is emphatic) but is no longer its own point in the sky, so letting work win draws one idea twice, beside the thought that absorbed it. **WORK beats a DISCARD:** a cold body “never ignited, so it has no size at all” is a stated PROPERTY, and testing work first makes it true by construction — triage really can discard a duplicate whose twin already routed and paid, and `updateIdea` never touches the ledger. **A REMNANT needs EVIDENCE, never the absence of an answer:** `share: 0` is a confirmed removal, `share: null` is an unreadable one, and ADR 0321 pays for that distinction upstream — so an idea with work and no readable facts is a STAR carrying `attributable: false`, because reading a falsy share as death turns one unmounted module into a sky of hollow rings. **A PROTOSTAR is the absence of WORK, not of a status:** ignition is being PAID for something, so a promoted-but-unshipped idea is still unignited, which is exactly what the nod queue exists for. **Position is a second axis** (`well` / `open-space` / `awaitingNod`) because the design says the object alone cannot carry it, and the nod is asked of EVERY task, not only the one that named the well. **`suggested_goal_id` is deliberately NOT a well** — `projection.js` records it as strictly advisory, the routing hint on a filing that did not convert, so reading it would put a thought in a nod queue with no task to promote; the binding `tasks.goal_id` now rides on both of the trail’s task joins at no extra round trip. The page reads the ledger ONCE: the state attach hands economy back the `byTask` breakdown size just computed, all of them or none (a partial map reads a missing entry as “no work” and draws a paid star as a protostar). An absent `reward` port does not empty the sky — states still resolve, flagged unattributable. Nothing writes; ADR 0313 stands. Rejected: resolving in each renderer; deriving from `share` alone (it merges a protostar with a remnant, the exact BV2.R19 distinction); treating an unreadable share as zero; status-first ordering; reading the hint as a home; a sixth object for “unknown brightness” (the vocabulary is the owner’s and is five — the uncertainty is a flag on the answer, not a new thing in the sky).](0322-a-remnant-needs-evidence-and-merged-wins.md) | ideas / economy |
417
417
  | 0323 | [**Hosting is three shapes, and we are not the landlord** ([task 1004140](https://cloudbongos.com/builders#/task/1004140), goal 1000106 — BONGOS-V2). Five projects ran as one Node process each on the control-plane droplet, about $0.80/month apiece — the cheapest arrangement available, bought with ~4,600 lines of `provision*.js` that write systemd units, create unix accounts, render Caddy snippets, `createdb`, install backup timers and count memory against a ceiling. That is the cost nobody priced: **we are a landlord.** The owner reframed the question — build on what others maintain, and let an owner keep the deployment platform they already use. **Seven decisions.** The shapes become `byo-host` (the owner runs it), `cloud-host` (we stand it up for them) and `dedicated-cloud-host` (the same path, larger plan); `co-tenant` is **retired, not renamed**, because it means *a process on our box from our checkout* — the opposite of bring-your-own-host, so reusing the name would make every existing comment lie. **DigitalOcean hosts hubs only**, which closes [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) Finding 1 by removing its precondition rather than hardening the cookie: with no customer code on a `cloudbongos.com` subdomain there is no tenant to receive the hub session. **Render is the default** — its Postgres carries PITR and automated backups on every paid tier; Fly's managed Postgres starts ~$38/mo and Railway's is explicitly unmanaged, which is the exact responsibility being shed. **Cloudflare is the front door, not the compute**: Workers is a V8-isolate runtime that would require rewriting Express and `pg`, and Containers is built for bursty workloads that sleep, which a hall with background sweeps and a live event stream does not survive. **No deployment-platform adapter is built at all** — their application already deploys itself from the repository, so the platform we do not talk to is the platform we do not have to add. **Secrets: the platform is the system of record and Bongos stores names, never values** — a manifest of name, purpose and environment, no application secret, no platform API token, no "is it set?" probe, because each of those makes us custodian of other companies' credentials for convenience the manifest already delivers; `byo-host` marks the manifest unverifiable rather than opening an exception. **The free tier becomes a capped trial**, since ~$14/project cannot be subsidised indefinitely. Per-project cost rises ~17×, which is the price of not being a landlord and the reason metering stops being deferred work. Mostly a DELETION: [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md)'s supervision seam is not extended (Render supervises its own processes), [ADR 0285](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md) stays true and stops being load-bearing with its queued resize held rather than spent, and the hub's offsite backup becomes the highest-consequence open item. Rejected: AWS/GCP (2–3× the cost for a bare instance, plus an IAM and billing surface to learn — maintenance added where the goal is to remove it); keeping the shared box and fixing only the cookie (cheaper, closes the finding, leaves the landlord runner and answers nothing else); a Vercel/Netlify/Render adapter layer (speculative pluggability); Bongos as the secrets vault pushing values through platform APIs.](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) | hosting / provisioning / cost |
418
418
  | 0324 | [**The public npm CLI channel is retired** ([task 1004144](https://cloudbongos.com/builders#/task/1004144), goal 1000091 — BONGOS-V2). The owner deleted `@cloudbongos/cli` from the registry on 2026-09-22 (`npm view` → E404; the `@cloudbongos` scope is empty). This supersedes [ADR 0258](0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md) and [ADR 0292](0292-both-install-paths-stay-and-vendoring-is-the-production-one.md) §2 — the latter titled "`@cloudbongos/cli` STAYS, on a new justification" and written *because* task 1003872 proposed removing it once the public core flip dissolved its original reason. 0292's measured argument (~0.5 MB front door vs a 46 MB core pull to print a task list) is still true and is NOT what changed; the owner's decision about carrying a public channel at all is. `scripts/gds/build-cli-package.js` + `tests/cli_package.mjs` are deleted, no doc offers `npx @cloudbongos/cli`, and the way in is the hall at `<origin>/builders` or a checkout. The COMPILED binary channel (`build-bongos.js` → `/downloads/bongos/*` via `config/bongos-cli.json`) is a different artifact and is untouched — the same near-twin task 1003889 warned about. Three tests used the package's FILES manifest as a stand-in for "what a terminal user can run" and now read `bin/bongos.js`'s verb table, which is what that set always meant.](0324-the-public-npm-cli-channel-is-retired.md) | distribution / CLI |
419
- | 0325 | [**The managed fleet control plane is the hub we already run** ([task 1001948](https://cloudbongos.com/builders#/task/1001948), goal 1000106 — BONGOS-V2). The spike was written against [ADR 0111](0111-instance-hosting-provisioning-module.md) §5/§7, which deferred the managed offering as **"genuinely greenfield (no fleet registry exists)"** behind two gates — Mercury and a real `bongos upgrade`. Both gates are shipped and **the premise is not**: measuring the call sites first changed the answer, as it did for [task 1004141](https://cloudbongos.com/builders#/task/1004141). The registry is `provisioning_instances` (owner, shape, domain, database, port, lifecycle, cost estimate, module selection, the liveness overlay and the served/available core-version snapshot), with `provisioning_intents` as the queue and `provisioning_events` as the audit trail; the operator is `scripts/gds/provision.js`, the single token-holder draining that queue behind ADR 0111 §2's trust boundary; oversight is `GET /provisioning/fleet` and `/fleet/cost-ledger`; and the update channel is not one mechanism but two shipped ones — the owner's per-instance deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md), health-gated with auto-rollback) and the opt-in patch-only sweep ([ADR 0136](0136-update-channel-subscription-policy.md)). **Decision: there is no greenfield fleet control plane to design, and none will be built — the hub IS it.** It never needed to sit *above* the per-repo databases [ADR 0100](0100-two-repo-instance-model.md) §3 locks: the roster is a hub table and the operator has never read one. The genuinely missing leg is a **platform leg** — `grep -rl RENDER_API scripts modules src` returns nothing, and after [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) a `cloud-host` is a Render service — and this ADR **deliberately specifies none of it**, because that is [task 1004142](https://cloudbongos.com/builders#/task/1004142)'s with those files open and a second spec for one implementation is how two drift. The fleet **driven** and the fleet **observed** are different populations: `byo-host` is in the registry and operated by nobody, its liveness the [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull, so every operator surface must state which it counts — a roster that mixes them reports a fleet-wide upgrade as complete while an unknown number were never asked. The operator holds **exactly one** credential and it is **its own** (the platform account token, root-only on the control plane, never the web tier, never per-customer); ADR 0323 D6's "names, never values" rule is about the CUSTOMER's secrets and is unchanged. Two gaps are filed rather than fixed here: the subscription roster living in `config/update-subscriptions.json` is the one fleet fact the roster route cannot report and a restored backup can lose independently of the rows it describes ([task 1004150](https://cloudbongos.com/builders#/task/1004150), a recommendation and not a ruling), and **`costEstimateUsd` returns 0 for every shape but `dedicated`** — measured live, all 24 fleet rows report `$0`, confidently and with no error, for projects about to cost ~\$14/month each ([task 1004149](https://cloudbongos.com/builders#/task/1004149)). **No fleet-wide "upgrade everything" button**: both channels are per-instance by construction, and one action that moves the whole fleet is the shape most likely to take every customer down at once. AGPL posture unchanged — ADR 0111 §5's one-sentence rule survives its own table. Rejected: building the separate control plane §5 described (a second registry and a second operator holding the same rows with nothing keeping them equal); specifying Render here; reading per-instance databases for fleet reporting; and continuing to defer the whole layer, which is now the reason the ledger reads \$0 for a fleet about to cost real money.](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) | hosting / fleet / provisioning |
419
+ | 0325 | [**The managed fleet control plane is the hub we already run** ([task 1001948](https://cloudbongos.com/builders#/task/1001948), goal 1000106 — BONGOS-V2). The spike was written against [ADR 0111](0111-instance-hosting-provisioning-module.md) §5/§7, which deferred the managed offering as **"genuinely greenfield (no fleet registry exists)"** behind two gates — Mercury and a real `bongos upgrade`. Both gates are shipped and **the premise is not**: measuring the call sites first changed the answer, as it did for [task 1004141](https://cloudbongos.com/builders#/task/1004141). The registry is `provisioning_instances` (owner, shape, domain, database, port, lifecycle, cost estimate, module selection, the liveness overlay and the served/available core-version snapshot), with `provisioning_intents` as the queue and `provisioning_events` as the audit trail; the operator is `scripts/gds/provision.js`, the single token-holder draining that queue behind ADR 0111 §2's trust boundary; oversight is `GET /provisioning/fleet` and `/fleet/cost-ledger`; and the update channel is not one mechanism but two shipped ones — the owner's per-instance deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md), health-gated with auto-rollback) and the opt-in patch-only sweep ([ADR 0136](0136-update-channel-subscription-policy.md)). **Decision: there is no greenfield fleet control plane to design, and none will be built — the hub IS it.** It never needed to sit *above* the per-repo databases [ADR 0100](0100-two-repo-instance-model.md) §3 locks: the roster is a hub table and the operator has never read one. The genuinely missing leg is a **platform leg** — `grep -rl RENDER_API scripts modules src` returns nothing, and after [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) a `cloud-host` is a Render service — and this ADR **deliberately specifies none of it**, because that is [task 1004142](https://cloudbongos.com/builders#/task/1004142)'s with those files open and a second spec for one implementation is how two drift. The fleet **driven** and the fleet **observed** are different populations: `byo-host` is in the registry and operated by nobody, its liveness the [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull, so every operator surface must state which it counts — a roster that mixes them reports a fleet-wide upgrade as complete while an unknown number were never asked. The operator holds **exactly one** credential and it is **its own** (the platform account token, root-only on the control plane, never the web tier, never per-customer); ADR 0323 D6's "names, never values" rule is about the CUSTOMER's secrets and is unchanged. Two gaps are filed rather than fixed here: the subscription roster living in `config/update-subscriptions.json` is the one fleet fact the roster route cannot report and a restored backup can lose independently of the rows it describes ([task 1004150](https://cloudbongos.com/builders#/task/1004150), a recommendation and not a ruling), and **`costEstimateUsd` returns 0 for every shape but `dedicated`** — measured live, all 24 fleet rows report `$0`, confidently and with no error, for projects about to cost ~\$14/month each ([task 1004148](https://cloudbongos.com/builders#/task/1004148)). **No fleet-wide "upgrade everything" button**: both channels are per-instance by construction, and one action that moves the whole fleet is the shape most likely to take every customer down at once. AGPL posture unchanged — ADR 0111 §5's one-sentence rule survives its own table. Rejected: building the separate control plane §5 described (a second registry and a second operator holding the same rows with nothing keeping them equal); specifying Render here; reading per-instance databases for fleet reporting; and continuing to defer the whole layer, which is now the reason the ledger reads \$0 for a fleet about to cost real money.](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) | hosting / fleet / provisioning |
@@ -4,13 +4,13 @@
4
4
  > Deterministic on a given tree — regenerate and commit rather than hand-editing.
5
5
  > Machine-readable twin: [`copy-registry.json`](copy-registry.json).
6
6
 
7
- **1922 strings** across 3 surfaces · 1661 distinct pieces of copy · 1922 certain, 0 heuristic · 179 shown in more than one place.
7
+ **1924 strings** across 3 surfaces · 1663 distinct pieces of copy · 1924 certain, 0 heuristic · 179 shown in more than one place.
8
8
 
9
9
  ## Surfaces
10
10
 
11
11
  | surface | lives at | files | strings |
12
12
  |---|---|---|---|
13
- | `builders-hall` | `modules/hall-ui/public` | 92 | 1427 |
13
+ | `builders-hall` | `modules/hall-ui/public` | 92 | 1429 |
14
14
  | `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
15
15
  | `landing` | `modules/public-landing/public` | 5 | 395 |
16
16
 
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
78
78
  | `07f422cbdb7e` | 2x | ) and capture-resistant by design. | modules/public-landing/public/projects.html:1030, modules/public-landing/public/projects.html:1057 |
79
79
  | … | | _139 more in the JSON registry_ | |
80
80
 
81
- ## `builders-hall` — 1427 strings
81
+ ## `builders-hall` — 1429 strings
82
82
 
83
83
  | id | conf | text | where |
84
84
  |---|---|---|---|
@@ -327,7 +327,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
327
327
  | `2366356926b4` | certain | Could not mark the primer just now. Try again. | `modules/hall-ui/public/primer.js:259` |
328
328
  | `753987b5da13` | certain | Could not reach the submit endpoint. | `modules/hall-ui/public/modules.js:297` |
329
329
  | `6f500726f0ac` | certain | Could not read the report: {…} | `modules/hall-ui/public/watch.js:640` |
330
- | `21c2f33168a3` | certain | Could not read this project's deploy state: {…} | `modules/hall-ui/public/deploy.js:310` |
330
+ | `21c2f33168a3` | certain | Could not read this project's deploy state: {…} | `modules/hall-ui/public/deploy.js:322` |
331
331
  | `8b470cebf53d` | certain | Could not remove your key. Try again. | `modules/hall-ui/public/settings.js:1133` |
332
332
  | `4cf6bc8b377b` | certain | Could not remove. Try again. | `modules/hall-ui/public/settings.js:1132` |
333
333
  | `7a778d188e87` | certain | Could not remove: {…} | `modules/hall-ui/public/government.js:369` |
@@ -376,6 +376,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
376
376
  | `a55e423263f0` | certain | Cross-project record | `modules/hall-ui/public/profile.html:94` |
377
377
  | `a55e423263f0` | certain | Cross-project record | `modules/hall-ui/public/settings.html:95` |
378
378
  | `24013a074724` | certain | Custom rank | `modules/hall-ui/public/government.html:113` |
379
+ | `cf7d7d5d5e83` | certain | Database behind. | `modules/hall-ui/public/deploy.js:286` |
379
380
  | `6744aed3d15e` | certain | Dated exploit-detail engagement reports — kept out of the repo (threat-model §6.3) and served only to Archons. | `modules/hall-ui/public/watch.html:131` |
380
381
  | `7c6891813679` | certain | Default ({…}) | `modules/hall-ui/public/settings.js:63` |
381
382
  | `7ec7dc1adaa6` | certain | Deploy — {{worldName}} | `modules/hall-ui/public/deploy.html:6` |
@@ -639,7 +640,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
639
640
  | `f14a91b3f625` | certain | Most recent first. Each row is one recorded work session; every id on it opens its record. | `modules/hall-ui/public/sessions.html:56` |
640
641
  | `e52ae74f0052` | certain | Move to goal | `modules/hall-ui/public/task.js:581` |
641
642
  | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:269` |
642
- | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:371` |
643
+ | `e77d5f3beaef` | certain | Move to {…} | `modules/hall-ui/public/deploy.js:383` |
643
644
  | `393139f648b8` | certain | Moving to core | `modules/hall-ui/public/deploy.js:140` |
644
645
  | `ae84ebe1d735` | certain | Name cannot be blank. | `modules/hall-ui/public/settings.js:761` |
645
646
  | `31e91ee6ce03` | certain | Name must be {…} characters or fewer. | `modules/hall-ui/public/settings.js:766` |
@@ -676,7 +677,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
676
677
  | `2a6d7552e21f` | certain | No other sessions. | `modules/hall-ui/public/settings-sessions.js:64` |
677
678
  | `9f73b3f3dad0` | certain | No rank floor — any builder can claim this task | `modules/hall-ui/public/task-lib.js:62` |
678
679
  | `3fd0c6fcb92e` | certain | No request and no invite needed. Sign in with GitHub, and a step-by-step guide takes you the rest of the way. | `modules/hall-ui/public/hall-render.js:277` |
679
- | `2947c1ddea9a` | certain | No running box to deploy. | `modules/hall-ui/public/deploy.js:348` |
680
+ | `2947c1ddea9a` | certain | No running box to deploy. | `modules/hall-ui/public/deploy.js:360` |
680
681
  | `9fb9f766450d` | certain | No sections yet | `modules/hall-ui/public/primer.js:130` |
681
682
  | `bef9fe43ba84` | certain | No sessions recorded yet. They land here as builders ship and end sessions. | `modules/hall-ui/public/sessions.js:146` |
682
683
  | `4485497d87bb` | certain | No specialities yet. Create one below — it takes a name and a craft, and you can train it afterwards. | `modules/hall-ui/public/settings-specialities.js:169` |
@@ -708,7 +709,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
708
709
  | `fec5d78497e8` | certain | Nothing in the backlog right now. | `modules/hall-ui/public/work.js:1107` |
709
710
  | `905e99f158c2` | certain | Nothing is discarded by omission: every task needs a decision before this can be submitted. Abandoning stamps | `modules/hall-ui/public/goals-render.js:1012` |
710
711
  | `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:843` |
711
- | `9ac63d97239a` | certain | Nothing newer is published that this box can take. There is nothing to deploy. | `modules/hall-ui/public/deploy.js:292` |
712
+ | `9ac63d97239a` | certain | Nothing newer is published that this box can take. There is nothing to deploy. | `modules/hall-ui/public/deploy.js:303` |
712
713
  | `073d7fb013ae` | certain | Nothing of yours is {…} right now. | `modules/hall-ui/public/thinking.js:311` |
713
714
  | `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/profile.js:242` |
714
715
  | `ad9a17a8a5eb` | certain | Nothing shipped yet. | `modules/hall-ui/public/work.js:1068` |
@@ -899,7 +900,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
899
900
  | `7e1d54119e08` | certain | Select task {…} for bulk release | `modules/hall-ui/public/work.js:242` |
900
901
  | `ae43a132699b` | certain | Selectable today | `modules/hall-ui/public/government.js:799` |
901
902
  | `53cc6d8dd23c` | certain | Send invitation | `modules/hall-ui/public/goals-render.js:1211` |
902
- | `a39b0f9d86e5` | certain | Serving {…} — read from the box itself, {…}. | `modules/hall-ui/public/deploy.js:295` |
903
+ | `a39b0f9d86e5` | certain | Serving {…} — read from the box itself, {…}. | `modules/hall-ui/public/deploy.js:306` |
903
904
  | `64a1f055129e` | certain | Session card richness | `modules/hall-ui/public/settings-render.js:83` |
904
905
  | `d14e8e9acaff` | certain | Session ended. | `modules/hall-ui/public/settings-sessions.js:81` |
905
906
  | `55e0a34bafed` | certain | Session log{…} | `modules/hall-ui/public/task.js:487` |
@@ -954,7 +955,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
954
955
  | `ec1c2dae4a90` | certain | Standing there | `modules/hall-ui/public/profile.js:314` |
955
956
  | `eaad12cacb9d` | certain | Start over | `modules/hall-ui/public/studio.js:235` |
956
957
  | `efe530c0e3a7` | certain | Still open | `modules/hall-ui/public/profile.js:494` |
957
- | `490a3cbb8c1c` | certain | Still working on core {…}. It keeps going without this page — reload to see where it got to. | `modules/hall-ui/public/deploy.js:407` |
958
+ | `490a3cbb8c1c` | certain | Still working on core {…}. It keeps going without this page — reload to see where it got to. | `modules/hall-ui/public/deploy.js:419` |
958
959
  | `9a4ced1afb6c` | certain | Stop waiting on task {…} | `modules/hall-ui/public/task.js:278` |
959
960
  | `b0861c874a2f` | certain | Stopped using it. | `modules/hall-ui/public/settings-specialities.js:200` |
960
961
  | `6c6544f5c011` | certain | Stored encrypted; never shown again after saving — only its last four digits. | `modules/hall-ui/public/settings.html:193` |
@@ -1082,7 +1083,8 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1082
1083
  | `9a685b01e2c6` | certain | This page opens at a higher rank. | `modules/hall-ui/public/government.html:37` |
1083
1084
  | `9a685b01e2c6` | certain | This page opens at a higher rank. | `modules/hall-ui/public/project-settings.html:36` |
1084
1085
  | `9a685b01e2c6` | certain | This page opens at a higher rank. | `modules/hall-ui/public/watch.html:40` |
1085
- | `f93fcb24c6ba` | certain | This platform | `modules/hall-ui/public/deploy.js:340` |
1086
+ | `f93fcb24c6ba` | certain | This platform | `modules/hall-ui/public/deploy.js:352` |
1087
+ | `3c8aaec39c40` | certain | This project's code expects {…} database change{…} that its database has not applied yet. Anything that relies | `modules/hall-ui/public/deploy.js:286` |
1086
1088
  | `6e9dfe1fb903` | certain | This restarts {…} onto core | `modules/hall-ui/public/deploy.js:269` |
1087
1089
  | `69e8beced2ff` | certain | This sends the idea back to its author. Say why, and which section. | `modules/hall-ui/public/board-room.js:363` |
1088
1090
  | `09256febc32d` | certain | This sentence has {…} {…} where a value is dropped in when it is shown. | `modules/hall-ui/public/copy-desk.js:422` |
@@ -1348,7 +1350,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
1348
1350
  | `2fb1dad96280` | certain | pending, on the People tab | `modules/hall-ui/public/watch.html:46` |
1349
1351
  | `3b22c5af029b` | certain | rank or above, or that it is your own. | `modules/hall-ui/public/copy-desk.html:47` |
1350
1352
  | `80ddf28baee7` | certain | rank-key (kebab-case) | `modules/hall-ui/public/government.html:82` |
1351
- | `41a4b66c21e5` | certain | reads the control plane's own observation · ADR 0293 · every move is recorded with who asked | `modules/hall-ui/public/deploy.js:352` |
1353
+ | `41a4b66c21e5` | certain | reads the control plane's own observation · ADR 0293 · every move is recorded with who asked | `modules/hall-ui/public/deploy.js:364` |
1352
1354
  | `f83f0a9d6b6a` | certain | recorded, on the Operations tab | `modules/hall-ui/public/watch.html:49` |
1353
1355
  | `26da1df6fd4a` | certain | reflects it. | `modules/hall-ui/public/settings.html:136` |
1354
1356
  | `d4bb81532896` | certain | repro steps, context, anything that helps… | `modules/hall-ui/public/ideas.html:84` |
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/gds/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 1922,
7
- "unique_ids": 1661,
8
- "certain": 1922,
6
+ "strings": 1924,
7
+ "unique_ids": 1663,
8
+ "certain": 1924,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 179
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 92,
16
- "strings": 1427
16
+ "strings": 1429
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -1067,7 +1067,7 @@
1067
1067
  "occurrences": 2,
1068
1068
  "at": [
1069
1069
  "modules/hall-ui/public/deploy.js:269",
1070
- "modules/hall-ui/public/deploy.js:371"
1070
+ "modules/hall-ui/public/deploy.js:383"
1071
1071
  ]
1072
1072
  },
1073
1073
  {
@@ -4122,7 +4122,7 @@
4122
4122
  "surface": "builders-hall",
4123
4123
  "text": "Could not read this project's deploy state: {…}",
4124
4124
  "file": "modules/hall-ui/public/deploy.js",
4125
- "line": 310,
4125
+ "line": 322,
4126
4126
  "origin": "js-markup",
4127
4127
  "confidence": "certain"
4128
4128
  },
@@ -4558,6 +4558,15 @@
4558
4558
  "origin": "html-text",
4559
4559
  "confidence": "certain"
4560
4560
  },
4561
+ {
4562
+ "id": "cf7d7d5d5e83",
4563
+ "surface": "builders-hall",
4564
+ "text": "Database behind.",
4565
+ "file": "modules/hall-ui/public/deploy.js",
4566
+ "line": 286,
4567
+ "origin": "js-markup",
4568
+ "confidence": "certain"
4569
+ },
4561
4570
  {
4562
4571
  "id": "6744aed3d15e",
4563
4572
  "surface": "builders-hall",
@@ -6930,7 +6939,7 @@
6930
6939
  "surface": "builders-hall",
6931
6940
  "text": "Move to {…}",
6932
6941
  "file": "modules/hall-ui/public/deploy.js",
6933
- "line": 371,
6942
+ "line": 383,
6934
6943
  "origin": "js-text-assign",
6935
6944
  "confidence": "certain"
6936
6945
  },
@@ -7263,7 +7272,7 @@
7263
7272
  "surface": "builders-hall",
7264
7273
  "text": "No running box to deploy.",
7265
7274
  "file": "modules/hall-ui/public/deploy.js",
7266
- "line": 348,
7275
+ "line": 360,
7267
7276
  "origin": "js-markup",
7268
7277
  "confidence": "certain"
7269
7278
  },
@@ -7551,7 +7560,7 @@
7551
7560
  "surface": "builders-hall",
7552
7561
  "text": "Nothing newer is published that this box can take. There is nothing to deploy.",
7553
7562
  "file": "modules/hall-ui/public/deploy.js",
7554
- "line": 292,
7563
+ "line": 303,
7555
7564
  "origin": "js-markup",
7556
7565
  "confidence": "certain"
7557
7566
  },
@@ -9270,7 +9279,7 @@
9270
9279
  "surface": "builders-hall",
9271
9280
  "text": "Serving {…} — read from the box itself, {…}.",
9272
9281
  "file": "modules/hall-ui/public/deploy.js",
9273
- "line": 295,
9282
+ "line": 306,
9274
9283
  "origin": "js-markup",
9275
9284
  "confidence": "certain"
9276
9285
  },
@@ -9765,7 +9774,7 @@
9765
9774
  "surface": "builders-hall",
9766
9775
  "text": "Still working on core {…}. It keeps going without this page — reload to see where it got to.",
9767
9776
  "file": "modules/hall-ui/public/deploy.js",
9768
- "line": 407,
9777
+ "line": 419,
9769
9778
  "origin": "js-notice",
9770
9779
  "confidence": "certain"
9771
9780
  },
@@ -10917,7 +10926,16 @@
10917
10926
  "surface": "builders-hall",
10918
10927
  "text": "This platform",
10919
10928
  "file": "modules/hall-ui/public/deploy.js",
10920
- "line": 340,
10929
+ "line": 352,
10930
+ "origin": "js-markup",
10931
+ "confidence": "certain"
10932
+ },
10933
+ {
10934
+ "id": "3c8aaec39c40",
10935
+ "surface": "builders-hall",
10936
+ "text": "This project's code expects {…} database change{…} that its database has not applied yet. Anything that relies on {…} may fail until the migrations run.",
10937
+ "file": "modules/hall-ui/public/deploy.js",
10938
+ "line": 286,
10921
10939
  "origin": "js-markup",
10922
10940
  "confidence": "certain"
10923
10941
  },
@@ -13311,7 +13329,7 @@
13311
13329
  "surface": "builders-hall",
13312
13330
  "text": "reads the control plane's own observation · ADR 0293 · every move is recorded with who asked",
13313
13331
  "file": "modules/hall-ui/public/deploy.js",
13314
- "line": 352,
13332
+ "line": 364,
13315
13333
  "origin": "js-markup",
13316
13334
  "confidence": "certain"
13317
13335
  },
@@ -2386,5 +2386,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2386
2386
  landed since 1.19.954 with no explicit bump. run 35690089430. (task 1002620)
2387
2387
  1.19.956 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2388
2388
  landed since 1.19.955 with no explicit bump. run 35691908420. (task 1002620)
2389
+ 1.19.957 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2390
+ landed since 1.19.956 with no explicit bump. run 35692861666. (task 1002620)
2391
+ 1.19.958 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2392
+ landed since 1.19.957 with no explicit bump. run 35735231183. (task 1002620)
2389
2393
  ---------------------------------------------------------------------------
2390
2394
  ```
@@ -277,6 +277,17 @@
277
277
  </div>`;
278
278
  }
279
279
 
280
+ // "Database behind" (task 1004153): the box's code ships migrations its database has not
281
+ // applied, e.g. after an upgrade with --skip-migrate. Silent at 0 AND at null — null is an
282
+ // unknown reading, and the no-answer note above already speaks for a box that did not answer.
283
+ function schemaBehindHtml(p) {
284
+ const n = p.schema_pending;
285
+ if (!Number.isInteger(n) || n <= 0) return '';
286
+ return `<p class="ps-invalid"><strong>Database behind.</strong> This project's code expects
287
+ ${escapeHtml(String(n))} database change${n === 1 ? '' : 's'} that its database has not applied yet.
288
+ Anything that relies on ${n === 1 ? 'it' : 'them'} may fail until the migrations run.</p>`;
289
+ }
290
+
280
291
  // One box's card. `isSelf` only changes the words, never what is offered: the runner
281
292
  // treats the platform's own row and a tenant's identically, and a page that implied
282
293
  // otherwise would be describing a rule that does not exist.
@@ -299,6 +310,7 @@
299
310
  ${p.reading === 'no_answer'
300
311
  ? `<p class="ps-invalid">The box did not answer when the control plane last looked, so this version is unknown rather than unchanged. A move is not offered while that is true.</p>`
301
312
  : ''}
313
+ ${schemaBehindHtml(p)}
302
314
  ${action}
303
315
  </div></li>`;
304
316
  }
@@ -259,6 +259,9 @@ function previewFromSnapshot(inst, snap, openIntent, lastMove) {
259
259
  // Named so the UI cannot accidentally render "up to date" over a missing reading.
260
260
  reading: served ? 'ok' : 'no_answer',
261
261
  observed_at: (snap && snap.observed_at) || null,
262
+ // Migrations the box's code ships that its database has not applied (task 1004153).
263
+ // null is UNKNOWN, not zero — an old core, or a box that did not answer.
264
+ schema_pending: snap && Number.isInteger(snap.schema_pending) ? snap.schema_pending : null,
262
265
  // What the upgrade SAYS it would do, from the runner's dry run (task 1004121).
263
266
  // Withheld unless it was taken against the move now on offer: a verdict about
264
267
  // 1.19.920 tells the owner nothing true about pressing a button that says 1.19.921,
@@ -0,0 +1,17 @@
1
+ -- provisioning_024 — each box's schemaPending in the core-version snapshot (task 1004153,
2
+ -- from idea 1001088).
3
+ --
4
+ -- /version reports `schemaPending`: core + enabled-module migrations the running code ships
5
+ -- that the instance's own database has not recorded. Only the instance a checkout talks to
6
+ -- was ever watched for it (the SessionStart divergence line); co-tenants were not. A
7
+ -- co-tenant upgraded with --skip-migrate (mercury has no scripts.migrate) serves new code
8
+ -- over an old schema and nothing said so. The runner already reads each box's loopback
9
+ -- /version on every tick for served_version; this stores the other half of that answer.
10
+ --
11
+ -- NULL means UNKNOWN — the box did not answer, or runs a core older than the field — and
12
+ -- is a different state from 0. The control must not render one as the other.
13
+ --
14
+ -- Additive, idempotent, no down-migration.
15
+
16
+ ALTER TABLE provisioning_core_version_snapshot
17
+ ADD COLUMN IF NOT EXISTS schema_pending integer;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.956",
3
+ "version": "1.19.958",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.956",
9
+ "version": "1.19.958",
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.956",
3
+ "version": "1.19.958",
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",
@@ -72,6 +72,21 @@ function parseServedVersion(stdout) {
72
72
  return m ? m[0] : null;
73
73
  }
74
74
 
75
+ /**
76
+ * The `schemaPending` count from a /version body — migrations the box's code ships that its
77
+ * own database has not recorded (task 1004153). PURE.
78
+ *
79
+ * null means UNKNOWN (no answer, not JSON, or a core older than the field), never 0: a
80
+ * co-tenant that cannot say must not read as one whose database is current. Bounded the
81
+ * same way scripts/gds/deploy-divergence.js bounds it, since the body is a network answer.
82
+ */
83
+ function parseSchemaPending(stdout) {
84
+ if (!stdout || typeof stdout !== 'string') return null;
85
+ let n;
86
+ try { n = JSON.parse(stdout).schemaPending; } catch { return null; }
87
+ return Number.isInteger(n) && n >= 0 && n <= 100000 ? n : null;
88
+ }
89
+
75
90
  /**
76
91
  * Is this target a version the instance may actually be moved to? PURE.
77
92
  *
@@ -466,7 +481,9 @@ async function snapshotCoreVersions(inst, deps) {
466
481
  const instanceDir = inst.hosting_shape === 'standalone' ? standaloneRoot(inst) : selfRoot();
467
482
  try {
468
483
  const probe = boxExec(versionCmd(inst), { allowFail: true });
469
- const served = parseServedVersion(probe && !probe.softFailed ? (probe.stdout || '') : null);
484
+ const body = probe && !probe.softFailed ? (probe.stdout || '') : null;
485
+ const served = parseServedVersion(body);
486
+ const schemaPending = parseSchemaPending(body);
470
487
  const listed = channelMod.listAvailableVersions({ cwd: instanceDir });
471
488
  const available = (listed && listed.versions) || [];
472
489
  // What the channel would take FROM the version the box is actually on. Resolving it
@@ -486,18 +503,19 @@ async function snapshotCoreVersions(inst, deps) {
486
503
  const preflight = null;
487
504
  await db.query(
488
505
  `INSERT INTO provisioning_core_version_snapshot
489
- (instance_id, served_version, available, recommended, observed_at, preflight, preflight_at)
490
- VALUES ($1, $2, $3::jsonb, $4, now(), $5::jsonb, $6)
506
+ (instance_id, served_version, available, recommended, observed_at, preflight, preflight_at, schema_pending)
507
+ VALUES ($1, $2, $3::jsonb, $4, now(), $5::jsonb, $6, $7)
491
508
  ON CONFLICT (instance_id) DO UPDATE
492
509
  SET served_version = EXCLUDED.served_version, available = EXCLUDED.available,
493
510
  recommended = EXCLUDED.recommended, observed_at = now(),
511
+ schema_pending = EXCLUDED.schema_pending,
494
512
  -- A tick with nothing to preview must not ERASE the last verdict; it simply
495
513
  -- has nothing newer to say. Only a real dry run overwrites one.
496
514
  preflight = COALESCE(EXCLUDED.preflight, provisioning_core_version_snapshot.preflight),
497
515
  preflight_at = COALESCE(EXCLUDED.preflight_at, provisioning_core_version_snapshot.preflight_at)`,
498
516
  [inst.id, served, JSON.stringify(available), recommended || null,
499
- preflight ? JSON.stringify(preflight) : null, preflight ? new Date() : null]);
500
- return { ok: true, served, available: available.length, preflight };
517
+ preflight ? JSON.stringify(preflight) : null, preflight ? new Date() : null, schemaPending]);
518
+ return { ok: true, served, available: available.length, preflight, schemaPending };
501
519
  } catch (e) {
502
520
  log(` ~ core-version snapshot for ${inst.slug} skipped (${(e && e.message) || e})`);
503
521
  return { ok: true, skipped: true };
@@ -519,4 +537,4 @@ async function snapshotAllCoreVersions(deps) {
519
537
  return { seen };
520
538
  }
521
539
 
522
- module.exports = { UPGRADE_SHAPES, resolveRunAs, versionCmd, parseServedVersion, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
540
+ module.exports = { UPGRADE_SHAPES, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.956'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.958'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -404,6 +404,15 @@ await t('no snapshot at all reports observed_at:null rather than a confident-loo
404
404
  assert.equal(p.available_count, 0);
405
405
  });
406
406
 
407
+ await t('schema_pending passes through as a count, and UNKNOWN stays null rather than reading as 0 (task 1004153)', () => {
408
+ const snap = { served_version: '1.19.758', available: [], recommended: null, observed_at: 'x' };
409
+ assert.equal(CU.previewFromSnapshot(previewInst, { ...snap, schema_pending: 3 }, null).schema_pending, 3);
410
+ assert.equal(CU.previewFromSnapshot(previewInst, { ...snap, schema_pending: 0 }, null).schema_pending, 0);
411
+ assert.equal(CU.previewFromSnapshot(previewInst, { ...snap, schema_pending: null }, null).schema_pending, null);
412
+ assert.equal(CU.previewFromSnapshot(previewInst, snap, null).schema_pending, null, 'a row written before provisioning_024');
413
+ assert.equal(CU.previewFromSnapshot(previewInst, undefined, null).schema_pending, null, 'no snapshot at all');
414
+ });
415
+
407
416
  await t('an open intent is surfaced with its target, so the page can say what is already running', () => {
408
417
  const p = CU.previewFromSnapshot(previewInst, { served_version: '1.19.758', available: [], recommended: null },
409
418
  { action: 'core-upgrade', state: 'running', target_version: '1.19.762' });
@@ -30,6 +30,8 @@ import { strict as assert } from 'node:assert';
30
30
  import { createRequire } from 'node:module';
31
31
  import fs from 'node:fs';
32
32
  import path from 'node:path';
33
+ // An ESM named import, not R.parseSchemaPending: knip credits a CJS export only when a test imports it this way.
34
+ import { parseSchemaPending } from '../scripts/gds/provision-core-upgrade.js';
33
35
 
34
36
  const require = createRequire(import.meta.url);
35
37
  const R = require('../scripts/gds/provision-core-upgrade.js');
@@ -404,6 +406,37 @@ await t('a box that does not answer still writes a snapshot — a NULL reading i
404
406
  assert.equal(w.params[3], null, 'and nothing is recommended against a version we could not read');
405
407
  });
406
408
 
409
+ await t('parseSchemaPending reads the count, and anything it cannot trust is null — never 0 (task 1004153)', () => {
410
+ assert.equal(parseSchemaPending(JSON.stringify({ coreVersion: '1.19.953', schemaPending: 3 })), 3);
411
+ assert.equal(parseSchemaPending(JSON.stringify({ coreVersion: '1.19.953', schemaPending: 0 })), 0);
412
+ assert.equal(parseSchemaPending(JSON.stringify({ coreVersion: '1.19.700' })), null, 'a core older than the field');
413
+ assert.equal(parseSchemaPending(JSON.stringify({ schemaPending: null })), null, 'the box itself does not know');
414
+ for (const bad of [-1, 1.5, '3', 100001]) {
415
+ assert.equal(parseSchemaPending(JSON.stringify({ schemaPending: bad })), null, `schemaPending=${JSON.stringify(bad)}`);
416
+ }
417
+ assert.equal(parseSchemaPending('cloudbongos 1.19.762 (ok)'), null, 'not JSON');
418
+ assert.equal(parseSchemaPending(''), null);
419
+ assert.equal(parseSchemaPending(null), null);
420
+ });
421
+
422
+ await t('the snapshot stores the box\'s schemaPending next to its served version (task 1004153)', async () => {
423
+ const queries = [];
424
+ const d = makeDeps({ queries });
425
+ d.exec = (cmd) => ({ ok: true, stdout: /\/version/.test(cmd) ? JSON.stringify({ coreVersion: '1.19.760', schemaPending: 4 }) : '' });
426
+ const r = await R.snapshotCoreVersions(inst(), d);
427
+ const w = queries.find((q) => /provisioning_core_version_snapshot/.test(q.sql));
428
+ assert.match(w.sql, /schema_pending = EXCLUDED\.schema_pending/, 'a re-read replaces the old count');
429
+ assert.equal(w.params[6], 4);
430
+ assert.equal(r.schemaPending, 4);
431
+ });
432
+
433
+ await t('a box that does not answer stores schema_pending NULL, not a clean 0', async () => {
434
+ const queries = [];
435
+ await R.snapshotCoreVersions(inst(), makeDeps({ served: null, queries }));
436
+ const w = queries.find((q) => /provisioning_core_version_snapshot/.test(q.sql));
437
+ assert.equal(w.params[6], null);
438
+ });
439
+
407
440
  await t('a shape the runner cannot reach is skipped entirely — no snapshot, no claim about it', async () => {
408
441
  const queries = [];
409
442
  const r = await R.snapshotCoreVersions(inst({ hosting_shape: 'dedicated' }), makeDeps({ queries }));
@@ -181,6 +181,20 @@ test('a box that did not answer is never rendered as up to date', async () => {
181
181
  assert.doesNotMatch(html, /id="dp-preview-1"/, 'and is offered no move');
182
182
  });
183
183
 
184
+ test('a box whose database is behind its code says so (task 1004153)', async () => {
185
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { schema_pending: 2 }) });
186
+ await b.settle();
187
+ assert.match(b.panel(), /Database behind\.<\/strong> This project's code expects\s+2 database changes/);
188
+ });
189
+
190
+ test('a database that is current, or whose state is unknown, gets no "behind" flag', async () => {
191
+ for (const schema_pending of [0, null, undefined]) {
192
+ const b = boot({ controlPlane: preview('1', 'cloudbongos', { schema_pending }) });
193
+ await b.settle();
194
+ assert.doesNotMatch(b.panel(), /Database behind/, `schema_pending=${schema_pending}`);
195
+ }
196
+ });
197
+
184
198
  test('an unenrolled control plane still lets the projects through', async () => {
185
199
  // The regression this pins: the platform's own missing row used to be the whole
186
200
  // page, so one un-run setup command would hide every project too.