@bongos/core 1.19.944 → 1.19.946
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 +24 -19
- package/docs/adr/0111-instance-hosting-provisioning-module.md +2 -1
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -1
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +96 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +1 -1
- package/scripts/gds/scope-map-guard.js +181 -1
- package/src/bongos/module-scope-map.js +67 -0
- package/src/module-api.js +1 -1
- package/tests/scope_map_guard.mjs +101 -1
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.946",
|
|
6
|
+
"core_contract": "1.19.946",
|
|
7
|
+
"source_commit": "427f912951779422ac61981f8d918086a2f0cc65",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-21T23:49:20.517Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 512,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
14
|
"functional_verbatim": 2340,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2878,
|
|
20
|
+
"tree_sha256": "3a0e9a1d401c4698c1d6c8fd6a429f0f65a2beabf547c47a910f336148776ac6",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -1027,7 +1027,7 @@
|
|
|
1027
1027
|
{
|
|
1028
1028
|
"path": "docs/adr/0111-instance-hosting-provisioning-module.md",
|
|
1029
1029
|
"mode": "0000644",
|
|
1030
|
-
"sha256": "
|
|
1030
|
+
"sha256": "d216d4cfb0f269afa00af7963b3fe16d97596a6407b26859c7fd3126c7fc518c"
|
|
1031
1031
|
},
|
|
1032
1032
|
{
|
|
1033
1033
|
"path": "docs/adr/0112-private-goals-invitations-join-requests.md",
|
|
@@ -1192,7 +1192,7 @@
|
|
|
1192
1192
|
{
|
|
1193
1193
|
"path": "docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md",
|
|
1194
1194
|
"mode": "0000644",
|
|
1195
|
-
"sha256": "
|
|
1195
|
+
"sha256": "74f14a90b88d585a4b50247a7dd54a01accc229e7f64a875c9491612e3d0dd3c"
|
|
1196
1196
|
},
|
|
1197
1197
|
{
|
|
1198
1198
|
"path": "docs/adr/0146-configurable-reward-mode-cost-plus-only.md",
|
|
@@ -1917,7 +1917,7 @@
|
|
|
1917
1917
|
{
|
|
1918
1918
|
"path": "docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md",
|
|
1919
1919
|
"mode": "0000644",
|
|
1920
|
-
"sha256": "
|
|
1920
|
+
"sha256": "4e1631b94e83af33a8ea9ebf51657e0a5fcb0b9da30a41d443e7caa8f6b3c35b"
|
|
1921
1921
|
},
|
|
1922
1922
|
{
|
|
1923
1923
|
"path": "docs/adr/0286-a-gated-project-s-manifest-tells-a-stranger-how-to-sign-in-and-nothing-else.md",
|
|
@@ -2104,10 +2104,15 @@
|
|
|
2104
2104
|
"mode": "0000644",
|
|
2105
2105
|
"sha256": "6a4ad58720701e760068d5ce1761733790beef0a142aa31a08782d029cc7a390"
|
|
2106
2106
|
},
|
|
2107
|
+
{
|
|
2108
|
+
"path": "docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md",
|
|
2109
|
+
"mode": "0000644",
|
|
2110
|
+
"sha256": "8fa6ef943e3fc0855d15f83baefaabf810994b80da2ecdbbcc28d752ed0498f3"
|
|
2111
|
+
},
|
|
2107
2112
|
{
|
|
2108
2113
|
"path": "docs/adr/README.md",
|
|
2109
2114
|
"mode": "0000644",
|
|
2110
|
-
"sha256": "
|
|
2115
|
+
"sha256": "eef598f074ff3e22a4748b9092eb83d04235205c34e66db6bbe76547f050843d"
|
|
2111
2116
|
},
|
|
2112
2117
|
{
|
|
2113
2118
|
"path": "docs/api-reference.md",
|
|
@@ -2677,7 +2682,7 @@
|
|
|
2677
2682
|
{
|
|
2678
2683
|
"path": "docs/module-api-changelog.md",
|
|
2679
2684
|
"mode": "0000644",
|
|
2680
|
-
"sha256": "
|
|
2685
|
+
"sha256": "3be3dcca6eabd22bc94ad8c59edb8fcc56bc14cf46cbd6d315807b2247d5a636"
|
|
2681
2686
|
},
|
|
2682
2687
|
{
|
|
2683
2688
|
"path": "docs/modules-contract.md",
|
|
@@ -8002,12 +8007,12 @@
|
|
|
8002
8007
|
{
|
|
8003
8008
|
"path": "package-lock.json",
|
|
8004
8009
|
"mode": "0000644",
|
|
8005
|
-
"sha256": "
|
|
8010
|
+
"sha256": "d9bcde780bfceb4b3f8c2c818db7552a5b21cd0280e123f35a81f71032234e8f"
|
|
8006
8011
|
},
|
|
8007
8012
|
{
|
|
8008
8013
|
"path": "package.json",
|
|
8009
8014
|
"mode": "0000644",
|
|
8010
|
-
"sha256": "
|
|
8015
|
+
"sha256": "289ec4343c26cd76b78320bdacd76382bfd5e4ef75670c307ff9fb400e866de3"
|
|
8011
8016
|
},
|
|
8012
8017
|
{
|
|
8013
8018
|
"path": "public-docs/index.html",
|
|
@@ -8592,7 +8597,7 @@
|
|
|
8592
8597
|
{
|
|
8593
8598
|
"path": "scripts/gds/fitness.js",
|
|
8594
8599
|
"mode": "0000644",
|
|
8595
|
-
"sha256": "
|
|
8600
|
+
"sha256": "35e377253453c975ce7a8e420d72af718a4f8ab6d3837d39fd38c1ebd5658ddc"
|
|
8596
8601
|
},
|
|
8597
8602
|
{
|
|
8598
8603
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -9122,7 +9127,7 @@
|
|
|
9122
9127
|
{
|
|
9123
9128
|
"path": "scripts/gds/scope-map-guard.js",
|
|
9124
9129
|
"mode": "0000644",
|
|
9125
|
-
"sha256": "
|
|
9130
|
+
"sha256": "39554a1c45f9d4c507df8d782cf2d928adae312110ba9a057421354d5999dc69"
|
|
9126
9131
|
},
|
|
9127
9132
|
{
|
|
9128
9133
|
"path": "scripts/gds/seam-wiring-guard.js",
|
|
@@ -9817,7 +9822,7 @@
|
|
|
9817
9822
|
{
|
|
9818
9823
|
"path": "src/bongos/module-scope-map.js",
|
|
9819
9824
|
"mode": "0000644",
|
|
9820
|
-
"sha256": "
|
|
9825
|
+
"sha256": "36da288c8087ebd653c0b208a02414ea55d09da53df22cb4fc3d281941756ec7"
|
|
9821
9826
|
},
|
|
9822
9827
|
{
|
|
9823
9828
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -10012,7 +10017,7 @@
|
|
|
10012
10017
|
{
|
|
10013
10018
|
"path": "src/module-api.js",
|
|
10014
10019
|
"mode": "0000644",
|
|
10015
|
-
"sha256": "
|
|
10020
|
+
"sha256": "4301e3d88d415bed2d9146b4bd1749be2341355554f78e441793ebca16d9e24a"
|
|
10016
10021
|
},
|
|
10017
10022
|
{
|
|
10018
10023
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13467,7 +13472,7 @@
|
|
|
13467
13472
|
{
|
|
13468
13473
|
"path": "tests/scope_map_guard.mjs",
|
|
13469
13474
|
"mode": "0000644",
|
|
13470
|
-
"sha256": "
|
|
13475
|
+
"sha256": "9722a12a61597d86f033d3f3a8cd93a7e99c701da54f79c8f6ac49afdea62ad3"
|
|
13471
13476
|
},
|
|
13472
13477
|
{
|
|
13473
13478
|
"path": "tests/scouting_owner_access_and_reach.mjs",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# ADR 0111 — Instance hosting + domains as a Cloud Bongos module: the `provisioning` module boundary + the self-host / managed split
|
|
2
2
|
|
|
3
|
-
- **Status:** Accepted (
|
|
3
|
+
- **Status:** Accepted, **superseded in part by [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md)** (2026-09-21) — the module boundary and the enqueue-only trust boundary STAND; the DigitalOcean droplet and `co-tenant` shapes it provisions do not. Customer projects move to a managed platform; DigitalOcean hosts hubs only.
|
|
4
|
+
- **Status (original):** Accepted (boundary + split decided with the owner; build is task-pending — see §Consequences for the seeded tasks)
|
|
4
5
|
- **Date:** 2026-07-03
|
|
5
6
|
- **Track:** internal (a portable Cloud Bongos platform capability)
|
|
6
7
|
- **Task:** [#1485](https://example.com/builders#/task/1485) (BONGOS-V1, goal 26 — *Cloud Bongos productization and self-host*) — "spec the module boundary + the self-host vs managed split **before** tasking."
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# 0145 — A free "own-repo" hosted project tier needs domain separation + per-project isolation (not a bare cloudbongos.com subdomain)
|
|
2
2
|
|
|
3
|
+
- **Status:** **Superseded in part by [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md)** (2026-09-21). Three of this ADR's decisions are replaced: the free tier becomes a **capped trial**, not all-free; customer projects leave the shared box for a managed platform, so **container isolation on our own box is no longer the plan**; and **Finding 1 is closed by removing its precondition** — with no customer code on a `cloudbongos.com` subdomain there is no tenant to receive the hub session cookie, so the sign-in hardening in the build sequence below is no longer a blocking prerequisite (it remains worth doing on its own merits). The two security FINDINGS below stand as analysis and are the reason the new direction is safe.
|
|
3
4
|
- **Status:** Accepted — owner decided 2026-07-15: **keep `cloudbongos.com`** (no separate domain), **container** isolation, **all-free** for now. Choosing the literal same domain makes hardening core sign-in a required prerequisite (see "Decisions" + "Required build sequence"). Build unblocked ([task 1002251](https://cloudbongos.com/builders#/task/1002251)); blocker 1000080 resolved.
|
|
4
5
|
- **Date:** 2026-07-15
|
|
5
6
|
- **Deciders:** Lars (owner/Archon) chose the direction after a security review; Claude ran the review + designed.
|
package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ADR 0285 — A shared box holds about twelve projects per GB, and memory is the wall
|
|
2
2
|
|
|
3
|
-
- **Status:** accepted
|
|
3
|
+
- **Status:** accepted; **no longer load-bearing as of [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md)** (2026-09-21). The measurements below are correct and still describe the box — it simply stops holding customer projects, so the ceiling no longer gates project creation and the queued resize ([task 1003923](https://cloudbongos.com/builders#/task/1003923)) is held rather than spent.
|
|
4
4
|
- **Date:** 2026-09-15
|
|
5
5
|
- **Task:** [1003877](https://cloudbongos.com/builders#/task/1003877) (goal 1000106), a spike. Informs [1003923](https://cloudbongos.com/builders#/task/1003923) (the resize) and [1001949](https://cloudbongos.com/builders#/task/1001949) (managed billing).
|
|
6
6
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# ADR 0323 — Hosting is three shapes, and we are not the landlord
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-21
|
|
5
|
+
- **Task:** [task 1004140](https://cloudbongos.com/builders#/task/1004140) (goal 1000106 — working area 1, Project creation)
|
|
6
|
+
- **Deciders:** Lars (Archon) — every numbered decision below is the owner's; Claude ran the audit and the platform comparison.
|
|
7
|
+
- **Gates:** [task 1004141](https://cloudbongos.com/builders#/task/1004141) (the rename) · [task 1004142](https://cloudbongos.com/builders#/task/1004142) (the migration) · [task 1004062](https://cloudbongos.com/builders#/task/1004062) (the env manifest)
|
|
8
|
+
- **Supersedes in part:** [ADR 0111](0111-instance-hosting-provisioning-module.md) · [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) · [ADR 0285](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md)
|
|
9
|
+
- **Related:** [ADR 0002](0002-digitalocean-over-hetzner.md) (why DigitalOcean) · [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) (the pull this ADR makes load-bearing for `byo-host`) · [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) (the seam whose second implementation this makes unnecessary)
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
Five active projects run as `hosting_shape='standalone'` on one `s-1vcpu-2gb` DigitalOcean droplet, beside the control plane itself. Each is a Node process and a Postgres database, about 80 MB warm; the box holds fifteen ([ADR 0285](0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md)) at roughly $0.80 per project per month. It is the cheapest arrangement available, and standing it up cost us `scripts/gds/provision*.js` — about 4,600 lines that write systemd units, create unix accounts, render Caddy snippets, run `createdb`, install per-instance backup timers, and count memory against a ceiling.
|
|
14
|
+
|
|
15
|
+
That is the cost nobody priced: we are a landlord. Every one of those legs is infrastructure we maintain so that someone else's code can run on our machine.
|
|
16
|
+
|
|
17
|
+
Three further facts made the arrangement worse than its price suggested:
|
|
18
|
+
|
|
19
|
+
- **Customer code sits on our hub's registrable domain.** [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) Finding 1 established that the hub's session cookie, scoped `.cloudbongos.com`, is sent by the browser to every `*.cloudbongos.com` host — including a tenant's. It required the sign-in hardening to land *before* any tenant went on a subdomain. Two tenants are on subdomains today, one of them not the platform owner's, and the hardening has not shipped.
|
|
20
|
+
- **`dedicated` was written and never used.** A per-project droplet is 15–20× the shared cost, and no project has ever run on one.
|
|
21
|
+
- **The self-host container path kept finding new floors.** [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) made supervision a seam and named three remaining blockers; two shipped, and each existed only because we were building a container host of our own.
|
|
22
|
+
|
|
23
|
+
The owner's direction, settled over 2026-09-20/21, reframed the question. The platform should not host other people's applications at all: it should build on what others maintain, and let the owner keep the deployment platform they already use.
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
### 1. Three owner-facing shapes
|
|
28
|
+
|
|
29
|
+
| Shape | Meaning |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `byo-host` | The owner runs the instance: their machine, their VPS, their own platform account. **New.** |
|
|
32
|
+
| `cloud-host` | We stand it up for them on a managed platform. Rename of `standalone`. |
|
|
33
|
+
| `dedicated-cloud-host` | The same path on a larger plan. Rename of `dedicated`. |
|
|
34
|
+
|
|
35
|
+
`control-plane` is unchanged and stays unrequestable — it is the hub, not a product option.
|
|
36
|
+
|
|
37
|
+
**`co-tenant` is retired, not renamed.** It means *a process on our box, from our checkout* — the most platform-owned shape there is, and the exact opposite of bring-your-own-host. Reusing the name would make every existing comment and ADR reference lie. One row carries it and it is `torn_down`.
|
|
38
|
+
|
|
39
|
+
`dedicated-cloud-host` is a **plan size, not a second code path**. The DigitalOcean droplet and cloud-init implementation that sat behind `dedicated` becomes dead code.
|
|
40
|
+
|
|
41
|
+
### 2. DigitalOcean hosts hubs only
|
|
42
|
+
|
|
43
|
+
No customer project is deployed to it. The control plane stays where it is; the projects leave.
|
|
44
|
+
|
|
45
|
+
This closes [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md) Finding 1 by removing its precondition rather than by hardening the cookie: with no customer code on a `cloudbongos.com` subdomain, there is no tenant to receive the hub session. The cookie hardening remains worth doing on its own merits and is no longer load-bearing.
|
|
46
|
+
|
|
47
|
+
### 3. Render is the default platform for a `cloud-host`
|
|
48
|
+
|
|
49
|
+
Render's Postgres carries point-in-time recovery and automated backups on every paid tier. Roughly $14 per project per month (web service plus database).
|
|
50
|
+
|
|
51
|
+
Rejected: **Fly.io**, whose managed Postgres starts around $38/mo — the compute is cheaper and the database is not. **Railway**, whose Postgres templates are explicitly unmanaged: you tune and back them up, which is precisely the responsibility this decision exists to shed.
|
|
52
|
+
|
|
53
|
+
### 4. Cloudflare is the front door, not the compute
|
|
54
|
+
|
|
55
|
+
DNS, TLS, and — when `byo-host` needs to reach a machine behind a router — Tunnels, for which a working client already exists at `scripts/gds/cf-tunnel.js`.
|
|
56
|
+
|
|
57
|
+
**Not the hall's runtime.** Cloudflare Workers is a V8-isolate runtime, not Node; Express and `pg` would have to be rewritten. Cloudflare Containers is built for bursty workloads that sleep between requests, and the hall runs background sweeps and a live event stream that do not survive sleeping. Its Postgres would be external in either case.
|
|
58
|
+
|
|
59
|
+
### 5. Bongos does not integrate with the customer's deployment platform
|
|
60
|
+
|
|
61
|
+
Their application already deploys itself: Vercel and Netlify watch the repository. Bongos writes code and pushes it. There is no adapter layer, and an adapter layer would solve a problem that does not exist.
|
|
62
|
+
|
|
63
|
+
This is what makes "bring your preferred platform" nearly free to support. The platform we do not talk to is the platform we do not have to add.
|
|
64
|
+
|
|
65
|
+
### 6. The deployment platform is the system of record for secrets; Bongos stores names, never values
|
|
66
|
+
|
|
67
|
+
A project declares its environment variables to Bongos as a **manifest** — name, purpose, environment, required or optional — so a builder writing code knows what the surface is. The values live where the running application reads them, in a store that already encrypts, audits and rotates them.
|
|
68
|
+
|
|
69
|
+
Bongos holds no application secret, no platform API token, and runs no "is it set?" probe. Adding any of those makes the platform the custodian of other companies' credentials, which is the responsibility this ADR is spending its whole budget to avoid. See [task 1004062](https://cloudbongos.com/builders#/task/1004062).
|
|
70
|
+
|
|
71
|
+
For `byo-host` there is no platform to ask, and the manifest is simply marked unverifiable. That keeps *Bongos holds no values* true everywhere, with no exception to reason about later.
|
|
72
|
+
|
|
73
|
+
### 7. The free tier is a capped trial
|
|
74
|
+
|
|
75
|
+
At ~$14 per project, an indefinite subsidised free tier is not viable. This replaces [ADR 0145](0145-free-hosted-project-tier-isolation-and-domain-separation.md)'s "all-free for now". Implementing the cap is its own task; it cannot be metered before the migration.
|
|
76
|
+
|
|
77
|
+
## Consequences
|
|
78
|
+
|
|
79
|
+
- **This wave is mostly a deletion.** Systemd units, per-instance unix accounts, Caddy snippets, `createdb`, backup timers and the capacity ceiling are things Render provides. Not all of `provision*.js` goes — the repository scaffold, the GitHub App manifest handshake and the core-upgrade path serve the hub and the `byo-host` scaffold too. The migration task removes what is genuinely orphaned, after the projects have moved, and not before.
|
|
80
|
+
|
|
81
|
+
- **Per-project cost rises about 17×, from $0.80 to ~$14.** That is the price of not being a landlord, and it is why decision 7 exists. It also makes metering ([task 1001949](https://cloudbongos.com/builders#/task/1001949)) and the spend rollup ([task 1001621](https://cloudbongos.com/builders#/task/1001621)) real work rather than deferred work.
|
|
82
|
+
|
|
83
|
+
- **[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.** Its measurements were correct and its ceiling still describes the box; the box just stops holding projects. The queued resize to 2 vCPU / 4 GB is therefore **not** to be spent ([task 1003923](https://cloudbongos.com/builders#/task/1003923), held).
|
|
84
|
+
|
|
85
|
+
- **[ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md)'s seam is not extended.** It correctly said it was "two implementations wide, not N". Render supervises its own processes, so the seam neither grows a third backend nor needs one.
|
|
86
|
+
|
|
87
|
+
- **The hub becomes the single point of failure it always was, now visibly.** With projects elsewhere, everything that can be lost with the droplet is the control plane's own database — which makes offsite backups ([task 1002214](https://cloudbongos.com/builders#/task/1002214)) the highest-consequence open item in the goal.
|
|
88
|
+
|
|
89
|
+
- **`byo-host` starts cheap and stays honest.** It reuses the pull that already exists ([ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md)): the owner supplies an origin and the hub reads the instance's own public manifest. Nothing is pushed to it, so it is excluded from the settings-apply and restart paths for the same reason `dedicated` was.
|
|
90
|
+
|
|
91
|
+
## Alternatives considered
|
|
92
|
+
|
|
93
|
+
- **Move to AWS or GCP.** Rejected. Their pricing is earned by consuming their managed services; a bare Linux instance from either is 2–3× DigitalOcean for the same memory, plus a console, an IAM model and a billing surface to learn. It adds maintenance where the goal is to remove it.
|
|
94
|
+
- **Keep the shared box and fix the cookie instead.** Cheaper, and it closes the security finding. Rejected because it leaves the 4,600-line landlord runner in place and answers none of the other three questions.
|
|
95
|
+
- **Build a deployment-platform adapter layer (Vercel/Netlify/Render).** Rejected as speculative pluggability, the thing [ADR 0306](0306-instance-supervision-is-a-seam-with-systemd-as-the-default.md) explicitly warned against. Their applications already deploy themselves.
|
|
96
|
+
- **Bongos as the secrets store, pushing values to each platform via its API.** Rejected: it makes us the custodian of other companies' credentials for convenience that the manifest already delivers.
|
package/docs/adr/README.md
CHANGED
|
@@ -414,3 +414,4 @@ This keeps the decision history honest and traceable.
|
|
|
414
414
|
| 0318 | [**Listing a file for task affinity is not claiming to own it** ([task 1004110](https://cloudbongos.com/builders#/task/1004110), goal 1000095 — BONGOS-V2). The case [ADR 0314](0314-module-protection-is-per-path-not-per-module.md) named and left open, closed by neither of the two routes it predicted. `builder-settings` derived `protected` — Archon-only to scope a goal to — on exactly one glob, `src/bongos/routes/me.js`, caught by the blanket `src/bongos/routes/` prefix. **That glob was never an ownership claim:** `MODULE_GLOBS` listed it for task AFFINITY and said in the same comment that the `GET /me` aggregator stays a thin core facade, and `isProtected()` cannot tell the two apart. **Both predicted routes were measured and rejected.** Narrowing the blanket prefix: all eighteen files under `src/bongos/routes/` are protected by that prefix AND NOTHING ELSE, so narrowing unwalls `auth.js`, `builders.js`, `backup.js` and `core-update.js` together — now pinned as a tripwire test rather than left as a caution. Moving `me.js` into `modules/builder-settings/`: the aggregator resolves TEN module ports (`lifecycle`, `reward`, `onboarding`, `government`, `specialities`, `grade`, `box`, `art` ×2, `discord`) and `builder-settings` is one of them, so hosting it inside one module means resolving nine foreign ports from the inside — the `core -> module` edge [ADR 0083](0083-modular-architecture-strangler-build-plan.md) rule 1 forbids. Decision: **ownership follows what the file is** — `me.js` moves to `kernel`, which already owns `src/bongos/routes/security.js`. This is 0314 one step on: owning the command is not owning the authority, and **being listed next to a module is not owning it either**. **Rejected: a general affinity tier** in `MODULE_GLOBS` mirroring `isCommandPath` — the audit does not support it. Three other modules (`kernel`, `devsecops`, `methodology`) are also protected only by paths outside their own directory, but in each the foreign path genuinely IS the domain; `builder-settings` was the only module protected by a path its own source called core, so a new tier would be a new place for a hole to hide, built for a population of one. `protectedModules()` goes 9 -> 8. **The wall that matters does not move:** a sub-Metic builder touching `/me` is still refused by `checkPermissionPaths`, asserted directly rather than assumed. The sixteen still-unowned route files remain [task 1004116](https://cloudbongos.com/builders#/task/1004116), whose option (a) this de-risks.](0318-listing-a-file-for-task-affinity-is-not-owning-it.md) | permissions / module scope |
|
|
415
415
|
| 0321 | [**Brightness is a payout-weighted share, and a removal's targets are rows** ([task 1004099](https://cloudbongos.com/builders#/task/1004099), goal 1000095 — BONGOS-V2). Size is an idea's total ideator payout and never falls; brightness is how much of that work is still alive, and the pair is what lets a thought whose version was torn out draw LARGE AND DARK rather than merging with one that never mattered. Two things had to be settled. **The removal facts were stored in the wrong direction**: [ADR 0319](0319-a-removal-verdict-is-a-row-beside-the-question.md) gave a confirmed removal a row carrying the verdict and `ship_task_id` — the task whose merge DELETED the files — and deliberately left out the tasks whose work was removed, saying brightness would design its own storage when it needed one. Those are different rows under different ideas, so reading one as the other dims the wrong star while looking like a working query. `<redacted>` stores `(proposal_task_id, removed_task_id, paths)` at PROPOSAL time, from the SAME derivation objects the proposal body renders as prose — one derivation, two renderings, so they cannot disagree and `removal-proposals.js` keeps its refusal to parse a body back. Proposal time and not verdict time because the attribution comes from capped, rate-limited GitHub commit-history reads that only exist while the cascade runs. NOT a runner fan-out ([ADR 0242](0242-a-cascade-is-a-declaration-table-on-the-event-that-already-exists.md)): the rule still files one task; this records a side fact about it. `ON CONFLICT DO NOTHING`, never `DO UPDATE`, since a degraded second walk finds FEWER paths and must not erase a good first read — and the writer runs on the cascade's SKIPPED row too, so a re-ship repairs what a derivation outage missed. **The share is weighted by PAYOUT, not by task count**, or deleting a one-line fix dims a star as hard as deleting the feature; weighting by what each hop earned also makes 'large and dead' exact, and makes the two channels a share of ONE number rather than two unrelated measurements. Basis is paid + estimated to match size, which required `priceDescentForIdea`/`ideaSizes` to return a per-task `byTask` summing EXACTLY to `credits` — including ledger payments for tasks the descent no longer reaches, which `idea-size.js` keeps in `credits` so size cannot fall and which would otherwise make the parts sum to less than the whole. Three edges, each silent: an idea that earned nothing is `share: 1, hasWork: false`, because zero over zero is not zero and a protostar is not a remnant (the BV2.R19 distinction); an unreadable removal port gives `share: null, attributable: false`, because 'could not look' is not 'nothing was removed'; and a CONFIRMED removal naming no target dims nothing at all — brightness cannot know which idea — so `countUnattributedConfirmed()` exists to say the gap out loud instead of quietly brightening the sky. Brightness lives in `economy` beside size and reaches lifecycle through the new `lifecycle.removals` port, resolved OPTIONALLY and NOT declared in `consumes` (declaring it would turn 'lifecycle off' into 'instance will not start' — the `ideas.descent` precedent). **No credit is clawed back and nothing here writes**: ADR 0313 stands, `shipped` is terminal, only the picture changes. Rejected: parsing the proposal body (ADR 0319 already refused it — a human-facing line shape fails silently when reworded); re-deriving at verdict time; treating `ship_task_id` as the removed task; counting tasks rather than payouts; clamping the share at the top, which would hide the anomaly it claims to prevent.](0321-brightness-is-a-weighted-share-and-the-targets-are-a-row.md) | ideas / economy |
|
|
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
|
+
| 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 |
|
|
@@ -2362,5 +2362,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2362
2362
|
landed since 1.19.942 with no explicit bump. run 35661533404. (task 1002620)
|
|
2363
2363
|
1.19.944 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2364
2364
|
landed since 1.19.943 with no explicit bump. run 35665131778. (task 1002620)
|
|
2365
|
+
1.19.945 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2366
|
+
landed since 1.19.944 with no explicit bump. run 35668642462. (task 1002620)
|
|
2367
|
+
1.19.946 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2368
|
+
landed since 1.19.945 with no explicit bump. run 35669244197. (task 1002620)
|
|
2365
2369
|
---------------------------------------------------------------------------
|
|
2366
2370
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.946",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.946",
|
|
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.
|
|
3
|
+
"version": "1.19.946",
|
|
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",
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -1277,7 +1277,7 @@ const CHECKS = [
|
|
|
1277
1277
|
// one (ADR 0289). Rationale + the seam→consumer table live in that file.
|
|
1278
1278
|
require('./seam-wiring-guard.js').checkSeamWiring,
|
|
1279
1279
|
require('./home-isolation-guard.js').checkHomeIsolation, // Check 34 — task 1003763: a test that sets HOME for a child without USERPROFILE isolates NOTHING on win32, so it silently stops testing what its comment says. Three tasks fixed this one site at a time. Rationale in that file.
|
|
1280
|
-
require('./scope-map-guard.js')
|
|
1280
|
+
...['checkDevsecopsOwnsItsChecks', 'checkEveryModuleDirIsScoped', 'checkNoNewUnownedRoot'].map((k) => require('./scope-map-guard.js')[k]), // Check 35 — task 1003949: a check file no MODULE_SCOPE_MAP key owns cannot be authorized by the goal scoped to own it, and the list drifts once per new check. Check 36 — task 1004132: the same defect one level up, a carved module whose own directory no key claims (modules/specialities/ shipped that way). Sharing the line per the ratchet convention above. Rationale in that file.
|
|
1281
1281
|
require('./adr-namespace.js').checkAdrNumbering, require('./exec-path-guard.js').checkExecPathsStatic, // Check 32 — task 1003371 / B50: what the server EXECUTES, out of whose tree — the blind spot all three 2026-08-29 CRITICALs shared. Rationale + reviewed exemptions live in that file. Sharing Check 31's line (the 19c/19d + 23/24 precedent above): fitness.js is AT the 1,500 ratchet and a new check cannot afford a line until task 1003825 buys it back.
|
|
1282
1282
|
];
|
|
1283
1283
|
|
|
@@ -117,4 +117,184 @@ function checkDevsecopsOwnsItsChecks() {
|
|
|
117
117
|
+ 'unless CI says otherwise (task 1003949).',
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
module
|
|
120
|
+
// PURE — every module directory on disk, by key. A module is a modules/<key>/ dir
|
|
121
|
+
// carrying module.json (the ADR 0083 manifest), which is the same structural test
|
|
122
|
+
// the loader's discoverModules() uses: a key is what has a manifest, never a
|
|
123
|
+
// hand-kept roster. Exported so the scan is testable against a fixture tree —
|
|
124
|
+
// an empty result here looks identical to a clean repo, which is the failure the
|
|
125
|
+
// sibling check above exists to refuse.
|
|
126
|
+
function moduleDirsOnDisk(modulesRoot) {
|
|
127
|
+
let entries = [];
|
|
128
|
+
try { entries = fs.readdirSync(modulesRoot, { withFileTypes: true }); } catch { return []; }
|
|
129
|
+
return entries
|
|
130
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(modulesRoot, e.name, 'module.json')))
|
|
131
|
+
.map((e) => e.name)
|
|
132
|
+
.sort();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A carved module whose own directory no MODULE_SCOPE_MAP key claims (task 1004132).
|
|
136
|
+
//
|
|
137
|
+
// THE DRIFT THIS STOPS, and it is the sibling of the check above rather than a new
|
|
138
|
+
// idea: modules/specialities/ shipped a full module — manifest, db slice, routes, two
|
|
139
|
+
// migrations — with no key here at all. scopeSubset() therefore resolved every file in
|
|
140
|
+
// it to no territory, so a goal scoped to `specialities` was refused out_of_scope for
|
|
141
|
+
// touching the module it is named after. It was the ONLY on-disk module in that state,
|
|
142
|
+
// which is exactly why nobody noticed: 21 of 22 were fine.
|
|
143
|
+
//
|
|
144
|
+
// The rule is deliberately narrow. It does NOT require every repo path to have an
|
|
145
|
+
// owner — src/, scripts/ and bin/ are unowned ON PURPOSE (ADR 0086: a goal grants
|
|
146
|
+
// authority BOUNDED to module territory, and POST /tasks stays Archon-only for
|
|
147
|
+
// ungoverned creation), so a broader "every root is owned" check would encode the
|
|
148
|
+
// opposite of the design. What must hold is narrower and absolute: if a directory
|
|
149
|
+
// declares itself a module, a goal must be able to be scoped to it.
|
|
150
|
+
function checkEveryModuleDirIsScoped() {
|
|
151
|
+
const name = 'every carved module owns its own directory (task 1004132)';
|
|
152
|
+
const dirs = moduleDirsOnDisk(path.join(REPO_ROOT, 'modules'));
|
|
153
|
+
|
|
154
|
+
// Same anti-vacuity rule as the check above: this repo HAS modules, so an empty
|
|
155
|
+
// scan means the manifest test broke, not that the tree is clean.
|
|
156
|
+
if (dirs.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
ok: false,
|
|
160
|
+
hardFail: true,
|
|
161
|
+
violations: ['modules/: found NO directory carrying a module.json. That is a broken scan, '
|
|
162
|
+
+ 'not a clean result — moduleDirsOnDisk() in scope-map-guard.js needs looking at.'],
|
|
163
|
+
warnings: [],
|
|
164
|
+
note: 'the scan produced an empty set, which cannot be right.',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { MODULE_SCOPE_MAP } = require('../../src/bongos/module-scope-map.js');
|
|
169
|
+
const globsOf = (v) => (Array.isArray(v) ? v : (v && v.globs) || []);
|
|
170
|
+
const claimed = new Set();
|
|
171
|
+
for (const v of Object.values(MODULE_SCOPE_MAP)) {
|
|
172
|
+
// A key may claim another module's dir (a cross-cutting owner), so collect every
|
|
173
|
+
// modules/<key>/ glob from EVERY key rather than only from the same-named one.
|
|
174
|
+
for (const g of globsOf(v)) {
|
|
175
|
+
const m = /^modules\/([^/]+)\/$/.exec(g);
|
|
176
|
+
if (m) claimed.add(m[1]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const unscoped = dirs.filter((k) => !claimed.has(k));
|
|
181
|
+
const violations = unscoped.map((k) => `modules/${k}/ carries a module.json but no MODULE_SCOPE_MAP `
|
|
182
|
+
+ `key claims it — scopeSubset() resolves the whole module to no territory, so a goal scoped to `
|
|
183
|
+
+ `'${k}' is denied out_of_scope for touching the module it is named after. Add a '${k}' key `
|
|
184
|
+
+ `owning 'modules/${k}/'.`);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
name,
|
|
188
|
+
ok: unscoped.length === 0,
|
|
189
|
+
hardFail: unscoped.length > 0,
|
|
190
|
+
violations,
|
|
191
|
+
warnings: [],
|
|
192
|
+
note: `${dirs.length} carved module(s) on disk, each claimed by a MODULE_SCOPE_MAP key.`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// PURE — every tracked top-level root, as `name` or `name/`. Exported so the scan
|
|
197
|
+
// is testable against a fixture list rather than only against this repo.
|
|
198
|
+
function trackedRoots(files) {
|
|
199
|
+
const out = new Set();
|
|
200
|
+
for (const f of files || []) {
|
|
201
|
+
if (!f) continue;
|
|
202
|
+
const i = f.indexOf('/');
|
|
203
|
+
out.add(i === -1 ? f : `${f.slice(0, i)}/`);
|
|
204
|
+
}
|
|
205
|
+
return [...out].sort();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// PURE — does ANY glob reach into this root? A root is "carved" the moment one
|
|
209
|
+
// does; src/ and scripts/ are partially owned and that is the normal in-progress
|
|
210
|
+
// state ADR 0086 §4 describes ("auto-tightens as ADR 0083 moves code into
|
|
211
|
+
// modules/<key>/"). What this answers is the narrower question: is this root
|
|
212
|
+
// territory the scope map has never heard of at all?
|
|
213
|
+
function rootIsTouched(root, globs) {
|
|
214
|
+
return (globs || []).some((g) => g === root || g.startsWith(root) || root.startsWith(g));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// New unowned territory, and stale entries in the record of the old (task 1004132).
|
|
218
|
+
//
|
|
219
|
+
// THE GAP THIS CLOSES, and it is the one the grade round caught me leaving open.
|
|
220
|
+
// I had argued that because the core is unowned BY DESIGN (ADR 0086: authority is
|
|
221
|
+
// bounded to module territory, POST /tasks stays Archon-only for ungoverned
|
|
222
|
+
// creation), no general check should exist. That is half right and wholly
|
|
223
|
+
// insufficient: the design says WHICH roots may be unowned, it does not stop the
|
|
224
|
+
// set growing. Without this, a new top-level directory lands tomorrow, no module
|
|
225
|
+
// claims it, and nothing anywhere fails — the exact shape that let
|
|
226
|
+
// modules/specialities/ sit unowned for its whole life.
|
|
227
|
+
//
|
|
228
|
+
// So the decision is recorded as DATA (UNOWNED_ROOTS in module-scope-map.js) and
|
|
229
|
+
// this check enforces it in BOTH directions, the discipline
|
|
230
|
+
// tests/host_topology_skips.mjs uses for its own allowlist:
|
|
231
|
+
// - a root that is neither carved nor listed -> new territory, fail
|
|
232
|
+
// - a listed root that is now carved or gone -> stale entry, fail
|
|
233
|
+
// A one-directional check would let the list rot into a rubber stamp.
|
|
234
|
+
function checkNoNewUnownedRoot(fileList) {
|
|
235
|
+
const name = 'no new unowned top-level root (task 1004132)';
|
|
236
|
+
let files = fileList;
|
|
237
|
+
if (!files) {
|
|
238
|
+
files = require('node:child_process')
|
|
239
|
+
.execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
|
|
240
|
+
.trim().split('\n').filter(Boolean);
|
|
241
|
+
}
|
|
242
|
+
const roots = trackedRoots(files);
|
|
243
|
+
|
|
244
|
+
// Same anti-vacuity rule as its two siblings: this repo HAS tracked files, so an
|
|
245
|
+
// empty scan is a broken `git ls-files`, not a clean tree.
|
|
246
|
+
if (roots.length === 0) {
|
|
247
|
+
return {
|
|
248
|
+
name,
|
|
249
|
+
ok: false,
|
|
250
|
+
hardFail: true,
|
|
251
|
+
violations: ['git ls-files returned NO tracked files. That is a broken scan, not a clean '
|
|
252
|
+
+ 'result — trackedRoots() in scope-map-guard.js needs looking at.'],
|
|
253
|
+
warnings: [],
|
|
254
|
+
note: 'the scan produced an empty set, which cannot be right.',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const map = require('../../src/bongos/module-scope-map.js');
|
|
259
|
+
const globs = [...map.globsForModules(map.moduleKeys()), ...map.SHARED_GLOBS];
|
|
260
|
+
const listed = map.UNOWNED_ROOTS || {};
|
|
261
|
+
|
|
262
|
+
const violations = [];
|
|
263
|
+
for (const r of roots) {
|
|
264
|
+
if (rootIsTouched(r, globs)) {
|
|
265
|
+
if (Object.prototype.hasOwnProperty.call(listed, r)) {
|
|
266
|
+
violations.push(`'${r}' is listed in UNOWNED_ROOTS but a module glob now reaches it — the `
|
|
267
|
+
+ 'entry is stale. Delete it; a root leaving that list is the map tightening as intended.');
|
|
268
|
+
}
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!Object.prototype.hasOwnProperty.call(listed, r)) {
|
|
272
|
+
violations.push(`'${r}' is tracked but NO module glob and no SHARED_GLOBS entry reaches it, and `
|
|
273
|
+
+ 'it is not in UNOWNED_ROOTS. New unowned territory: no goal can ever authorize a task that '
|
|
274
|
+
+ `touches it. Give it an owning module, or add it to UNOWNED_ROOTS in module-scope-map.js `
|
|
275
|
+
+ 'with the reason it has none.');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const r of Object.keys(listed)) {
|
|
279
|
+
if (!roots.includes(r)) {
|
|
280
|
+
violations.push(`UNOWNED_ROOTS lists '${r}', which is not a tracked root any more — stale entry, `
|
|
281
|
+
+ 'delete it so the list keeps meaning something.');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
name,
|
|
287
|
+
ok: violations.length === 0,
|
|
288
|
+
hardFail: violations.length > 0,
|
|
289
|
+
violations,
|
|
290
|
+
warnings: [],
|
|
291
|
+
note: `${roots.length} tracked root(s); ${Object.keys(listed).length} recorded as deliberately `
|
|
292
|
+
+ 'unowned (ADR 0086 — core is Archon territory). Shrink-only: an entry may leave, not appear silently.',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
checkDevsecopsOwnsItsChecks, siblingRequires, unownedBy, FITNESS,
|
|
298
|
+
checkEveryModuleDirIsScoped, moduleDirsOnDisk,
|
|
299
|
+
checkNoNewUnownedRoot, trackedRoots, rootIsTouched,
|
|
300
|
+
};
|
|
@@ -145,6 +145,16 @@ const MODULE_GLOBS = {
|
|
|
145
145
|
'modules/ideas/',
|
|
146
146
|
'scripts/gds/capture.js', 'scripts/gds/triage.js', 'scripts/gds/render-ideas.js',
|
|
147
147
|
],
|
|
148
|
+
// Role sub-specialities (task 1003855, ADR 0296). Carved into modules/specialities/
|
|
149
|
+
// but never given a key here, so every file it owns — module.json, db.js, routes,
|
|
150
|
+
// its two migrations — resolved to NO module territory: a goal scoped to
|
|
151
|
+
// `specialities` was denied out_of_scope for touching the module it is named after
|
|
152
|
+
// (found by task 1004132). Every OTHER on-disk module owns its own directory; this
|
|
153
|
+
// was the only one that did not, and checkEveryModuleDirIsScoped in
|
|
154
|
+
// scripts/gds/scope-map-guard.js now fails CI if a new carve repeats it.
|
|
155
|
+
specialities: [
|
|
156
|
+
'modules/specialities/',
|
|
157
|
+
],
|
|
148
158
|
// --- builder prefs (wandering/skill/sound/render) + builder-needs ---
|
|
149
159
|
// Carved into modules/builder-settings/ (BV1.R80 / ADR 0093 §1): the five pref
|
|
150
160
|
// libs + builder-needs + their db slice + the `/me/*-prefs` routes + the
|
|
@@ -521,6 +531,62 @@ const SHARED_GLOBS = Object.freeze(
|
|
|
521
531
|
SHARED_GLOBS_RAW.filter((g) => !touchesOverlap([g], PROTECTED_GLOBS)),
|
|
522
532
|
);
|
|
523
533
|
|
|
534
|
+
// ── Roots no module glob touches, and why (task 1004132) ────────────────────────
|
|
535
|
+
//
|
|
536
|
+
// This is the RECORDED DECISION behind "the core is owned by nobody", not a note
|
|
537
|
+
// about it. `checkNoNewUnownedRoot` in scripts/gds/scope-map-guard.js reads this
|
|
538
|
+
// map and fails CI BOTH ways: a tracked root that appears here-less is new unowned
|
|
539
|
+
// territory, and an entry whose root became owned (or stopped existing) is stale.
|
|
540
|
+
//
|
|
541
|
+
// WHY THESE ARE ALLOWED TO BE UNOWNED. Per ADR 0086 a goal grants authority
|
|
542
|
+
// BOUNDED to module territory, and `POST /tasks` stays Archon-only for ungoverned
|
|
543
|
+
// creation. So a path with no module owner is not homeless — it is reachable only
|
|
544
|
+
// through the Archon door, which is the safety model, not a gap in it. What must
|
|
545
|
+
// not happen is that set GROWING silently, which is what this freezes.
|
|
546
|
+
//
|
|
547
|
+
// SHRINK-ONLY, like the fitness ratchets. ADR 0086 §4 expects the map to
|
|
548
|
+
// "auto-tighten as ADR 0083 moves code into modules/<key>/" — so an entry leaving
|
|
549
|
+
// this list is the system working. Adding one needs a stated reason, here, in the
|
|
550
|
+
// same commit that adds the root.
|
|
551
|
+
//
|
|
552
|
+
// A `review` reason is an HONEST placeholder, not an endorsement: it marks a root
|
|
553
|
+
// that probably does have a natural owner but whose assignment is its own decision.
|
|
554
|
+
// Assigning nineteen roots in one pass is the one-at-a-time churn task 1004132 was
|
|
555
|
+
// filed to avoid; freezing the set and naming the arguable ones is the cheap half.
|
|
556
|
+
const UNOWNED_ROOTS = Object.freeze({
|
|
557
|
+
// Generated packaging manifests — written by package-core.js, never hand-edited.
|
|
558
|
+
'.bongos-core.json': 'generated package manifest (scripts/gds/package-core.js)',
|
|
559
|
+
'.cloudbongos-core.json': 'generated package manifest (scripts/gds/package-core.js)',
|
|
560
|
+
// Container build + repo-level dotfiles. review: devsecops already owns the
|
|
561
|
+
// neighbouring .npmrc / .gitattributes / package.json, so these plausibly belong
|
|
562
|
+
// to it — but that is a scope-wall change and wants its own decision.
|
|
563
|
+
'.dockerignore': 'review: container build, plausibly devsecops',
|
|
564
|
+
'Dockerfile': 'review: container build, plausibly devsecops',
|
|
565
|
+
'docker-compose.yml': 'review: container build, plausibly devsecops',
|
|
566
|
+
'docker-entrypoint.sh': 'review: container build, plausibly devsecops',
|
|
567
|
+
'.gitignore': 'review: repo-level dotfile, plausibly devsecops',
|
|
568
|
+
'.env.local.example': 'review: repo-level dotfile, plausibly devsecops',
|
|
569
|
+
// Design tooling checked into the tree. review: plausibly ui-design.
|
|
570
|
+
'.impeccable/': 'review: design tooling, plausibly ui-design',
|
|
571
|
+
// Governance + legal. Deliberately not methodology: methodology owns docs/, and
|
|
572
|
+
// these are the project's constitutional documents rather than its handbook.
|
|
573
|
+
'LICENSE': 'project legal text',
|
|
574
|
+
'CODE_OF_CONDUCT.md': 'project governance text',
|
|
575
|
+
'CONTRIBUTING.md': 'project governance text',
|
|
576
|
+
'GOVERNANCE.md': 'project governance text',
|
|
577
|
+
// Top-level product/design statements, read by humans before any module matters.
|
|
578
|
+
'README.md': 'repo front door',
|
|
579
|
+
'DESIGN.md': 'top-level design statement',
|
|
580
|
+
'PRODUCT.md': 'top-level product statement',
|
|
581
|
+
// The core itself. bin/ is the CLI entrypoint and clients/ is the GENERATED API
|
|
582
|
+
// client (scripts/gds/gen-api-client.js); public-docs/ is the published site.
|
|
583
|
+
// These are core, and core is Archon territory by ADR 0086 — the primary case
|
|
584
|
+
// this list exists to record.
|
|
585
|
+
'bin/': 'core CLI entrypoint — Archon territory (ADR 0086)',
|
|
586
|
+
'clients/': 'generated API client (scripts/gds/gen-api-client.js)',
|
|
587
|
+
'public-docs/': 'published docs site — core',
|
|
588
|
+
});
|
|
589
|
+
|
|
524
590
|
// A CLI ENTRYPOINT — a single executable script under scripts/, e.g.
|
|
525
591
|
// 'scripts/gds/setup.js'. Structural, not a list: it is a path test, so no roster
|
|
526
592
|
// is hand-maintained here and nothing has to be kept in step with PROTECTED_GLOBS.
|
|
@@ -676,6 +742,7 @@ function moduleDriftFiles(moduleKey, files) {
|
|
|
676
742
|
}
|
|
677
743
|
|
|
678
744
|
module.exports = {
|
|
745
|
+
UNOWNED_ROOTS,
|
|
679
746
|
MODULE_GLOBS,
|
|
680
747
|
MODULE_SCOPE_MAP,
|
|
681
748
|
SHARED_GLOBS,
|
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.
|
|
74
|
+
const CORE_VERSION = '1.19.946'; // 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');
|
|
@@ -17,7 +17,11 @@ import assert from 'node:assert/strict';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
|
-
const { checkDevsecopsOwnsItsChecks, siblingRequires
|
|
20
|
+
const { checkDevsecopsOwnsItsChecks, siblingRequires, checkEveryModuleDirIsScoped, moduleDirsOnDisk,
|
|
21
|
+
checkNoNewUnownedRoot, trackedRoots, rootIsTouched } = require('../scripts/gds/scope-map-guard.js');
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
21
25
|
|
|
22
26
|
test('every file fitness.js requires is owned by some MODULE_SCOPE_MAP key', () => {
|
|
23
27
|
const r = checkDevsecopsOwnsItsChecks();
|
|
@@ -80,3 +84,99 @@ test('the PASS note carries the count, so the check cannot look green while doin
|
|
|
80
84
|
const n = Number(r.note.match(/^(\d+) file/)[1]);
|
|
81
85
|
assert.ok(n > 14, `expected more than the fourteen the filing task listed, saw ${n}`);
|
|
82
86
|
});
|
|
87
|
+
|
|
88
|
+
// ── the same defect one level up: a carved module nobody can scope to (task 1004132) ──
|
|
89
|
+
//
|
|
90
|
+
// modules/specialities/ shipped a whole module — manifest, db slice, routes, two
|
|
91
|
+
// migrations — with no MODULE_SCOPE_MAP key, so a goal scoped to `specialities` was
|
|
92
|
+
// refused out_of_scope for touching the module it is named after. 21 of the 22 modules
|
|
93
|
+
// were fine, which is exactly why it went unseen.
|
|
94
|
+
|
|
95
|
+
test('every carved module on disk is claimed by some MODULE_SCOPE_MAP key', () => {
|
|
96
|
+
const r = checkEveryModuleDirIsScoped();
|
|
97
|
+
assert.equal(r.ok, true, r.violations.join('\n'));
|
|
98
|
+
assert.equal(r.hardFail, false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('specialities specifically — the module this check was written for', () => {
|
|
102
|
+
const { MODULE_SCOPE_MAP } = require('../src/bongos/module-scope-map.js');
|
|
103
|
+
const globsOf = (v) => (Array.isArray(v) ? v : (v && v.globs) || []);
|
|
104
|
+
const all = Object.values(MODULE_SCOPE_MAP).flatMap(globsOf);
|
|
105
|
+
assert.ok(all.includes('modules/specialities/'),
|
|
106
|
+
'modules/specialities/ lost its owner again — a goal scoped to specialities cannot touch it');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('moduleDirsOnDisk counts a manifest, not a directory', () => {
|
|
110
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-modules-'));
|
|
111
|
+
fs.mkdirSync(path.join(root, 'real'));
|
|
112
|
+
fs.writeFileSync(path.join(root, 'real', 'module.json'), '{"key":"real"}');
|
|
113
|
+
fs.mkdirSync(path.join(root, 'not-a-module')); // no manifest
|
|
114
|
+
fs.writeFileSync(path.join(root, 'stray.txt'), 'x'); // not a directory
|
|
115
|
+
assert.deepEqual(moduleDirsOnDisk(root), ['real']);
|
|
116
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('the scan cannot pass vacuously — an empty modules/ is a BROKEN scan, not a clean one', () => {
|
|
120
|
+
// The failure this mirrors is the one the sibling check above documents: a scanner
|
|
121
|
+
// that matches nothing reports a healthy repo, and healthy is what broken looks like.
|
|
122
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-empty-'));
|
|
123
|
+
assert.deepEqual(moduleDirsOnDisk(empty), []);
|
|
124
|
+
assert.deepEqual(moduleDirsOnDisk(path.join(empty, 'does-not-exist')), [],
|
|
125
|
+
'a missing modules/ must return empty rather than throw — the check turns that into a hard fail');
|
|
126
|
+
fs.rmSync(empty, { recursive: true, force: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── the record of what is deliberately unowned, and that it cannot rot (task 1004132) ──
|
|
130
|
+
//
|
|
131
|
+
// The decision "the core is owned by nobody" is DATA (UNOWNED_ROOTS), not prose, so
|
|
132
|
+
// that it can be enforced. A one-directional check would let the list become a
|
|
133
|
+
// rubber stamp, so both directions are pinned here.
|
|
134
|
+
|
|
135
|
+
test('the repo has no unowned root that UNOWNED_ROOTS does not account for', () => {
|
|
136
|
+
const r = checkNoNewUnownedRoot();
|
|
137
|
+
assert.equal(r.ok, true, r.violations.join('\n'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('a NEW unowned root fails — the growth this exists to stop', () => {
|
|
141
|
+
const files = require('node:child_process')
|
|
142
|
+
.execFileSync('git', ['ls-files'], { encoding: 'utf8' }).trim().split('\n');
|
|
143
|
+
const r = checkNoNewUnownedRoot([...files, 'vendor/thing.js']);
|
|
144
|
+
assert.equal(r.ok, false);
|
|
145
|
+
assert.equal(r.hardFail, true);
|
|
146
|
+
assert.match(r.violations.join('\n'), /'vendor\/' is tracked but NO module glob/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('a STALE entry fails too — the list may shrink, never lie', () => {
|
|
150
|
+
const files = require('node:child_process')
|
|
151
|
+
.execFileSync('git', ['ls-files'], { encoding: 'utf8' }).trim().split('\n');
|
|
152
|
+
const r = checkNoNewUnownedRoot(files.filter((f) => f !== 'LICENSE'));
|
|
153
|
+
assert.equal(r.ok, false);
|
|
154
|
+
assert.match(r.violations.join('\n'), /lists 'LICENSE', which is not a tracked root any more/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the scan cannot pass vacuously', () => {
|
|
158
|
+
const r = checkNoNewUnownedRoot([]);
|
|
159
|
+
assert.equal(r.ok, false);
|
|
160
|
+
assert.equal(r.hardFail, true);
|
|
161
|
+
assert.match(r.violations.join('\n'), /broken scan, not a clean result/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('trackedRoots collapses paths to roots; rootIsTouched sees a PARTIAL carve', () => {
|
|
165
|
+
assert.deepEqual(trackedRoots(['src/a/b.js', 'src/c.js', 'LICENSE', 'bin/x']),
|
|
166
|
+
['LICENSE', 'bin/', 'src/']);
|
|
167
|
+
// src/ is partially owned (one file claimed) — that is the normal in-progress
|
|
168
|
+
// state of ADR 0083's carve, and must NOT read as unowned territory.
|
|
169
|
+
assert.equal(rootIsTouched('src/', ['src/bongos/llm-pricing.js']), true);
|
|
170
|
+
assert.equal(rootIsTouched('vendor/', ['src/bongos/llm-pricing.js']), false);
|
|
171
|
+
// A directory glob covering the whole root counts.
|
|
172
|
+
assert.equal(rootIsTouched('modules/', ['modules/ideas/']), true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('every UNOWNED_ROOTS entry carries a reason', () => {
|
|
176
|
+
const { UNOWNED_ROOTS } = require('../src/bongos/module-scope-map.js');
|
|
177
|
+
for (const [root, reason] of Object.entries(UNOWNED_ROOTS)) {
|
|
178
|
+
assert.equal(typeof reason, 'string', `${root} has no reason`);
|
|
179
|
+
assert.ok(reason.trim().length > 8,
|
|
180
|
+
`${root}'s reason is too thin to justify an unowned root: ${JSON.stringify(reason)}`);
|
|
181
|
+
}
|
|
182
|
+
});
|