@bongos/core 1.19.977 → 1.19.979
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 +19 -14
- package/docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md +98 -0
- package/docs/adr/README.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/goal-inbox.js +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/rot_inbox.mjs +10 -0
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.979",
|
|
6
|
+
"core_contract": "1.19.979",
|
|
7
|
+
"source_commit": "ee45fc71b6ae43fb6472ac38f34d0e9dec14404d",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-23T16:19:27.558Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 517,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
14
|
"functional_verbatim": 2386,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2929,
|
|
20
|
+
"tree_sha256": "8713bc7853b04fa457c4d48cbc0c13dc443de6dd160e398ad5c02723c17961bb",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2129,10 +2129,15 @@
|
|
|
2129
2129
|
"mode": "0000644",
|
|
2130
2130
|
"sha256": "2c80244a082fb8c907bc83690724dbfb775adc35da124977668d8e56425d99b2"
|
|
2131
2131
|
},
|
|
2132
|
+
{
|
|
2133
|
+
"path": "docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md",
|
|
2134
|
+
"mode": "0000644",
|
|
2135
|
+
"sha256": "8a59c1aa7f81d335e96c6305bb4f22689c4a0fee627eecdec9585e76a3477bea"
|
|
2136
|
+
},
|
|
2132
2137
|
{
|
|
2133
2138
|
"path": "docs/adr/README.md",
|
|
2134
2139
|
"mode": "0000644",
|
|
2135
|
-
"sha256": "
|
|
2140
|
+
"sha256": "465a6e705a62c9d57ccca883d11ef09afa22e39dfdcb42f572c09a7411aef475"
|
|
2136
2141
|
},
|
|
2137
2142
|
{
|
|
2138
2143
|
"path": "docs/api-reference.md",
|
|
@@ -2702,7 +2707,7 @@
|
|
|
2702
2707
|
{
|
|
2703
2708
|
"path": "docs/module-api-changelog.md",
|
|
2704
2709
|
"mode": "0000644",
|
|
2705
|
-
"sha256": "
|
|
2710
|
+
"sha256": "fa27fc84bcbf5bb7c86dc1fc4ff9aaa72ba27a1716c78e62cce7a1c3ce8b3f30"
|
|
2706
2711
|
},
|
|
2707
2712
|
{
|
|
2708
2713
|
"path": "docs/modules-contract.md",
|
|
@@ -4922,7 +4927,7 @@
|
|
|
4922
4927
|
{
|
|
4923
4928
|
"path": "modules/hall-ui/public/goal-inbox.js",
|
|
4924
4929
|
"mode": "0000644",
|
|
4925
|
-
"sha256": "
|
|
4930
|
+
"sha256": "b64ea71dc70993e2d3f8e728a0c3ff6450ffb14d8bc84ea7fb6825f9758aa089"
|
|
4926
4931
|
},
|
|
4927
4932
|
{
|
|
4928
4933
|
"path": "modules/hall-ui/public/goal-map-lib.js",
|
|
@@ -8142,12 +8147,12 @@
|
|
|
8142
8147
|
{
|
|
8143
8148
|
"path": "package-lock.json",
|
|
8144
8149
|
"mode": "0000644",
|
|
8145
|
-
"sha256": "
|
|
8150
|
+
"sha256": "f3b5649999aab68e1fce91958eaaca189b5416153cf2ec03e875270af08201fb"
|
|
8146
8151
|
},
|
|
8147
8152
|
{
|
|
8148
8153
|
"path": "package.json",
|
|
8149
8154
|
"mode": "0000644",
|
|
8150
|
-
"sha256": "
|
|
8155
|
+
"sha256": "35ae208891acb8d39254b253897becb92a3a7edc7723088cd567f235b23ea75b"
|
|
8151
8156
|
},
|
|
8152
8157
|
{
|
|
8153
8158
|
"path": "public-docs/index.html",
|
|
@@ -10172,7 +10177,7 @@
|
|
|
10172
10177
|
{
|
|
10173
10178
|
"path": "src/module-api.js",
|
|
10174
10179
|
"mode": "0000644",
|
|
10175
|
-
"sha256": "
|
|
10180
|
+
"sha256": "3401e9932ded1fa4ee698228b446607fc67a82978f9c2bb31c6e4e5377876f03"
|
|
10176
10181
|
},
|
|
10177
10182
|
{
|
|
10178
10183
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13647,7 +13652,7 @@
|
|
|
13647
13652
|
{
|
|
13648
13653
|
"path": "tests/rot_inbox.mjs",
|
|
13649
13654
|
"mode": "0000644",
|
|
13650
|
-
"sha256": "
|
|
13655
|
+
"sha256": "7611e8595f5ea2b314d92d756c15ba8dfc6843daf32c0130e00ed13a24fe38b8"
|
|
13651
13656
|
},
|
|
13652
13657
|
{
|
|
13653
13658
|
"path": "tests/route_permission_check.mjs",
|
package/docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# ADR 0328 — A hosted project's authority is its membership row, not its owner column
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-09-22
|
|
5
|
+
- **Task:** [task 1003882](https://cloudbongos.com/builders#/task/1003882) (goal 1000094 — BONGOS-V2)
|
|
6
|
+
- **Deciders:** the scoping call — "every API action must name its project; a non-member may only apply" — is the owner's, recorded 2026-09-13 on the task. This ADR is the mechanism that carries it out: where the underlying membership data already lives, what a route must call to consult it, and what a non-member is told.
|
|
7
|
+
- **Related:** [ADR 0016](0016-trust-boundary-server-enforced-permissions.md) (rank is server-side, uncached — the model this composes with, never replaces), [ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3 (the ownership axis this composition mirrors), [ADR 0083](0083-modular-architecture-strangler-build-plan.md) (modules cooperate through named ports, never by importing each other — the rule that shapes Decision 2), [ADR 0141](<redacted>.md) / [0201](0201-cross-project-application-vouched-row-live-record.md) / [0247](0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md) (the membership table and the join flow this decision routes *into*, not around), [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) (`client_id = slug` at `origin = domain`, the join key this whole decision turns on), [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) (names `provisioning_instances` as the fleet registry this ADR gates)
|
|
8
|
+
- **Companion (not superseded, not included):** [task 1003394](https://cloudbongos.com/builders#/task/1003394), the `requireRank` → `requirePermission` migration. Different axis, different question: that migration asks *what may this rank do at all*; this ADR asks *who may act on this one row*.
|
|
9
|
+
- **Filed by this ADR:** [task 1004159](https://cloudbongos.com/builders#/task/1004159) (extract `requireProjectOwner`, one gate replacing the three existing local implementations across 14 routes) · [task 1004160](https://cloudbongos.com/builders#/task/1004160) (route-rank-check.js project-scope classification) · [task 1004161](https://cloudbongos.com/builders#/task/1004161) (the `platform-identity.projectMembershipKind` port + multi-member widening) · [task 1004162](https://cloudbongos.com/builders#/task/1004162) (the 403 + apply-path refusal for a `pending`/`visitor` caller)
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
**What "project" means here, precisely — because the codebase has two candidates and they answer different questions.**
|
|
14
|
+
|
|
15
|
+
1. `modules/provisioning/` — the hub's **fleet registry**. `provisioning_instances` is one row per hosted instance: `id` (bigint), `slug`, `domain`, `owner_builder_id`, lifecycle status, cost estimate. [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) confirms this IS the whole fleet control plane — there is no separate registry to design.
|
|
16
|
+
2. `modules/platform-identity/` — the hub's **federated membership ledger**. `<redacted>` is `(client_id, github_id, membership_kind)` with `membership_kind ∈ {owner, member, visitor, pending}`. Per [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md), a control-plane project is projected onto this catalog as `client_id = slug` at `origin = https://<domain>` — the same `slug` `provisioning_instances` already carries. `listMembershipsForAccount()` backs `GET /my-projects`; `POST /my-projects/join` (ADR 0141 §3 / 0201 / 0247) is the existing, shipped **apply flow**.
|
|
17
|
+
|
|
18
|
+
**The composition this ADR needs already exists once, in the opposite direction.** `modules/provisioning/provisioning.js:projectOwnedBy(db, { slug, host, builderId })` is published as the kernel port `provisioning.projectOwnedBy` (task 1003580, registered in `modules/provisioning/seams.js`), so that `modules/platform-identity/invite-authz.js` can ask "is this builder the owner of the control-plane project this catalog row names" **without importing provisioning** (ADR 0083's one-way rule — modules cooperate through named ports, never by requiring each other). `invite-authz.js`'s `requireProjectInviter` is the working proof this shape is sound: ownership is asked first, mounted after `requireBuilder`, resolved via `resolveOptional` (graceful no-op when provisioning isn't installed), and on any "no" — including "no such project" — it falls through unchanged to the ordinary permission gate, so a stranger cannot distinguish a private project from a nonexistent one by the shape or timing of the refusal.
|
|
19
|
+
|
|
20
|
+
**The gap is not that this pattern is missing. It is that the module which OWNS the fact does not use it on itself, and has grown THREE slightly different local copies of it instead of one.** `auth.js:264`'s `requireBuilder` only ever answers "who are you" — it has no project dimension, by design (it is a KERNEL_FILE and provisioning is a module, ADR 0083). But provisioning's own 14 `/provisioning/instances/:id...` routes (the full, verified list: `disconnect.js:26`; `env-manifest.js:105,128,156,184`; `provisioning.js:605,685,773,820,854,993,1041,1094,1238` — `force-teardown` at `provisioning.js:256` is excluded, correctly gated separately on `requirePermission('provisioning.fleet.manage')`, archon-only) never call `provisioning.projectOwnedBy` through a composed gate. Instead there are **three redundant, hand-maintained implementations of "owner OR archon"**, none visible to `route-rank-check.js`, none consulting `platform-identity`:
|
|
21
|
+
|
|
22
|
+
- `provisioning.js:596`'s `instanceAccessDenied(inst, builder)` — a local function, own-or-archon, used by 6 of the 9 routes in that file (`:605`, `:685`, `:773`, `:820`, `:854`, `:1041`).
|
|
23
|
+
- `env-manifest.js:32`'s `resolveOwnInstance(req, res)` — a second, independently-written local function, same own-or-archon rule, used by all 4 routes in that file.
|
|
24
|
+
- `disconnect.js:31-33` — a third copy of the identical own-or-archon comparison, inlined rather than extracted, because that file has only the one route.
|
|
25
|
+
|
|
26
|
+
**And three routes have neither** — `provisioning.js:993` (teardown), `:1094` (domain), `:1238` (github-app) each inline the bare comparison with **no archon override at all**:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
30
|
+
if (!inst || String(inst.owner_builder_id) !== String(req.builder.id)) {
|
|
31
|
+
return res.fail('instance_not_found', 404);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This is not merely undocumented duplication — it is an **existing inconsistency between sibling routes in the same module**: an archon can already read, edit settings, restart, or manage invites on someone else's project for fleet support (the 11 routes above), but cannot tear it down, change its domain, or connect its GitHub App through the same API an owner uses, even though `force-teardown` proves the intent to let an archon act on any instance already exists one route over. Fourteen decisions to trust, expressed three ways, one of them narrower than the rest — and none of the three implementations is visible to `route-rank-check.js`, exactly what "Why now" describes: `admission.policy: open` means any GitHub sign-up reaches `any-builder`, and a route whose only defense is a correctly-typed `if` a few lines into its handler is one refactor away from silently losing that line, with nothing structural to catch it.
|
|
36
|
+
|
|
37
|
+
**And a single `owner_builder_id` column cannot express what the owner's decision asks for even when present.** `<redacted>` already has a `member` kind — a team sharing one paid hosted instance — completely unconsulted by the surface that would need to enforce it.
|
|
38
|
+
|
|
39
|
+
## Decision
|
|
40
|
+
|
|
41
|
+
### 1. Project scope is carried exactly as it is today — the path segment — resolved through the slug, not a new identifier
|
|
42
|
+
|
|
43
|
+
No new header, no session binding, no new CLI convention. `/provisioning/instances/:id/...` already names its project in the URL, and every caller — the hall web UI, or a terminal typing against the HTTP API directly — already reaches it by that path segment. ([ADR 0324](0324-the-public-npm-cli-channel-is-retired.md) retired the public `@cloudbongos/cli` npm channel; that is a distribution-channel decision about how a terminal gets its commands, not about how a request names its project, so it does not change this answer.) `getInstanceById` already returns `{ id, slug, owner_builder_id, domain, ... }`, so the slug needed for membership lookups (keyed on `client_id = slug`) is already one query away — no second identifier to introduce or keep in sync.
|
|
44
|
+
|
|
45
|
+
### 2. `requireBuilder` composes with project authority through a helper that reuses the existing `provisioning.projectOwnedBy` composition shape — Phase 1 needs no new port at all
|
|
46
|
+
|
|
47
|
+
**Phase 1 (consolidate three implementations into one, and correct the inconsistency between them — task 1004159, no schema change, no new port).** Provisioning already holds `owner_builder_id` directly; it needs no cross-module port to check its own column. Extract one local helper, `requireProjectOwner(loadFn, { archonOverride = true } = {})`, mirroring `requireProjectInviter`'s composed-gate shape (mounted after `requireBuilder`, loads the row, admits on ownership match OR — by default — a plain **rank** check, `req.builder.rank === 'archon'`, denies 404 `instance_not_found` fail-closed on every other path). **This must be the exact rank test `instanceAccessDenied` and `resolveOwnInstance` already run — not `requirePermission('provisioning.fleet.manage')`.** The two are not interchangeable: that permission's floor is `metic` (ADR 0157 lowered it from `archon`, `provisioning.js:252-253`), used only by the separately-gated `force-teardown` route; wiring it into `requireProjectOwner` instead of the plain rank test would silently widen all 14 routes — teardown and disconnect included — from archon-only to metic-and-up, an unstated widening this ADR's whole posture forbids. With the correct rank test, all 14 routes point at the one helper: the 11 that already resolve to "owner OR archon" today (via `instanceAccessDenied`, `resolveOwnInstance`, or `disconnect.js`'s inline copy) get a mechanical, behavior-preserving swap; the 3 that resolve only to "owner" today (`teardown`, `domain`, `github-app`) get the SAME rank-based default the other 11 already carry — an explicit, named widening, not an accident: it makes those three routes consistent with the fleet-support access every sibling route in the same module already grants, closing the gap `force-teardown`'s own existence already implies was intended. **Every archon action taken on a project the archon does not own — through the widened routes or the existing ones — must write a `provisioning_events` row naming the archon actor**, the same `actor: 'api:archon:<id>'` convention the existing routes already use for their own-vs-archon audit trail (`provisioning.js:712,883,1048`); task 1004159's acceptance criteria pin this for the 3 newly-widened routes specifically, since they have no such event today.
|
|
48
|
+
|
|
49
|
+
**Phase 2 (the multi-member widening — task 1004161).** Extend the same helper to resolve a NEW port, `platform-identity.projectMembershipKind(clientId, githubId) → Promise<'owner'|'member'|'visitor'|'pending'|null>` — a thin wrapper over the already-correct `getMembership()` — via `resolveOptional`, admitting on `owner` or `member`. **This is only as trustworthy as who may write a `member` row, which this ADR consumes and does not change or re-audit.** The one write path that sets `membership_kind` to a value other than the sign-in default (`POST /sso/membership/check-in`, `modules/platform-identity/routes/sso.js`) is authenticated by the PROJECT's own `client_id`/`client_secret` — not a builder's session — and is update-only against a row that already carries a hub-witnessed sign-in (`touchMembership`'s `WHERE ... signin_verified_at IS NOT NULL`). So a `member` row on the hub reflects what the FEDERATED PROJECT's own server reported about its own builder, under the trust model ADR 0141/0201/0247 already established for federated check-ins generally. This ADR's gate inherits that existing trust boundary as-is for provisioning's admission decision; it neither tightens nor loosens it, and does not itself re-verify the check-in path's authorization. **The fallback is scoped narrowly, and the two failure shapes are NOT the same decision:** when the port is unregistered (a self-hosted instance without `platform-identity`), fall back to the Phase 1 direct-column check — the ADR 0083 "graceful degradation on a module-less instance" contract, and the correct answer there, since such an instance has no fleet of *other people's* projects to protect. When the port IS registered but the call throws or resolves an unrecognized value (a hub where the port should be answering and is not), **deny** — do not silently widen the fallback to cover a runtime fault on infrastructure that is supposed to be present. Task 1004161's acceptance criteria must test both paths separately.
|
|
50
|
+
|
|
51
|
+
This is a **new, parallel axis**, not a fifth branch bolted onto `requirePermission` — exactly as the existing ownership axis (`authorizeOwned`/`authorizeOwnedOrAny`, [ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3, R100) is kept structurally separate from the plain permission resolve, for the same reason: a fact `requireBuilder` cannot know (which concrete row, and who stands on it) must be supplied by the handler's own loaded row, and folding it into `requirePermission` would silently drop that second fact for every other caller of that gate.
|
|
52
|
+
|
|
53
|
+
### 3. What a non-member gets depends on what they already are, not one blanket code
|
|
54
|
+
|
|
55
|
+
Today's blanket `instance_not_found` (404) hides existence for every non-owner. That floor is correct for a total stranger and does not change.
|
|
56
|
+
|
|
57
|
+
- **No membership row at all** → stays **404** `instance_not_found`. This is the same posture `invite-authz.js` already guarantees for its own ownership question — a caller who owns nothing and holds no membership learns nothing about whether the project exists.
|
|
58
|
+
- **A `pending` or `visitor` row exists** (task 1004162, sequenced after the port lands) → **403** `project_membership_required`, naming the concrete next step: `POST /my-projects/join` if `pending` isn't already filed, or "ask the project owner" if it is. This is the "apply to join it" the owner's decision names — routed into the flow ADR 0141/0201/0247 already ships, not a second one invented for provisioning. The distinction is not cosmetic: a row already existing means the caller's relationship to this project is already on record (visible on their own `GET /my-projects`), so confirming its existence tells them nothing they could not already see.
|
|
59
|
+
- **`owner` or `member`** → admitted, as today (owner) or newly (member).
|
|
60
|
+
|
|
61
|
+
### 4. `route-rank-check.js` gains a project-scope dimension, structurally, the way it already reads permission floors
|
|
62
|
+
|
|
63
|
+
The existing check parses `requirePermission('key')` call sites out of route source to classify rank (its own header: "classification is STRUCTURAL... a route is gated when its middleware chain carries requireRank/requireBuilder/requirePermission"). Task 1004160 adds the same structural read for `requireProjectOwner(...)`:
|
|
64
|
+
|
|
65
|
+
- A route matching an explicit path-prefix list (starting with `/provisioning/instances/:id`, the same posture `KNOWN_RANKS` already takes — new project-scoped surfaces opt in by adding their prefix, not by a heuristic) whose middleware chain does **not** call `requireProjectOwner(...)` classifies as a new finding, `project_scope_missing`: write method → `blocker` (hard FAIL, the same severity a missing rank gate gets today); read method → `major` → `concerns` (the Hacker adjudicates, same as an unclassifiable rank today). This is an explicit opt-in list, not a pattern match on "any route that loads a project row" — the same residual blind spot `KNOWN_RANKS` already accepts (a genuinely new project-scoped surface under an un-listed prefix is silently unclassified until someone adds its prefix), recorded as a Consequence below rather than solved here.
|
|
66
|
+
- A route with no project dimension (`GET /provisioning/fleet`, `GET /provisioning/health`, `GET /provisioning/slug-available`, and `POST /provisioning/instances/:id/force-teardown` — archon-only via `requirePermission('provisioning.fleet.manage')` and deliberately NOT owner-or-member) is excluded by not matching the prefix list — no annotation needed, mirroring how a route outside `/goals/` needs no goal-rank comment today.
|
|
67
|
+
|
|
68
|
+
This closes exactly the gap "Why now" names: a route added later that touches `provisioning_instances.:id` and forgets the gate fails the ship gate instead of silently joining the fourteen.
|
|
69
|
+
|
|
70
|
+
### 5. Migration order: the 14 evidenced routes first; the "96 write routes" figure is NOT this ADR's scope
|
|
71
|
+
|
|
72
|
+
The task's own point 5 asks for migration order "for the 96 write routes." This ADR deliberately answers a narrower question than literally asked, and says so rather than leaving a reader to reconcile the two numbers: the 96-route figure is the whole app's `any-builder` write-route census, not project-scoped routes specifically. The overwhelming majority are ordinary Bongos task/goal/claim actions on a **single-tenant instance's own board**, which [ADR 0100](0100-two-repo-instance-model.md) §3 already isolates one-database-per-repo — there is no "wrong project" a single-tenant board's own task-claim route could reach, so it has no project dimension to check. Re-scoping every write route to "name its project" would be re-litigating ADR 0100, not implementing this decision, so the migration order below covers only the routes that actually have a project to name.
|
|
73
|
+
|
|
74
|
+
**The actual blast radius is the 14 `/provisioning/instances/:id...` routes** named in Context, all in one module, all already loading the same row. Order, smallest-risk first:
|
|
75
|
+
|
|
76
|
+
1. Task 1004159 lands the structural helper and converts all 14 call sites — a mechanical, behavior-preserving swap for 11 of them, and a stated, intentional widening (the archon override) for the other 3. The owner/stranger cases for the 11 already-gated routes have existing coverage to preserve; the archon case for the 3 newly-widened routes (`teardown`, `domain`, `github-app`) has none and must be written new — this ADR does not assert existing coverage it has not verified. Those 3 routes' `provisioning.recordEvent` calls currently hardcode `actor: 'api:self'` (correct today, since only the owner can reach them); once an archon can, that becomes a false audit record unless it is fixed to the same `isOwner ? 'api:self' : 'api:archon:<id>'` pattern the other 11 routes already use.
|
|
77
|
+
2. Task 1004160 (`route-rank-check.js`) lands only AFTER 1004159 — turning on a hard-fail check for a gate that does not exist yet would fail CI on this ADR's own landing commit. Its dependency edge in Bongos encodes this. **This ordering has a cost, named rather than hidden:** between the two landings, a new `/provisioning/instances/:id...` route could ship with no structural gate and nothing would catch it — the same exposure that exists today, just not yet closed. The window is bounded by how quickly 1004160 follows 1004159, not eliminated by the ADR.
|
|
78
|
+
3. Task 1004161 (the membership port + multi-member widening) depends only on 1004159 (one call site to widen, not fourteen) and can land in parallel with 1004160.
|
|
79
|
+
4. Task 1004162 (the refined 403/404 split) depends on 1004161 — the distinction it draws does not exist until the gate can tell "no row" from "a weaker row."
|
|
80
|
+
|
|
81
|
+
**The permanent `/api/gds` + `/api/bongos` aliases** carry no new promise here: both alias to the same route table, so a route gated by `requireProjectOwner` is gated identically under either prefix. Nothing about this decision changes the route table's shape, only what one more piece of middleware in the existing chain checks.
|
|
82
|
+
|
|
83
|
+
## Consequences
|
|
84
|
+
|
|
85
|
+
- **A team can share one paid hosted instance for the first time**, once task 1004161 lands — `member`-kind rows already exist in the schema and are already correct; they are simply not consulted yet by the surface that would need them.
|
|
86
|
+
- **An archon gains fleet-support reach on `teardown`, `domain`, and `github-app` that does not exist today**, once task 1004159 lands — a stated widening, matching the access every other project-scoped route in the same module already grants, not a side effect discovered later.
|
|
87
|
+
- **Three redundant local implementations (`instanceAccessDenied`, `resolveOwnInstance`, `disconnect.js`'s inline copy) collapse into one function**, auditable by `route-rank-check.js` the same way a missing rank check already is — the exact mechanism "Why now" says does not exist today.
|
|
88
|
+
- **A self-hosted instance running `provisioning` without `platform-identity` installed is unaffected**: the port's `resolveOptional` fallback is the existing owner-only check, verbatim, kept as the fallback branch instead of the only branch — and a hub where the port SHOULD be answering and errors instead denies rather than silently falling back (Decision 2, Phase 2).
|
|
89
|
+
- **A per-request membership lookup on top of the existing instance load, once Phase 2 lands.** `<redacted>` is keyed `(client_id, github_id)`; task 1004161 must confirm that pair is indexed (or add the index) rather than relying on the ADR's code-cost framing alone — this ADR does not itself specify the query plan or a memoization posture for a single request that consults the same membership twice (e.g. the gate, then the handler's own actor-labeling logic); task 1004161 settles both alongside the index.
|
|
90
|
+
- **The `route-rank-check.js` classifier's path-prefix list is a residual blind spot, not a closed one.** A genuinely new project-scoped surface mounted under a prefix nobody added to the list is silently unclassified rather than flagged, exactly as an unlisted rank pattern is today — the same trade-off `KNOWN_RANKS` already accepts, not a new one this ADR introduces.
|
|
91
|
+
- **No schema migration for Phase 1, and none for Phase 2 either.** `<redacted>` and its `owner`/`member`/`visitor`/`pending` vocabulary already exist and are already correct; this ADR's mechanism cost is one local helper (Phase 1), one seam port (Phase 2), and one `route-rank-check.js` pattern addition — the code cost. The query cost (the point above) is separate and is task 1004161's to confirm.
|
|
92
|
+
|
|
93
|
+
## Alternatives considered
|
|
94
|
+
|
|
95
|
+
- **A new `provisioning_instance_members` table, scoped to provisioning alone.** Rejected: `<redacted>` already answers this exact question for the exact same `slug`/`client_id`, per ADR 0246's own projection rule. A second table would repeat the second-fleet-registry mistake ADR 0325 already refused for the registry itself, one layer down.
|
|
96
|
+
- **Fold project authority into `requirePermission` as a fifth axis.** Rejected for the same reason the ownership axis is its own entry point ([ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3, R100): project standing is parameterized by a concrete row `requirePermission` is never handed, so admitting on a bare permission key would collapse per-project standing into "holds the key everywhere" — precisely the widening the ownership axis exists to prevent for `.own` keys.
|
|
97
|
+
- **Migrate all 96 `any-builder` write routes under one project-scope umbrella.** Rejected: the other ~82 routes act on a single-tenant instance's own Bongos data, which has no project dimension per ADR 0100 — applying this decision there would be inventing multi-tenancy for a system deliberately built single-tenant-per-repo, a far larger and unrelated call the owner's one-line decision does not ask for.
|
|
98
|
+
- **Drop the 404 existence-hiding and return 403 for every unauthorized instance id.** Rejected: a stranger with no standing on a project should learn nothing about whether it exists, exactly as `invite-authz.js`'s existing ownership refusal already guarantees. Only a caller who already has a membership row (however weak) is told anything more, because the row itself is proof they already know the project exists.
|
package/docs/adr/README.md
CHANGED
|
@@ -419,3 +419,4 @@ This keeps the decision history honest and traceable.
|
|
|
419
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 |
|
|
420
420
|
| 0326 | [**An upgrade may ADD a deny rule to a file the instance owns** ([task 1004156](https://cloudbongos.com/builders#/task/1004156), goal 1000090 — BONGOS-V2). Task 1003966 gave the instance starter its deny rules — no reading `.env` files, keys or session tokens, no shelving with the repo-global stash — and they reached **nobody who was already here**. The starter is written only when `.claude/settings.json` is ABSENT, and `bongos upgrade` materializes from the published package, which excludes that file on purpose; so an upgrade always takes the starter branch, always finds a settings.json already there, and left it alone. Every instance on the allow-only starter from task 1003962 — the live one included — kept it forever. That was deliberate ("rewriting a file the instance owns is the line task 1003962 drew"), and the recorded remedy was manual: re-run `bongos init`, or delete the file. **A manual remedy for a security gap protects the instances whose owners read release notes, which is not the set that needs protecting.** The owner's call: ADD the missing deny rules and NAME each one in the run output. The argument is that a deny rule is **monotonic in the safe direction** — an allow rule widens what the local agent may do, a deny rule can only narrow, so appending one cannot make any instance less safe than it was a moment earlier. That is what makes this the one merge worth doing without asking per-instance, and it extends [ADR 0121](0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md) §2's already-licensed "merge OR leave-and-report" from `--adopt` to every materialize. "Never clobber" survives in the sense that was load-bearing: **nothing the instance wrote is removed, reordered or rewritten** — their allow rules, own denies, `ask` list, hooks, env and every key outside `permissions` come back exactly as they were, with new rules appended after. Deliberately NOT done: **no provenance**, so an owner who deletes a rule we add gets it back next upgrade and (deny beating allow in Claude Code's order) cannot override it with an allow — rejected as speculative machinery for a case nobody has hit, revisit on a real complaint; a file that will not parse, or whose `permissions`/`permissions.deny` is not the expected shape, is left BYTE-FOR-BYTE alone and reported, the posture task 1004023 took for the core's own file, because a half-understood rewrite of a permissions file is worse than the gap; and **the allow half is never topped up** — the safety argument is directionality and does not survive being applied to a rule that grants. `materializeClaude` gains `summary.settingsDenyAdded` + `settingsDenyUncheckable`, both printed through `log()` since `upgrade.js` does not iterate `summary.notes`. The test asserting an instance's settings.json comes back byte-identical is superseded by a strictly stronger one — every rule still there, unmoved, deny-only growth — the weaker spelling being what froze the gap in place. **AMENDED 2026-09-22 by [task 1004158](https://cloudbongos.com/builders#/task/1004158): the two shelf rules are now starter-only and the top-up asks for the CREDENTIAL half alone.** 0326 itself named this exclusion as the remedy if the rule ever bit; the owner took it ahead of that, because the monotonicity argument licenses a rule about reading a secret and does not reach one that guards a workflow — with no provenance and deny beating allow, an instance that loses the shelf cannot give it back. A fresh instance still gets both. The top-up only ever appends, so this is **forward-looking only**: an instance that already took them keeps them, live included.](0326-an-upgrade-may-add-a-deny-rule-to-a-file-the-instance-owns.md) | security / instance onboarding |
|
|
421
421
|
| 0327 | [**A `cloud-host` runs on the owner's own account, and the key is borrowed, not stored** ([task 1004175](https://cloudbongos.com/builders#/task/1004175), goal 1000106 — BONGOS-V2). [ADR 0323](0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md) moved a `cloud-host` to Render to stop being a landlord but left **whose account** unexamined; [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md) §4 then ruled the platform token is ours and "never per-customer". Standing it up disproved the premise: a Render API key is issued against a **user** and carries their rights in **every workspace they belong to** — the owner's reached three, one of them another builder's, holding their live production. The same check found `mercury` **already on Render**, run by its own owner, which 0323 would have migrated OFF a correct setup onto our bill. The remaining landlord relationship was never the servers; it was the **account**. **Decisions:** (1) each project brings its own platform key — we hold no billing relationship for anyone's compute, and a leaked key's blast radius is exactly its own owner's account, a property that comes from the decision and not from cryptography; (2) the key is **borrowed for standup and cleared**, never stored, because nothing afterwards needs it — Render auto-deploys from Git, the deploy door ([ADR 0293](0293-the-owners-deploy-door-is-an-intent-the-control-plane-drains.md)) is a push, liveness is the [ADR 0252](0252-the-self-hosted-visibility-report-is-a-pull-and-auto-appear-buys-a-retraction-lever.md) pull — so it rides the queue as a short-lived value encrypted with the existing `secret-box.js`, the [ADR 0133](0133-github-app-manifest-onboarding-signin.md) handshake's shape; (3) we **never destroy a customer's infrastructure** — teardown disconnects, the owner deletes their own services, a safety property first and the reason (2) works; (4) **two shapes, not three** (`dedicated-cloud-host` removed — on someone's own account a bigger plan is a setting, finishing 0323 §1's own "plan size, not a second code path"), and the **free tier is free**, 0323 §7's cap retired with its premise. **Deliberately NOT built, because nothing is stored:** an asymmetric sealing module, per-row credential binding, rotation/revocation, and the separate-unix-user precondition — each designed in full, then deleted when the retention question was asked instead of assumed. **Stated honestly:** encrypting the in-transit value protects it from a database dump, NOT from a compromised web tier, because the owner's key arrives there in plaintext first; that defence was never available at any retention length. **§5 records a measurement, fixed elsewhere:** [ADR 0111](0111-instance-hosting-provisioning-module.md) §2's boundary is enforced by the CODE, not the kernel — `cloudbongos.service` runs `User=lars` with no sandboxing and `/etc/cloudbongos/box.env` is mode 600 owned by `lars`, so the web tier CAN read the DigitalOcean, Cloudflare and platform tokens; 0325 §4's "root-only" is not what is on the box. (A second finding in the same section — a "mode 777" key file — was **void and is corrected in place**: the path is a symlink, whose mode display is meaningless, to a 600 target. It was inferred from an `ls` rather than an attempted read.) **Resolved 2026-09-23 by [task 1004176](https://cloudbongos.com/builders#/task/1004176)**, and more narrowly than §5 proposed — not the unix split (which would mean relocating the checkout, config dir, `web.env` and the secret-box master key the web tier legitimately needs), but a systemd drop-in with `InaccessiblePaths`, verified as the service user; proving it as **root** reports "no effect" against a working config, because `CAP_DAC_OVERRIDE` bypasses the mode-000 over-mount. It is emphatically NOT a precondition here — that is the point of storing nothing, and had we sealed per-project keys with a private key in that same file the two would have had to land together. **Consequences:** the migration shrinks to an **adoption**; the cost ledger's question inverts (our spend on a `cloud-host` is ~\$0 and correct — what an owner is worth telling is THEIR Render cost, a different column, [task 1004148](https://cloudbongos.com/builders#/task/1004148)); and metering loses its forcing function, cost *visibility* surviving as a feature while billing for hosting does not ([task 1001949](https://cloudbongos.com/builders#/task/1001949)). Rejected: storing each key sealed with a key pair (three days plus a live-site infra change, to keep a value needed once); one platform account holding every project (0325 §4 as written — keeps us the billing landlord and makes one credential reach every customer); generating a Blueprint and having the owner click once in Render (simplest, but the click is the exact friction `cloud-host` exists to remove); and keeping the key in the browser to call Render directly (standup is a runner job with retries and an audit trail, not a page that stays open).](0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md) | hosting / provisioning / credentials |
|
|
422
|
+
| 0328 | [**A hosted project's authority is its membership row, not its owner column** ([task 1003882](https://cloudbongos.com/builders#/task/1003882), goal 1000094 — BONGOS-V2). The owner's decision (2026-09-13): every API action must name its project, and a non-member may only apply. Two "project" concepts exist and answer different questions: `provisioning_instances` (the hub's fleet registry, single `owner_builder_id`, [ADR 0325](0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md)) and `<redacted>` (`client_id, github_id, membership_kind ∈ {owner,member,visitor,pending}`, already correct, already backing `GET /my-projects`). The composition this needs already exists once, in the opposite direction: `provisioning.projectOwnedBy` is a published port (task 1003580) that `platform-identity/invite-authz.js` already consumes via `resolveOptional` to ask "does this builder own this project" without importing provisioning (ADR 0083). **The gap is that the module which owns the fact does not use it on itself, and has grown three separate local copies of it instead of one** — `instanceAccessDenied` (6 routes), `resolveOwnInstance` (4 routes) and `disconnect.js`'s own inline copy (1 route) all implement "owner OR archon", while 3 routes (`teardown`, `domain`, `github-app`) implement only "owner" with no archon override at all — an existing inconsistency between sibling routes in the same module, not just duplication, and none of the three implementations is visible to `route-rank-check.js`. Decision: (1) project scope stays the existing path segment, resolved through `slug` (the `client_id = slug` join key [ADR 0246](0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md) already establishes); (2) Phase 1 extracts one local `requireProjectOwner` helper replacing all three implementations — a behavior-preserving swap for 11 routes and a stated, intentional archon-override widening for the 3 that lacked it — no new port needed since provisioning already holds the column; Phase 2 adds a new port `platform-identity.projectMembershipKind` and widens the same helper to admit `owner` OR `member`, falling back to the Phase 1 column check when platform-identity isn't installed (ADR 0083's degrade-gracefully contract) — a NEW parallel axis, not a fifth branch on `requirePermission`, mirroring why the ownership axis ([ADR 0151](0151-governance-permissions-as-atom-ranks-as-roles.md) §3, R100) is kept separate; (3) a caller with NO membership row still gets 404 `instance_not_found` (existence-hidden, unchanged), but a `pending`/`visitor` row gets 403 `project_membership_required` naming `POST /my-projects/join` (ADR 0141/0201/0247's existing apply flow) — the "apply to join it" the owner's decision names, routed into the flow that already ships rather than a second one; (4) `route-rank-check.js` gains a `project_scope_missing` finding (blocker on write, concerns on read) for any `/provisioning/instances/:id...` route missing the gate, the same structural read it already gives `requirePermission`; (5) migration order is the 14 evidenced routes only — the task's own "96 write routes" figure is the whole app's census, and the other ~82 act on a single-tenant instance's own Bongos board, which [ADR 0100](0100-two-repo-instance-model.md) §3 already isolates one-database-per-repo and has no project dimension to name. Filed: [task 1004159](https://cloudbongos.com/builders#/task/1004159) (the helper) · [task 1004160](https://cloudbongos.com/builders#/task/1004160) (route-rank-check.js, depends on 1004159) · [task 1004161](https://cloudbongos.com/builders#/task/1004161) (the membership port, depends on 1004159) · [task 1004162](https://cloudbongos.com/builders#/task/1004162) (the 403/404 split, depends on 1004161). Rejected: a second `provisioning_instance_members` table (the membership table already exists and joins on the same key — a second one repeats the mistake ADR 0325 already refused); folding project authority into `requirePermission` as a fifth axis (same reasoning R100 already applied to `.own` keys); migrating all 96 write routes (re-litigates ADR 0100's single-tenant model, not this decision); dropping 404 existence-hiding entirely (a total stranger should learn nothing, exactly as `invite-authz.js` already guarantees).](0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md) | security / authorization / provisioning |
|
|
@@ -2428,5 +2428,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2428
2428
|
landed since 1.19.975 with no explicit bump. run 35878415178. (task 1002620)
|
|
2429
2429
|
1.19.977 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2430
2430
|
landed since 1.19.976 with no explicit bump. run 35880930697. (task 1002620)
|
|
2431
|
+
1.19.978 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2432
|
+
landed since 1.19.977 with no explicit bump. run 35882259144. (task 1002620)
|
|
2433
|
+
1.19.979 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2434
|
+
landed since 1.19.978 with no explicit bump. run 35887876013. (task 1002620)
|
|
2431
2435
|
---------------------------------------------------------------------------
|
|
2432
2436
|
```
|
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
const more = count > 5 ? `<p class="ginbox__pending">…${count - 5} more awaiting a nod.</p>` : '';
|
|
279
279
|
box.innerHTML = '<h3 class="ginbox__sec-h">Reported work awaiting a nod '
|
|
280
280
|
+ `<span class="count-pill">${count}</span></h3>`
|
|
281
|
-
+ `<ul class="
|
|
281
|
+
+ `<ul class="ginbox">${rows.slice(0, 5).map(nodRowHtml).join('')}</ul>${more}`;
|
|
282
282
|
}
|
|
283
283
|
window.OTBAttention?.refresh();
|
|
284
284
|
}
|
|
@@ -358,7 +358,7 @@
|
|
|
358
358
|
const more = count > 5 ? `<p class="ginbox__pending">…${count - 5} more awaiting an answer.</p>` : '';
|
|
359
359
|
box.innerHTML = '<h3 class="ginbox__sec-h">Was this removed? '
|
|
360
360
|
+ `<span class="count-pill">${count}</span></h3>`
|
|
361
|
-
+ `<ul class="
|
|
361
|
+
+ `<ul class="ginbox">${rows.slice(0, 5).map(removalRowHtml).join('')}</ul>${more}`;
|
|
362
362
|
}
|
|
363
363
|
window.OTBAttention?.refresh();
|
|
364
364
|
}
|
|
@@ -457,7 +457,7 @@
|
|
|
457
457
|
: `<span class="ginbox__sub"> — untouched ${escapeHtml(String(t.rot_days ?? '?'))}+ days</span>`;
|
|
458
458
|
box.innerHTML = '<h3 class="ginbox__sec-h">Going quiet '
|
|
459
459
|
+ `<span class="count-pill">${rows.length}</span>${timerNote}</h3>`
|
|
460
|
-
+ `<ul class="
|
|
460
|
+
+ `<ul class="ginbox">${shown.map(rotRowHtml).join('')}</ul>${more}`;
|
|
461
461
|
window.OTBAttention?.refresh();
|
|
462
462
|
}
|
|
463
463
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.979",
|
|
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.979",
|
|
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.979",
|
|
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/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.979'; // 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');
|
package/tests/rot_inbox.mjs
CHANGED
|
@@ -616,6 +616,16 @@ test('row builders emit the CSS classes style.css actually defines, not ginbox__
|
|
|
616
616
|
'rotRowHtml must emit the defined ginbox__body/ginbox__actions classes');
|
|
617
617
|
});
|
|
618
618
|
|
|
619
|
+
test('the nod, removal and rot lists use the defined .ginbox list class, not ginbox__list (task 1003982)', () => {
|
|
620
|
+
assert.match(STYLE_SRC, /^\.ginbox\s*\{[^}]*list-style:\s*none/m, 'style.css must define .ginbox with list-style: none');
|
|
621
|
+
assert.doesNotMatch(GOAL_INBOX_SRC, /ginbox__list/,
|
|
622
|
+
'.ginbox__list is undefined in style.css — with it the browser default bullets and 40px indent leak through');
|
|
623
|
+
for (const fn of ['nodRowHtml', 'removalRowHtml', 'rotRowHtml']) {
|
|
624
|
+
assert.match(GOAL_INBOX_SRC, new RegExp(`<ul class="ginbox">\\$\\{[^\\n]*?map\\(${fn}\\)`),
|
|
625
|
+
`the list built from ${fn} must be <ul class="ginbox">`);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
619
629
|
test('acting on a rot line refreshes the ROT slice, not the goals list', () => {
|
|
620
630
|
assert.match(GOAL_INBOX_SRC, /ROT_TOASTS\[verb\] \|\| 'Done\.', loadRot\)/,
|
|
621
631
|
'refreshing the wrong slice leaves the acted-on row sitting there — the task-1003066 lesson');
|