@bongos/core 1.21.139 → 1.21.141
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 +31 -21
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md +29 -2
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +3 -4
- package/docs/copy-inventory.md +13 -11
- package/docs/copy-registry.json +42 -24
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +25 -25
- package/modules/hall-ui/public/goals.js +31 -8
- package/modules/lifecycle/claimable-counts.js +19 -0
- package/modules/lifecycle/routes/tasks.js +10 -10
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/hall_goals_open_first.mjs +120 -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.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.141",
|
|
6
|
+
"core_contract": "1.21.141",
|
|
7
|
+
"source_commit": "1df6b5f512d4695c82c2cf8b94ff0234d721598a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-17T04:41:31.753Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 608,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3158,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3795,
|
|
20
|
+
"tree_sha256": "8da5a6404d48fa329a19e2d6d06c7787c097c75f8641723d71c8b54fb33a9d44",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -337,7 +337,7 @@
|
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/index.d.ts",
|
|
339
339
|
"mode": "0000644",
|
|
340
|
-
"sha256": "
|
|
340
|
+
"sha256": "32473bc4b3af9c775d902e05d2ef4a9ba5ab1b76028ca3cbe9b4589ef5e75beb"
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -2102,7 +2102,7 @@
|
|
|
2102
2102
|
{
|
|
2103
2103
|
"path": "docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md",
|
|
2104
2104
|
"mode": "0000644",
|
|
2105
|
-
"sha256": "
|
|
2105
|
+
"sha256": "0a40681d78739b99df16e4207e71283d15d4b3895bf24c757c9894060254a03f"
|
|
2106
2106
|
},
|
|
2107
2107
|
{
|
|
2108
2108
|
"path": "docs/adr/0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md",
|
|
@@ -2377,7 +2377,7 @@
|
|
|
2377
2377
|
{
|
|
2378
2378
|
"path": "docs/adr/README.md",
|
|
2379
2379
|
"mode": "0000644",
|
|
2380
|
-
"sha256": "
|
|
2380
|
+
"sha256": "7efe6a2db56553a970553c66cec08e8c93aebecaa3d6fbe91702f7ab2b85cebc"
|
|
2381
2381
|
},
|
|
2382
2382
|
{
|
|
2383
2383
|
"path": "docs/api-reference.md",
|
|
@@ -2387,7 +2387,7 @@
|
|
|
2387
2387
|
{
|
|
2388
2388
|
"path": "docs/api/openapi.json",
|
|
2389
2389
|
"mode": "0000644",
|
|
2390
|
-
"sha256": "
|
|
2390
|
+
"sha256": "bcd476792cd9928a3398018992090ff5a0123750f2c87df439a346fc311945b4"
|
|
2391
2391
|
},
|
|
2392
2392
|
{
|
|
2393
2393
|
"path": "docs/architecture.md",
|
|
@@ -2412,12 +2412,12 @@
|
|
|
2412
2412
|
{
|
|
2413
2413
|
"path": "docs/copy-inventory.md",
|
|
2414
2414
|
"mode": "0000644",
|
|
2415
|
-
"sha256": "
|
|
2415
|
+
"sha256": "ee6fea80103d420481d7fa395352caf17d518a80459918575b90cc4ecb865e4b"
|
|
2416
2416
|
},
|
|
2417
2417
|
{
|
|
2418
2418
|
"path": "docs/copy-registry.json",
|
|
2419
2419
|
"mode": "0000644",
|
|
2420
|
-
"sha256": "
|
|
2420
|
+
"sha256": "115ba4452f8cbfba88bef9b9c974514e714e1af5852a2fb06f8f46dc24a457c9"
|
|
2421
2421
|
},
|
|
2422
2422
|
{
|
|
2423
2423
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -2927,7 +2927,7 @@
|
|
|
2927
2927
|
{
|
|
2928
2928
|
"path": "docs/module-api-changelog.md",
|
|
2929
2929
|
"mode": "0000644",
|
|
2930
|
-
"sha256": "
|
|
2930
|
+
"sha256": "d64fdd18ff9a02daa52471cd6c719b8218b98824b2f9b0ad6cecc57ef4d044d6"
|
|
2931
2931
|
},
|
|
2932
2932
|
{
|
|
2933
2933
|
"path": "docs/modules-contract.md",
|
|
@@ -3017,7 +3017,7 @@
|
|
|
3017
3017
|
{
|
|
3018
3018
|
"path": "docs/page-readings.json",
|
|
3019
3019
|
"mode": "0000644",
|
|
3020
|
-
"sha256": "
|
|
3020
|
+
"sha256": "18e50539ef47bd75e92eedb553130ef086077264e29fa504c748169a13642191"
|
|
3021
3021
|
},
|
|
3022
3022
|
{
|
|
3023
3023
|
"path": "docs/project-context.template.md",
|
|
@@ -5992,7 +5992,7 @@
|
|
|
5992
5992
|
{
|
|
5993
5993
|
"path": "modules/hall-ui/public/goals.js",
|
|
5994
5994
|
"mode": "0000644",
|
|
5995
|
-
"sha256": "
|
|
5995
|
+
"sha256": "92178535b69db9ded040f26961adf79ed72249634c4103decc98a63593d8261c"
|
|
5996
5996
|
},
|
|
5997
5997
|
{
|
|
5998
5998
|
"path": "modules/hall-ui/public/goals.states.json",
|
|
@@ -7129,6 +7129,11 @@
|
|
|
7129
7129
|
"mode": "0000644",
|
|
7130
7130
|
"sha256": "3586cc2aa07913870ad99073c534e70ef2e2c5ff6a5ef2ddeb141cfd397269cd"
|
|
7131
7131
|
},
|
|
7132
|
+
{
|
|
7133
|
+
"path": "modules/lifecycle/claimable-counts.js",
|
|
7134
|
+
"mode": "0000644",
|
|
7135
|
+
"sha256": "e030803cd033ad2ccebe77de0aa53a6b2b214466fb72b25324197126b20f87e3"
|
|
7136
|
+
},
|
|
7132
7137
|
{
|
|
7133
7138
|
"path": "modules/lifecycle/closeness-weights.json",
|
|
7134
7139
|
"mode": "0000644",
|
|
@@ -7697,7 +7702,7 @@
|
|
|
7697
7702
|
{
|
|
7698
7703
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
7699
7704
|
"mode": "0000644",
|
|
7700
|
-
"sha256": "
|
|
7705
|
+
"sha256": "bbedfd0018e793c222c09df377443bf8ddf54dcb3a06e7e19be961ea5ecf86d0"
|
|
7701
7706
|
},
|
|
7702
7707
|
{
|
|
7703
7708
|
"path": "modules/lifecycle/routes/version-route-authz.js",
|
|
@@ -10532,12 +10537,12 @@
|
|
|
10532
10537
|
{
|
|
10533
10538
|
"path": "package-lock.json",
|
|
10534
10539
|
"mode": "0000644",
|
|
10535
|
-
"sha256": "
|
|
10540
|
+
"sha256": "a17c1cac4c54a3cfe28d40724b89152ae163a6b594c259df09be108a1c731aae"
|
|
10536
10541
|
},
|
|
10537
10542
|
{
|
|
10538
10543
|
"path": "package.json",
|
|
10539
10544
|
"mode": "0000644",
|
|
10540
|
-
"sha256": "
|
|
10545
|
+
"sha256": "89edc8b83b355ce6f57b15754238190e50f97088c4c111d085c1482995fa8946"
|
|
10541
10546
|
},
|
|
10542
10547
|
{
|
|
10543
10548
|
"path": "public-docs/index.html",
|
|
@@ -10557,7 +10562,7 @@
|
|
|
10557
10562
|
{
|
|
10558
10563
|
"path": "release-notes.json",
|
|
10559
10564
|
"mode": "0000644",
|
|
10560
|
-
"sha256": "
|
|
10565
|
+
"sha256": "4ee7aaaddb122c4798467734d00de8ef8c22a3c1f00a3af5a907d969ee4b8f79"
|
|
10561
10566
|
},
|
|
10562
10567
|
{
|
|
10563
10568
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -12922,7 +12927,7 @@
|
|
|
12922
12927
|
{
|
|
12923
12928
|
"path": "src/module-api.js",
|
|
12924
12929
|
"mode": "0000644",
|
|
12925
|
-
"sha256": "
|
|
12930
|
+
"sha256": "31a1c9f84de47ff566c8cf8c4c4b281b3fde6fe888e18ba1b9a1fda78673225f"
|
|
12926
12931
|
},
|
|
12927
12932
|
{
|
|
12928
12933
|
"path": "src/module-loader/catalog.js",
|
|
@@ -15439,6 +15444,11 @@
|
|
|
15439
15444
|
"mode": "0000644",
|
|
15440
15445
|
"sha256": "5fafbaf95ca562dbafa5fb370e2ef98da2a258eb8a8139d962a0b19ebf693904"
|
|
15441
15446
|
},
|
|
15447
|
+
{
|
|
15448
|
+
"path": "tests/hall_goals_open_first.mjs",
|
|
15449
|
+
"mode": "0000644",
|
|
15450
|
+
"sha256": "79eb37297318f1dfe4661d4fdee68a80c4705141ee142584609232765aaaf2d7"
|
|
15451
|
+
},
|
|
15442
15452
|
{
|
|
15443
15453
|
"path": "tests/hall_gov_counts.mjs",
|
|
15444
15454
|
"mode": "0000644",
|
|
@@ -187,7 +187,7 @@ export interface GetStoreModulesLatestResponse { latest: unknown }
|
|
|
187
187
|
export interface GetStoreUsageReportingResponse { enabled: unknown }
|
|
188
188
|
export interface GetTaskRecommendationsForMeResponse { recommendations: unknown; count: unknown }
|
|
189
189
|
export interface GetTaskRecommendationsMineResponse { recommendations: unknown; count: unknown }
|
|
190
|
-
export interface GetTasksClaimableResponse {
|
|
190
|
+
export interface GetTasksClaimableResponse { by_goal?: unknown; claimable_total: unknown; claimable?: unknown; active_claims?: unknown }
|
|
191
191
|
export interface GetTasksIdCriteriaResponse { criteria: unknown }
|
|
192
192
|
export interface GetTasksIdDependenciesResponse { dependencies: unknown; dependents: unknown }
|
|
193
193
|
export interface GetTasksIdRecommendationsResponse { recommendations: unknown; count: unknown }
|
package/docs/adr/0328-a-hosted-projects-authority-is-its-membership-row-not-its-owner-column.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ADR 0328 — A hosted project's authority is its membership row, not its owner column
|
|
2
2
|
|
|
3
|
-
- **Status:** accepted
|
|
3
|
+
- **Status:** accepted. **Amended 2026-10-16:** Phase 2 (admitting `member`) is suspended because a `member` row is written at every hub sign-in and is not a grant from the project. The gate stays owner-or-archon. See the *Amendment* at the end ([task 1004754](https://cloudbongos.com/builders#/task/1004754)).
|
|
4
4
|
- **Date:** 2026-09-22
|
|
5
5
|
- **Task:** [task 1003882](https://cloudbongos.com/builders#/task/1003882) (goal 1000094 — BONGOS-V2)
|
|
6
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.
|
|
@@ -46,6 +46,8 @@ No new header, no session binding, no new CLI convention. `/provisioning/instanc
|
|
|
46
46
|
|
|
47
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
48
|
|
|
49
|
+
> **Suspended 2026-10-16.** The premise below is wrong: the hub's sign-in default is itself `member`, so the rows this phase would admit include everyone who has signed in. Do not build this phase from this text. See the *Amendment* at the end.
|
|
50
|
+
|
|
49
51
|
**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
52
|
|
|
51
53
|
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.
|
|
@@ -82,7 +84,7 @@ The task's own point 5 asks for migration order "for the 96 write routes." This
|
|
|
82
84
|
|
|
83
85
|
## Consequences
|
|
84
86
|
|
|
85
|
-
-
|
|
87
|
+
- ~~**A team can share one paid hosted instance for the first time**, once task 1004161 lands.~~ *Withdrawn 2026-10-16:* `member`-kind rows are not a project grant (see the *Amendment*). Sharing an instance waits on an owner-made grant ([task 1004753](https://cloudbongos.com/builders#/task/1004753)).
|
|
86
88
|
- **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
89
|
- **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
90
|
- **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).
|
|
@@ -96,3 +98,28 @@ The task's own point 5 asks for migration order "for the 96 write routes." This
|
|
|
96
98
|
- **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
99
|
- **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
100
|
- **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.
|
|
101
|
+
|
|
102
|
+
## Amendment — 2026-10-16: Phase 2 is suspended, because a `member` row is not a grant
|
|
103
|
+
|
|
104
|
+
[Task 1004754](https://cloudbongos.com/builders#/task/1004754). This is the owner's decision of 2026-10-16. It was made when the session claiming [task 1004161](https://cloudbongos.com/builders#/task/1004161) stopped before writing code.
|
|
105
|
+
|
|
106
|
+
**What Phase 2 got wrong.** Decision 2 says the only write that sets `membership_kind` "to a value other than the sign-in default" is the project's own check-in, so a `member` row reflects what the project reported. That is true, but the sign-in default is itself `member`:
|
|
107
|
+
|
|
108
|
+
- `recordFederatedSignin` (`modules/platform-identity/platform-identity.js`) inserts `membership_kind = 'member'`, with the hub's witness, on every completed hub sign-in.
|
|
109
|
+
- It runs at `POST /sso/token`, which completes before the instance's own admission gate runs (`src/bongos/auth-idp.js` `completeIdpFlow` checks `firstTimeAdmissionBlocked` only after the code exchange). A person the project refuses still gets a witnessed `member` row.
|
|
110
|
+
- It runs at `POST /sso/device/poll`. `POST /sso/device/start` takes only a typed origin and needs no sign-in, so any GitHub user can run the device flow against any federated project and get a witnessed `member` row. The project's server is never asked.
|
|
111
|
+
- Instance side, `membershipKind` (`src/bongos/auth-admission.js`) reports `owner` for the first admin and `member` for every other builder. On an instance with `admission.policy: open`, every GitHub sign-up is a builder.
|
|
112
|
+
|
|
113
|
+
So `member` means "has signed in here". It does not mean "the owner trusts this person with the hosting". Admitting it on the 14 routes would let any GitHub user tear down, re-domain, disconnect, restart or reconfigure any federated hosted project. The witness (`signin_verified_at`) proves that the sign-in happened, not that anyone approved it.
|
|
114
|
+
|
|
115
|
+
**What changes.**
|
|
116
|
+
|
|
117
|
+
1. Phase 2 is suspended. `requireProjectOwner` stays the Phase 1 owner-or-archon gate. Task 1004161 is parked in the backlog and depends on the design task below.
|
|
118
|
+
2. A person other than the owner can reach a project's provisioning routes only through a grant that the project's owner (or an archon) makes on purpose. That grant must be stored apart from the sign-in default, and must have one named writer. [Task 1004753](https://cloudbongos.com/builders#/task/1004753) designs it: who writes it, which routes a grantee may use (reads and writes separately; repo-private and the github-app pick-up stay owner-only because they act with the owner's own GitHub token), whether the hub's sign-in default should stop being `member`, and the audit actor a grantee's write carries (neither `api:self` nor `api:archon:<id>`).
|
|
119
|
+
3. Task 1004162's 403 split assumed this vocabulary, so it is re-checked against the new design before it is built.
|
|
120
|
+
|
|
121
|
+
**What still holds, verified for task 1004161:**
|
|
122
|
+
|
|
123
|
+
- `client_id = slug` is a sound join key. `scripts/gds/provision-net.js` registers each hosted instance's credentialed client with `clientId = inst.slug`. `provisioning_instances.slug` is `UNIQUE` and is never reused. A catalog-only client cannot check in.
|
|
124
|
+
- `(client_id, github_id)` is already indexed by the table's own `UNIQUE (client_id, github_id)` constraint (`platform_identity_003_project_memberships.sql`). The per-request read needs no new index.
|
|
125
|
+
- Decision 2 has two failure shapes, and they still apply to whatever port carries the grant. If the port is unregistered, fall back to the Phase 1 owner-or-archon check. If the port is registered but throws or returns an unknown value, deny.
|
package/docs/adr/README.md
CHANGED
|
@@ -446,7 +446,7 @@ These 20 numbers are each shared by exactly two files. They are **accepted histo
|
|
|
446
446
|
| 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 |
|
|
447
447
|
| 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 |
|
|
448
448
|
| 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 |
|
|
449
|
-
| 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 |
|
|
449
|
+
| 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). **Amended 2026-10-16 ([task 1004754](https://cloudbongos.com/builders#/task/1004754)):** Phase 2 is suspended. The hub writes a `member` row at every sign-in, including the device flow anyone can start, so admitting `member` would hand any GitHub user any hosted project. The gate stays owner-or-archon until an owner-made grant is designed ([task 1004753](https://cloudbongos.com/builders#/task/1004753)). 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 |
|
|
450
450
|
| 0329 | [**The rendered fit check is a kit floor and an advisory ship-check row, not a grader worker** ([task 1004203](https://cloudbongos.com/builders#/task/1004203), goal 1000095 — UI quality). Nothing in the pipeline rendered a page, so titles clipped by character count ran past their cards (task 1003480, task 1004204) and a second `.mSec` child would have landed on the first (task 1003078), each found only in a browser. Two new kit floors, `text-fit` (text measured word by word past its box; SVG text past its card rect) and `sibling-overlap` (in-flow, block-level, text-bearing siblings), run in every `render.js` render; `scripts/gds/render-check.js` renders the pages a branch changed at 1280 + 390 and `/ship-check` shows them as an ADVISORY row that never flips its exit. Not a grader worker: that would put a browser, fixtures and a harness on the grader host for signal the builder already saw; the artist gate (ADR 0241) stays the human look before deploy.](0329-the-rendered-fit-check-is-a-kit-floor-and-an-advisory-ship-check-row.md) | ui-design / kit + ship-check |
|
|
451
451
|
| 0330 | [**An ask is answered where it was asked** ([task 1004216](https://cloudbongos.com/builders#/task/1004216), goal 1000095 — BONGOS-V2), amending [ADR 0187](0187-collab-four-decisions.md) §4a. `lifecycle_004` let a builder ASK for help and let the **asker** mark their own row answered; the person being **asked** had no write at all, so the answer happened somewhere Bongos cannot see and the row sat open until its author remembered it — six asks addressed to the owner had been open between four and thirteen days on 2026-09-23, every one of them answerable. That was not an oversight: §4a deferred conversation to the Agora and a test named *"there is deliberately NO reply thread"* enforced it. **The deferral lost its addressee** — task 1002946 is ABANDONED and goal 1000064 ARCHIVED (verified against the live DB), so the rule had stopped meaning "wait for the Agora" and started meaning "an ask stays unanswerable forever to keep a promise to something that does not exist". **Decisions:** (1) an ask carries a thread of replies, APPEND-ONLY in the SCHEMA rather than by convention — no `updated_at`, `edited_by`, `deleted_at` or `parent_reply_id`, no reactions, no mentions, and a trigger that refuses UPDATE and DELETE outright, because a thing you can edit, nest, react to and address at people is a chat and that is what §4a declined; any builder may reply, since the board read is already project-wide and the good case is somebody you did not ask knowing the answer; (2) the **ADDRESSEE** may close an ask (`answered`), widening an asker-only UPDATE that was coherent only while nothing could be said on the row — but `withdrawn` does NOT widen, since "I no longer need this" is the asker's sentence alone and widening it would be a way to make an ask vanish unanswered; `settled_by` records which of the two closed it, and ADR 0187 §3's consent posture is untouched — closing ends a question, it does not accept work; (3) `/collab-review` posts the reply and **leaves the ask open** (owner decision), since an answer the reader has not read is exactly what an unattended run must halt on. **Consequences:** a reply cannot be taken back, by design; the FK **RESTRICTS rather than cascades** — the first draft cascaded, which reads right and was wrong, because a cascade is a plain DELETE the trigger refuses, so deleting an answered ask failed outright (caught by applying the migration to a real Postgres, not by reading it); the old test was REPLACED, not deleted, so the record that the restraint was deliberate survives; and replies inherit the pull-not-push posture, so nothing still tells an asker they were answered. Rejected: a single `answer` column (cheapest and unambiguously not a chat, but a follow-up would have nowhere to go except a second ask, splitting one conversation across two rows); waiting for the Agora (abandoned — waiting means forever); letting anyone close any ask (an open question is somebody's, and a stranger ending it should not be one click away).](0330-an-ask-is-answered-where-it-was-asked.md) | collab / lifecycle / schema |
|
|
452
452
|
| 0331 | [**The sky is the ideator's home, and the project sky takes the goal map's page** ([task 1004229](https://cloudbongos.com/builders#/task/1004229), goal 1000095 — BONGOS-V2), amending [ADR 0227](0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md) and decision 11 of `docs/specs/<redacted>.md`. The owner reviewed an interactive prototype of the ideator sky and the project sky over five rounds on 2026-09-24 and adopted it AS IS — copy, layout, colours, tour text, toggles, interactions — which contradicts two things already written down, so they are rewritten rather than leaving the build graded against a contract nobody means any more. **Decisions:** (1) the sky is the ideator's HOME surface — it replaces *Your thinking*, and the project sky replaces the goal map page — so "never load-bearing" and "arrives last" are DROPPED as a sequencing hedge from before there was a design to look at; the rule that survives at full force is the accessibility half, that the sky is never the ONLY path to anything it shows (the list drawer — the room — is one key away, and every drawn thing is a row in it); (2) ADR 0227 is AMENDED, not reversed — its measured refusal of a whole-corpus task map stands, and the project sky is §1's own prescription rather than its refusal: the goal graph is the entry point, tasks are drawn only INSIDE one goal past a zoom threshold (the per-goal drill-down §10.5 measured readable at 85.2px pitch worst case), below that they are dust density on the rings with NO identities, and the goal map's table survives as the sky's ledger drawer (§6's table equivalent kept); the nav slot the goal map was deliberately denied is GRANTED to the project sky, under Goals (owner, 2026-09-24), so part 4 changes the page and the test asserting no slot together; (3) the five states and the two channels are UNTOUCHED — criterion `ideator-sky-tells-the-truth` names a model, not a picture, and this amendment changes the picture's standing only; (4) the two HEURISTICS in the prototype — an idea's origin guessed from its text, and "born of a thought" found by scanning text for idea ids — must not ship as ledger facts: parts 3 and 4 hide both layers, part 5 replaces them with a `filed_via` column and a real born-of read. **Consequences:** `docs/design/mocks/sky/sky.template.html` is the PIXEL CONTRACT for parts 2–5, and every deviation a gate or the file ratchet forces is listed in that part's ship notes; no snapshot JSON or built page is ever committed (it carries real task and idea titles and this repo is public); ADR 0264's deleted goal "Living project sky" is superseded by this work under Working area 3. Rejected: shipping the sky as an extra and keeping "never load-bearing" (makes the adopted home surface unroutable); reversing ADR 0227 (its refusal is measured and still correct — the sky needs its drill-down shape, not its repeal); shipping the heuristics behind a disclaimer (a labelled guess is still a guess in a picture whose promise is that it cannot be faked).](0331-the-sky-is-the-ideators-home.md) | design / ideator / sky |
|
package/docs/api/openapi.json
CHANGED
|
@@ -28773,14 +28773,13 @@
|
|
|
28773
28773
|
"GetTasksClaimableResponse": {
|
|
28774
28774
|
"type": "object",
|
|
28775
28775
|
"properties": {
|
|
28776
|
-
"
|
|
28776
|
+
"by_goal": {},
|
|
28777
28777
|
"claimable_total": {},
|
|
28778
|
+
"claimable": {},
|
|
28778
28779
|
"active_claims": {}
|
|
28779
28780
|
},
|
|
28780
28781
|
"required": [
|
|
28781
|
-
"
|
|
28782
|
-
"claimable_total",
|
|
28783
|
-
"active_claims"
|
|
28782
|
+
"claimable_total"
|
|
28784
28783
|
]
|
|
28785
28784
|
},
|
|
28786
28785
|
"GetTasksIdCriteriaResponse": {
|
package/docs/copy-inventory.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
> Deterministic on a given tree — regenerate and commit rather than hand-editing.
|
|
5
5
|
> Machine-readable twin: [`copy-registry.json`](copy-registry.json).
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**2760 strings** across 3 surfaces · 2338 distinct pieces of copy · 2760 certain, 0 heuristic · 242 shown in more than one place.
|
|
8
8
|
|
|
9
9
|
## Surfaces
|
|
10
10
|
|
|
11
11
|
| surface | lives at | files | strings |
|
|
12
12
|
|---|---|---|---|
|
|
13
|
-
| `builders-hall` | `modules/hall-ui/public` | 146 |
|
|
13
|
+
| `builders-hall` | `modules/hall-ui/public` | 146 | 1952 |
|
|
14
14
|
| `status-dashboard` | `modules/status-ui/public` | 4 | 100 |
|
|
15
15
|
| `landing` | `modules/public-landing/public` | 9 | 708 |
|
|
16
16
|
|
|
@@ -78,7 +78,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
78
78
|
| `8226c979db4d` | 3x | Not now | modules/hall-ui/public/collab.js:668, modules/hall-ui/public/paintbrush.js:119, modules/hall-ui/public/paintbrush.js:183 |
|
|
79
79
|
| … | | _202 more in the JSON registry_ | |
|
|
80
80
|
|
|
81
|
-
## `builders-hall` —
|
|
81
|
+
## `builders-hall` — 1952 strings
|
|
82
82
|
|
|
83
83
|
| id | conf | text | where |
|
|
84
84
|
|---|---|---|---|
|
|
@@ -238,7 +238,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
238
238
|
| `118026595c16` | certain | Board Room | `modules/hall-ui/public/board-room.html:31` |
|
|
239
239
|
| `2b8ad43383de` | certain | Board Room — {{worldName}} | `modules/hall-ui/public/board-room.html:6` |
|
|
240
240
|
| `2b8ad43383de` | certain | Board Room — {{worldName}} | `modules/hall-ui/public/board-room.html:6` |
|
|
241
|
-
| `da910fee01d7` | certain | Browse goals → | `modules/hall-ui/public/goals.js:
|
|
241
|
+
| `da910fee01d7` | certain | Browse goals → | `modules/hall-ui/public/goals.js:100` |
|
|
242
242
|
| `25b4ab94728a` | certain | Build from your own machine — get a copy of the project, sign in once, then work in Claude. | `modules/hall-ui/public/hall-render.js:813` |
|
|
243
243
|
| `08bfcc9198ba` | certain | Builder id | `modules/hall-ui/public/government.html:109` |
|
|
244
244
|
| `fd1903586e8e` | certain | Builders hall · this session | `modules/hall-ui/public/craft-mode.js:810` |
|
|
@@ -579,10 +579,10 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
579
579
|
| `6f0deb0b667d` | certain | Goal {…} added. | `modules/hall-ui/public/gate.js:480` |
|
|
580
580
|
| `8a421ae268e6` | certain | Goal {…} removed. | `modules/hall-ui/public/gate.js:504` |
|
|
581
581
|
| `e0bc28d10785` | certain | Goals could not be loaded. Refresh to try again. | `modules/hall-ui/public/goals-page.js:895` |
|
|
582
|
-
| `4517f2253d0f` | certain | Goals couldn’t be loaded just now. Refresh to try again. | `modules/hall-ui/public/goals.js:
|
|
582
|
+
| `4517f2253d0f` | certain | Goals couldn’t be loaded just now. Refresh to try again. | `modules/hall-ui/public/goals.js:144` |
|
|
583
583
|
| `6a85997bda21` | certain | Goals created | `modules/hall-ui/public/profile.html:154` |
|
|
584
584
|
| `6bbd703b5aad` | certain | Goals it can work on | `modules/hall-ui/public/gate.js:376` |
|
|
585
|
-
| `2ed2cb8fbffd` | certain | Goals page | `modules/hall-ui/public/goals.js:
|
|
585
|
+
| `2ed2cb8fbffd` | certain | Goals page | `modules/hall-ui/public/goals.js:111` |
|
|
586
586
|
| `320a0516a33e` | certain | Goals seeded — awaiting kickoff | `modules/hall-ui/public/roadmap.js:271` |
|
|
587
587
|
| `bba4caf837e9` | certain | Goals — {{worldName}} | `modules/hall-ui/public/goals.html:6` |
|
|
588
588
|
| `bba4caf837e9` | certain | Goals — {{worldName}} | `modules/hall-ui/public/goals.html:6` |
|
|
@@ -737,7 +737,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
737
737
|
| `af8ea41cdf53` | certain | Loading the terminal commands… | `modules/hall-ui/public/commands.html:46` |
|
|
738
738
|
| `1dffb93947f2` | certain | Loading this project’s skills… | `modules/hall-ui/public/settings.html:308` |
|
|
739
739
|
| `d28d321e8a1e` | certain | Loading versions… | `modules/hall-ui/public/task-detail.js:418` |
|
|
740
|
-
| `3c43f157035e` | certain | Loading your goals… | `modules/hall-ui/public/goals.js:
|
|
740
|
+
| `3c43f157035e` | certain | Loading your goals… | `modules/hall-ui/public/goals.js:112` |
|
|
741
741
|
| `7a239a17afc8` | certain | Loading your preferences… | `modules/hall-ui/public/settings.html:35` |
|
|
742
742
|
| `04df1bd30129` | certain | Loading your rank… | `modules/hall-ui/public/ranks.html:30` |
|
|
743
743
|
| `c27711f9eb4d` | certain | Locked until you reach the end of the article above. | `modules/hall-ui/public/primer.html:103` |
|
|
@@ -888,6 +888,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
888
888
|
| `c404f22d7892` | certain | Noted on the review — it stays open. | `modules/hall-ui/public/studio.js:423` |
|
|
889
889
|
| `1376762e97db` | certain | Nothing anyone asked of you has been settled yet. | `modules/hall-ui/public/collab.html:125` |
|
|
890
890
|
| `cd652d04d616` | certain | Nothing changes yet. This files a task carrying your wording; whoever claims it lands the diff, and it ships g | `modules/hall-ui/public/copy-desk.html:168` |
|
|
891
|
+
| `65434cfeb080` | certain | Nothing claimable right now | `modules/hall-ui/public/goals.js:71` |
|
|
891
892
|
| `b4b8ed14e704` | certain | Nothing explored yet. Pick a starting point in the sky. | `modules/hall-ui/public/sky-panel.js:134` |
|
|
892
893
|
| `fec5d78497e8` | certain | Nothing in the backlog right now. | `modules/hall-ui/public/work.js:817` |
|
|
893
894
|
| `905e99f158c2` | certain | Nothing is discarded by omission: every task needs a decision before this can be submitted. Abandoning stamps | `modules/hall-ui/public/goals-render.js:1032` |
|
|
@@ -944,7 +945,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
944
945
|
| `ec2a3a90b788` | certain | Open your account page | `modules/hall-ui/public/settings.html:110` |
|
|
945
946
|
| `f6f68ba322be` | certain | Open {…} | `modules/hall-ui/public/goal-inbox.js:53` |
|
|
946
947
|
| `f6f68ba322be` | certain | Open {…} | `modules/hall-ui/public/goal-inbox.js:69` |
|
|
947
|
-
| `f6f68ba322be` | certain | Open {…} | `modules/hall-ui/public/goals.js:
|
|
948
|
+
| `f6f68ba322be` | certain | Open {…} | `modules/hall-ui/public/goals.js:84` |
|
|
948
949
|
| `35d46f963cd1` | certain | Opening the doc… | `modules/hall-ui/public/page-notes.js:279` |
|
|
949
950
|
| `7a8dfadb7468` | certain | Opening the rendered page… | `modules/hall-ui/public/approval-queue.js:129` |
|
|
950
951
|
| `62e75a9d0af4` | certain | Opening the reviews… | `modules/hall-ui/public/studio.html:171` |
|
|
@@ -1326,6 +1327,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1326
1327
|
| `82479c539fca` | certain | The name shown beside your work — in the roster, the leaderboard, and the work feed. Your sign-in and the cred | `modules/hall-ui/public/settings.html:78` |
|
|
1327
1328
|
| `4b20ecf5528b` | certain | The next page could not be read. | `modules/hall-ui/public/tweak-editor.js:575` |
|
|
1328
1329
|
| `b88dd8b96b72` | certain | The next person to notice this sentence reads your note instead of re-litigating it. | `modules/hall-ui/public/copy-desk.html:135` |
|
|
1330
|
+
| `0a98e249b324` | certain | The open goals you’ve joined, and the work you can claim in each. Browse, join, and leave goals — and read the | `modules/hall-ui/public/goals.js:111` |
|
|
1329
1331
|
| `329a1d350f07` | certain | The page changed after your draft was saved, so the draft is not shown over its new words. Rewrite the lines a | `modules/hall-ui/public/tweak-editor.js:310` |
|
|
1330
1332
|
| `aa716f3dd551` | certain | The page could not be loaded. Try again. | `modules/hall-ui/public/builders.js:1467` |
|
|
1331
1333
|
| `28e0c1457b08` | certain | The page is in. | `modules/hall-ui/public/tweak-editor.html:96` |
|
|
@@ -1354,7 +1356,6 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1354
1356
|
| `e48a9b77d91b` | certain | The session log opens from a higher rank. | `modules/hall-ui/public/sessions.html:41` |
|
|
1355
1357
|
| `af664a644b13` | certain | The seven government structures, for getting a team up to speed before deciding when to flip a switch. | `modules/hall-ui/public/government.js:847` |
|
|
1356
1358
|
| `6360a5988f26` | certain | The shape and health of the codebase. | `modules/hall-ui/public/watch.html:108` |
|
|
1357
|
-
| `fc422cced9e3` | certain | The shared goals you’ve joined. Browse, join, and leave goals — and read their criteria — on the | `modules/hall-ui/public/goals.js:94` |
|
|
1358
1359
|
| `ac46dc53d95f` | certain | The shared repository the core ships and the modules this project built for itself, each credited to its autho | `modules/hall-ui/public/modules.html:50` |
|
|
1359
1360
|
| `282638f26534` | certain | The six readings | `modules/hall-ui/public/street.html:204` |
|
|
1360
1361
|
| `5f631f51f29e` | certain | The skill list could not be loaded. Reload the page to try again. | `modules/hall-ui/public/settings.js:127` |
|
|
@@ -1573,7 +1574,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1573
1574
|
| `b91183c02c63` | certain | You created this task, but someone is holding a claim on it. Once that is released you can retire it yourself. | `modules/hall-ui/public/task.js:783` |
|
|
1574
1575
|
| `da274462bb30` | certain | You have no open asks. | `modules/hall-ui/public/collab.html:72` |
|
|
1575
1576
|
| `6b759151f722` | certain | You have reached the end. The button below is now active. | `modules/hall-ui/public/primer.js:309` |
|
|
1576
|
-
| `
|
|
1577
|
+
| `044387a6f90c` | certain | You haven’t joined any open goals. | `modules/hall-ui/public/goals.js:100` |
|
|
1577
1578
|
| `8e527ab12334` | certain | You {…}. One vote per member. | `modules/hall-ui/public/board-room.js:451` |
|
|
1578
1579
|
| `8e1915b19274` | certain | You're in the {…} server with your rank role. | `modules/hall-ui/public/settings.js:716` |
|
|
1579
1580
|
| `56cfdcc60079` | certain | You're not signed in yet. Sign in to manage your connections, your keys and the way the hall behaves. | `modules/hall-ui/public/settings.html:44` |
|
|
@@ -1938,7 +1939,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1938
1939
|
| `73c1372b43d9` | certain | {…} of {…} | `modules/hall-ui/public/sky-panel.js:120` |
|
|
1939
1940
|
| `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals-lib.js:32` |
|
|
1940
1941
|
| `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals-render.js:701` |
|
|
1941
|
-
| `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals.js:
|
|
1942
|
+
| `cc5106af4b35` | certain | {…} of {…} criteria satisfied | `modules/hall-ui/public/goals.js:55` |
|
|
1942
1943
|
| `c3d9687e2375` | certain | {…} of {…} shipped | `modules/hall-ui/public/profile.js:671` |
|
|
1943
1944
|
| `f32710d0ef37` | certain | {…} of {…} steps cleared. | `modules/hall-ui/public/hall-render.js:985` |
|
|
1944
1945
|
| `f5e456c3aaf6` | certain | {…} of {…} turned on. Each one, in a line: | `modules/hall-ui/public/settings-specialities.js:152` |
|
|
@@ -1952,6 +1953,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1952
1953
|
| `e08fa22622f0` | certain | {…} shipped, {…} confirmed, {…} completed, {…} remaining of {…} | `modules/hall-ui/public/roadmap.js:351` |
|
|
1953
1954
|
| `5d79bba0d667` | certain | {…} task {…} on live work — {…}. Sequence them before two builders collide. | `modules/hall-ui/public/goal-inbox.js:112` |
|
|
1954
1955
|
| `ad27ae9f5433` | certain | {…} tasks · {…} | `modules/hall-ui/public/roadmap.js:477` |
|
|
1956
|
+
| `db8f35bfcb4f` | certain | {…} task{…} you can claim | `modules/hall-ui/public/goals.js:72` |
|
|
1955
1957
|
| `955499559f0a` | certain | {…} tokens | `modules/hall-ui/public/sessions.js:173` |
|
|
1956
1958
|
| `8e28c14b106d` | certain | {…} total | `modules/hall-ui/public/roadmap.js:120` |
|
|
1957
1959
|
| `1a8a6267e490` | certain | {…} unfinished task{…} — say what happens to each | `modules/hall-ui/public/goals-render.js:1032` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 2760,
|
|
7
|
+
"unique_ids": 2338,
|
|
8
|
+
"certain": 2760,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 242
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 146,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1952
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -613,7 +613,7 @@
|
|
|
613
613
|
"at": [
|
|
614
614
|
"modules/hall-ui/public/goal-inbox.js:53",
|
|
615
615
|
"modules/hall-ui/public/goal-inbox.js:69",
|
|
616
|
-
"modules/hall-ui/public/goals.js:
|
|
616
|
+
"modules/hall-ui/public/goals.js:84"
|
|
617
617
|
]
|
|
618
618
|
},
|
|
619
619
|
{
|
|
@@ -734,7 +734,7 @@
|
|
|
734
734
|
"at": [
|
|
735
735
|
"modules/hall-ui/public/goals-lib.js:32",
|
|
736
736
|
"modules/hall-ui/public/goals-render.js:701",
|
|
737
|
-
"modules/hall-ui/public/goals.js:
|
|
737
|
+
"modules/hall-ui/public/goals.js:55"
|
|
738
738
|
]
|
|
739
739
|
},
|
|
740
740
|
{
|
|
@@ -4049,7 +4049,7 @@
|
|
|
4049
4049
|
"surface": "builders-hall",
|
|
4050
4050
|
"text": "Browse goals →",
|
|
4051
4051
|
"file": "modules/hall-ui/public/goals.js",
|
|
4052
|
-
"line":
|
|
4052
|
+
"line": 100,
|
|
4053
4053
|
"origin": "js-markup",
|
|
4054
4054
|
"confidence": "certain"
|
|
4055
4055
|
},
|
|
@@ -7118,7 +7118,7 @@
|
|
|
7118
7118
|
"surface": "builders-hall",
|
|
7119
7119
|
"text": "Goals couldn’t be loaded just now. Refresh to try again.",
|
|
7120
7120
|
"file": "modules/hall-ui/public/goals.js",
|
|
7121
|
-
"line":
|
|
7121
|
+
"line": 144,
|
|
7122
7122
|
"origin": "js-markup",
|
|
7123
7123
|
"confidence": "certain"
|
|
7124
7124
|
},
|
|
@@ -7145,7 +7145,7 @@
|
|
|
7145
7145
|
"surface": "builders-hall",
|
|
7146
7146
|
"text": "Goals page",
|
|
7147
7147
|
"file": "modules/hall-ui/public/goals.js",
|
|
7148
|
-
"line":
|
|
7148
|
+
"line": 111,
|
|
7149
7149
|
"origin": "js-markup",
|
|
7150
7150
|
"confidence": "certain"
|
|
7151
7151
|
},
|
|
@@ -8540,7 +8540,7 @@
|
|
|
8540
8540
|
"surface": "builders-hall",
|
|
8541
8541
|
"text": "Loading your goals…",
|
|
8542
8542
|
"file": "modules/hall-ui/public/goals.js",
|
|
8543
|
-
"line":
|
|
8543
|
+
"line": 112,
|
|
8544
8544
|
"origin": "js-markup",
|
|
8545
8545
|
"confidence": "certain"
|
|
8546
8546
|
},
|
|
@@ -9894,6 +9894,15 @@
|
|
|
9894
9894
|
"origin": "html-text",
|
|
9895
9895
|
"confidence": "certain"
|
|
9896
9896
|
},
|
|
9897
|
+
{
|
|
9898
|
+
"id": "65434cfeb080",
|
|
9899
|
+
"surface": "builders-hall",
|
|
9900
|
+
"text": "Nothing claimable right now",
|
|
9901
|
+
"file": "modules/hall-ui/public/goals.js",
|
|
9902
|
+
"line": 71,
|
|
9903
|
+
"origin": "js-markup",
|
|
9904
|
+
"confidence": "certain"
|
|
9905
|
+
},
|
|
9897
9906
|
{
|
|
9898
9907
|
"id": "b4b8ed14e704",
|
|
9899
9908
|
"surface": "builders-hall",
|
|
@@ -10403,7 +10412,7 @@
|
|
|
10403
10412
|
"surface": "builders-hall",
|
|
10404
10413
|
"text": "Open {…}",
|
|
10405
10414
|
"file": "modules/hall-ui/public/goals.js",
|
|
10406
|
-
"line":
|
|
10415
|
+
"line": 84,
|
|
10407
10416
|
"origin": "js-markup",
|
|
10408
10417
|
"confidence": "certain"
|
|
10409
10418
|
},
|
|
@@ -13836,6 +13845,15 @@
|
|
|
13836
13845
|
"origin": "html-text",
|
|
13837
13846
|
"confidence": "certain"
|
|
13838
13847
|
},
|
|
13848
|
+
{
|
|
13849
|
+
"id": "0a98e249b324",
|
|
13850
|
+
"surface": "builders-hall",
|
|
13851
|
+
"text": "The open goals you’ve joined, and the work you can claim in each. Browse, join, and leave goals — and read their criteria — on the",
|
|
13852
|
+
"file": "modules/hall-ui/public/goals.js",
|
|
13853
|
+
"line": 111,
|
|
13854
|
+
"origin": "js-markup",
|
|
13855
|
+
"confidence": "certain"
|
|
13856
|
+
},
|
|
13839
13857
|
{
|
|
13840
13858
|
"id": "329a1d350f07",
|
|
13841
13859
|
"surface": "builders-hall",
|
|
@@ -14088,15 +14106,6 @@
|
|
|
14088
14106
|
"origin": "html-text",
|
|
14089
14107
|
"confidence": "certain"
|
|
14090
14108
|
},
|
|
14091
|
-
{
|
|
14092
|
-
"id": "fc422cced9e3",
|
|
14093
|
-
"surface": "builders-hall",
|
|
14094
|
-
"text": "The shared goals you’ve joined. Browse, join, and leave goals — and read their criteria — on the",
|
|
14095
|
-
"file": "modules/hall-ui/public/goals.js",
|
|
14096
|
-
"line": 94,
|
|
14097
|
-
"origin": "js-markup",
|
|
14098
|
-
"confidence": "certain"
|
|
14099
|
-
},
|
|
14100
14109
|
{
|
|
14101
14110
|
"id": "ac46dc53d95f",
|
|
14102
14111
|
"surface": "builders-hall",
|
|
@@ -16060,11 +16069,11 @@
|
|
|
16060
16069
|
"confidence": "certain"
|
|
16061
16070
|
},
|
|
16062
16071
|
{
|
|
16063
|
-
"id": "
|
|
16072
|
+
"id": "044387a6f90c",
|
|
16064
16073
|
"surface": "builders-hall",
|
|
16065
|
-
"text": "You haven’t joined any goals
|
|
16074
|
+
"text": "You haven’t joined any open goals.",
|
|
16066
16075
|
"file": "modules/hall-ui/public/goals.js",
|
|
16067
|
-
"line":
|
|
16076
|
+
"line": 100,
|
|
16068
16077
|
"origin": "js-markup",
|
|
16069
16078
|
"confidence": "certain"
|
|
16070
16079
|
},
|
|
@@ -19349,7 +19358,7 @@
|
|
|
19349
19358
|
"surface": "builders-hall",
|
|
19350
19359
|
"text": "{…} of {…} criteria satisfied",
|
|
19351
19360
|
"file": "modules/hall-ui/public/goals.js",
|
|
19352
|
-
"line":
|
|
19361
|
+
"line": 55,
|
|
19353
19362
|
"origin": "js-markup",
|
|
19354
19363
|
"confidence": "certain"
|
|
19355
19364
|
},
|
|
@@ -19470,6 +19479,15 @@
|
|
|
19470
19479
|
"origin": "js-markup",
|
|
19471
19480
|
"confidence": "certain"
|
|
19472
19481
|
},
|
|
19482
|
+
{
|
|
19483
|
+
"id": "db8f35bfcb4f",
|
|
19484
|
+
"surface": "builders-hall",
|
|
19485
|
+
"text": "{…} task{…} you can claim",
|
|
19486
|
+
"file": "modules/hall-ui/public/goals.js",
|
|
19487
|
+
"line": 72,
|
|
19488
|
+
"origin": "js-markup",
|
|
19489
|
+
"confidence": "certain"
|
|
19490
|
+
},
|
|
19473
19491
|
{
|
|
19474
19492
|
"id": "955499559f0a",
|
|
19475
19493
|
"surface": "builders-hall",
|
|
@@ -3115,5 +3115,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3115
3115
|
landed since 1.21.137 with no explicit bump. run 38920999785. (task 1002620)
|
|
3116
3116
|
1.21.139 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3117
3117
|
landed since 1.21.138 with no explicit bump. run 38927010049. (task 1002620)
|
|
3118
|
+
1.21.140 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3119
|
+
landed since 1.21.139 with no explicit bump. run 38941756016. (task 1002620)
|
|
3120
|
+
1.21.141 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3121
|
+
landed since 1.21.140 with no explicit bump. run 39003470982. (task 1002620)
|
|
3118
3122
|
---------------------------------------------------------------------------
|
|
3119
3123
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"adr": "0341 D3",
|
|
6
6
|
"reread": "node scripts/gds/page-reader.js --stale",
|
|
7
7
|
"how": ["Generated by node scripts/gds/page-reader.js (BV2.TW04, task 1004315; ADR 0341 D3). Do not hand-edit.","One entry per page of docs/page-inventory.json. Lines are in page order: the default state first, and a line only another state shows sits where that state shows it, with its state named.","placement: placed = a docs/copy-registry.json row renders it (file, line, string_id); shared = shell text every page of the surface carries (top bar, sidebar, footer), with its row when the registry has one; unplaced = the registry cannot place it (a rewrite becomes an engineer task, ADR 0341 D4).","text keeps {…} where the registry row has a hole, or where a value the page read from its API was filled in. A string that is only such data is counted under omitted.data, not listed.","reading_hash is over the non-shared lines (key, section, text); files_hash is over the page's inventory files. --check compares files_hash only and never opens a browser.","Rendered through the hall-preview harness (hall) and the ui-design kit stub (landing, status), at 1280x800 with the clock pinned, so a reading is the fixture world's page."],
|
|
8
|
-
"counts": {"pages":46,"lines":5551,"placed":1701,"shared":398,"unplaced":3452,"omitted_data":
|
|
8
|
+
"counts": {"pages":46,"lines":5551,"placed":1701,"shared":398,"unplaced":3452,"omitted_data":3575},
|
|
9
9
|
"pages": [
|
|
10
10
|
{
|
|
11
11
|
"id": "builders:atlas",
|
|
@@ -1439,10 +1439,10 @@
|
|
|
1439
1439
|
"surface": "builders",
|
|
1440
1440
|
"title": "Builders' Hall",
|
|
1441
1441
|
"states_read": ["home","standing","standing-karma","out","founding","genesis","decide","demo"],
|
|
1442
|
-
"files_hash": "
|
|
1443
|
-
"reading_hash": "
|
|
1442
|
+
"files_hash": "96338f028ee183da",
|
|
1443
|
+
"reading_hash": "6ba0ac4c30cc2d79",
|
|
1444
1444
|
"counts": {"lines":283,"placed":77,"shared":10,"unplaced":196},
|
|
1445
|
-
"omitted": {"data":
|
|
1445
|
+
"omitted": {"data":216},
|
|
1446
1446
|
"notes": ["founding: an action failed (page.focus: Timeout 6000ms exceeded.); read as it stood"],
|
|
1447
1447
|
"lines": [
|
|
1448
1448
|
{"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":860,"string_id":"87cee78ca134"},
|
|
@@ -1454,23 +1454,23 @@
|
|
|
1454
1454
|
{"key":"L0007","section":"top bar","text":"Building","placement":"shared"},
|
|
1455
1455
|
{"key":"L0008","section":"top bar","text":"Artistry","placement":"shared"},
|
|
1456
1456
|
{"key":"L0009","section":"top bar","text":"Ideating","placement":"shared"},
|
|
1457
|
-
{"key":"L0010","section":"Good
|
|
1458
|
-
{"key":"L0011","section":"Good
|
|
1459
|
-
{"key":"L0012","section":"Good
|
|
1460
|
-
{"key":"L0013","section":"Good
|
|
1461
|
-
{"key":"L0014","section":"Good
|
|
1462
|
-
{"key":"L0015","section":"Good
|
|
1463
|
-
{"key":"L0016","section":"Good
|
|
1464
|
-
{"key":"L0017","section":"Good
|
|
1465
|
-
{"key":"L0018","section":"Good
|
|
1466
|
-
{"key":"L0019","section":"Good
|
|
1467
|
-
{"key":"L0020","section":"Good
|
|
1468
|
-
{"key":"L0021","section":"Good
|
|
1469
|
-
{"key":"L0022","section":"Good
|
|
1470
|
-
{"key":"L0023","section":"Good
|
|
1471
|
-
{"key":"L0024","section":"Good
|
|
1472
|
-
{"key":"L0025","section":"Good
|
|
1473
|
-
{"key":"L0026","section":"Good
|
|
1457
|
+
{"key":"L0010","section":"Good morning, {…}","text":"Good morning, {…}","placement":"unplaced"},
|
|
1458
|
+
{"key":"L0011","section":"Good morning, {…}","text":"{…} is 83 percent {…}. 150 tasks are claimable right now.","placement":"unplaced"},
|
|
1459
|
+
{"key":"L0012","section":"Good morning, {…}","text":"Work","placement":"unplaced","state":"standing"},
|
|
1460
|
+
{"key":"L0013","section":"Good morning, {…}","text":"Credits","placement":"unplaced"},
|
|
1461
|
+
{"key":"L0014","section":"Good morning, {…}","text":"Shipped","placement":"placed","file":"modules/hall-ui/public/task.js","line":677,"string_id":"0c0a50ba1666"},
|
|
1462
|
+
{"key":"L0015","section":"Good morning, {…}","text":"Karma","placement":"unplaced"},
|
|
1463
|
+
{"key":"L0016","section":"Good morning, {…}","text":"Achievements","placement":"unplaced","state":"standing"},
|
|
1464
|
+
{"key":"L0017","section":"Good morning, {…}","text":"Rebase owed — new claims paused","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":564,"string_id":"06cbba1440b1","state":"standing"},
|
|
1465
|
+
{"key":"L0018","section":"Good morning, {…}","text":"— {…} confirmed {…} can't land yet, so {…} reward is pending (it lands on ship). Rebase + re-ship or release {…}: {…}.","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":564,"string_id":"68b547166d48","state":"standing"},
|
|
1466
|
+
{"key":"L0019","section":"Good morning, {…}","text":"+11 in 30d","placement":"unplaced"},
|
|
1467
|
+
{"key":"L0020","section":"Good morning, {…}","text":"Rank","placement":"unplaced"},
|
|
1468
|
+
{"key":"L0021","section":"Good morning, {…}","text":"Member since {…}","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":569,"string_id":"df335a1224ae","state":"standing"},
|
|
1469
|
+
{"key":"L0022","section":"Good morning, {…}","text":"+11 karma in 30d","placement":"unplaced","state":"standing"},
|
|
1470
|
+
{"key":"L0023","section":"Good morning, {…}","text":"Preferred work:","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":584,"string_id":"ed8edd684547","state":"standing"},
|
|
1471
|
+
{"key":"L0024","section":"Good morning, {…}","text":"{…}, {…}, {…}, ui","placement":"unplaced","state":"standing"},
|
|
1472
|
+
{"key":"L0025","section":"Good morning, {…}","text":"Change in Settings →","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":584,"string_id":"0279db21e29a","state":"standing"},
|
|
1473
|
+
{"key":"L0026","section":"Good morning, {…}","text":"karma","placement":"unplaced","state":"standing"},
|
|
1474
1474
|
{"key":"L0027","section":"Standing","text":"Standing","placement":"unplaced"},
|
|
1475
1475
|
{"key":"L0028","section":"Standing","text":"Credits","placement":"unplaced"},
|
|
1476
1476
|
{"key":"L0029","section":"Standing","text":"On the board","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":633,"string_id":"11a1905c76b2"},
|
|
@@ -1478,10 +1478,10 @@
|
|
|
1478
1478
|
{"key":"L0031","section":"Standing","text":"{…}, {…}, {…}, ui","placement":"unplaced"},
|
|
1479
1479
|
{"key":"L0032","section":"Standing","text":"View full standing →","placement":"placed","file":"modules/hall-ui/public/hall-widgets.js","line":86,"string_id":"4babdfeaf27f"},
|
|
1480
1480
|
{"key":"L0033","section":"Goals","text":"Goals","placement":"unplaced"},
|
|
1481
|
-
{"key":"L0034","section":"Goals","text":"The
|
|
1482
|
-
{"key":"L0035","section":"Goals","text":"Goals page","placement":"placed","file":"modules/hall-ui/public/goals.js","line":
|
|
1483
|
-
{"key":"L0036","section":"Goals","text":"You haven’t joined any goals
|
|
1484
|
-
{"key":"L0037","section":"Goals","text":"Browse goals →","placement":"placed","file":"modules/hall-ui/public/goals.js","line":
|
|
1481
|
+
{"key":"L0034","section":"Goals","text":"The open goals you’ve joined, and the work you can claim in each. Browse, join, and leave goals — and read their criteria — on the","placement":"placed","file":"modules/hall-ui/public/goals.js","line":111,"string_id":"0a98e249b324"},
|
|
1482
|
+
{"key":"L0035","section":"Goals","text":"Goals page","placement":"placed","file":"modules/hall-ui/public/goals.js","line":111,"string_id":"2ed2cb8fbffd"},
|
|
1483
|
+
{"key":"L0036","section":"Goals","text":"You haven’t joined any open goals.","placement":"placed","file":"modules/hall-ui/public/goals.js","line":100,"string_id":"044387a6f90c"},
|
|
1484
|
+
{"key":"L0037","section":"Goals","text":"Browse goals →","placement":"placed","file":"modules/hall-ui/public/goals.js","line":100,"string_id":"da910fee01d7"},
|
|
1485
1485
|
{"key":"L0038","section":"Needs your attention13","text":"Needs your attention","placement":"placed","file":"modules/hall-ui/public/goal-inbox.js","line":597,"string_id":"7def635421ea"},
|
|
1486
1486
|
{"key":"L0039","section":"From the system","text":"From the system","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":711,"string_id":"d32c5e058d1e"},
|
|
1487
1487
|
{"key":"L0040","section":"From the system","text":"Rebase owed","placement":"placed","file":"modules/hall-ui/public/hall-render.js","line":680,"string_id":"4f619c19a8e7"},
|
|
@@ -30,6 +30,16 @@
|
|
|
30
30
|
return res.data;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const isOpenGoal = (g) => !!g && g.status === 'open';
|
|
34
|
+
|
|
35
|
+
// goal id -> how many claimable tasks it holds. The server counts (?group=goal
|
|
36
|
+
// returns counts and no rows), so this card never downloads the feed itself.
|
|
37
|
+
function claimableByGoal(byGoal) {
|
|
38
|
+
const counts = new Map();
|
|
39
|
+
for (const [k, n] of Object.entries(byGoal || {})) counts.set(String(k), Number(n) || 0);
|
|
40
|
+
return counts;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
let host = null;
|
|
34
44
|
let myId = null;
|
|
35
45
|
const sameId = (a, b) => a != null && b != null && String(a) === String(b);
|
|
@@ -55,7 +65,14 @@
|
|
|
55
65
|
return `<span class="goal-badge ${cls}">${escapeHtml(status)}</span>`;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
// task 1004755: the claimable count rides under each open goal, so the card
|
|
69
|
+
// leads with the builder's open goals and shows the work waiting in each.
|
|
70
|
+
function claimableLine(n) {
|
|
71
|
+
if (!n) return '<span class="goal-mine__work card-foot">Nothing claimable right now</span>';
|
|
72
|
+
return `<span class="goal-mine__work card-foot">${n} task${n === 1 ? '' : 's'} you can claim</span>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function row(detail, counts) {
|
|
59
76
|
const g = detail.goal;
|
|
60
77
|
const crit = Array.isArray(detail.criteria) ? detail.criteria : [];
|
|
61
78
|
const sat = crit.filter((c) => c.satisfied).length;
|
|
@@ -68,7 +85,7 @@
|
|
|
68
85
|
<a class="goal-mine__link" href="${href}" title="Open ${escapeHtml(g.title || 'goal')}"
|
|
69
86
|
style="flex:1;display:flex;align-items:center;gap:12px;min-width:0;color:inherit;text-decoration:none;">
|
|
70
87
|
<span class="goal-mine__ring">${ring(sat, crit.length, achieved)}</span>
|
|
71
|
-
<span class="goal-mine__title">${escapeHtml(g.title || 'Untitled goal')}</span>
|
|
88
|
+
<span class="goal-mine__title" style="display:flex;flex-direction:column;min-width:0;">${escapeHtml(g.title || 'Untitled goal')}${claimableLine((counts && counts.get(String(g.id))) || 0)}</span>
|
|
72
89
|
<span class="goal-mine__meta"><span class="goal-mine__ver">${escapeHtml(g.version_id || '')}</span>${statusBadge(g.status)}</span>
|
|
73
90
|
</a>
|
|
74
91
|
</li>`;
|
|
@@ -77,21 +94,21 @@
|
|
|
77
94
|
// task 2184 (Home rule: a summary card never lists more than 5 rows): cap the
|
|
78
95
|
// joined-goals list; the overflow count rides the "All goals" foot link.
|
|
79
96
|
const MAX_HOME_ROWS = 5;
|
|
80
|
-
function render(joined) {
|
|
97
|
+
function render(joined, counts) {
|
|
81
98
|
const body = host.querySelector('#goals-body');
|
|
82
99
|
if (!joined.length) {
|
|
83
|
-
body.innerHTML = '<p class="goal-empty">You haven’t joined any goals
|
|
100
|
+
body.innerHTML = '<p class="goal-empty">You haven’t joined any open goals. <a href="/goals">Browse goals →</a></p>';
|
|
84
101
|
return;
|
|
85
102
|
}
|
|
86
103
|
const shown = joined.slice(0, MAX_HOME_ROWS);
|
|
87
104
|
const more = joined.length - shown.length;
|
|
88
|
-
body.innerHTML = `<ul class="goal-mine-list">${shown.map(row).join('')}</ul>` +
|
|
105
|
+
body.innerHTML = `<ul class="goal-mine-list">${shown.map((d) => row(d, counts)).join('')}</ul>` +
|
|
89
106
|
`<p class="goal-mine__foot card-foot"><a href="/goals">${more > 0 ? `All ${joined.length} goals →` : 'All goals →'}</a></p>`;
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
const SKELETON =
|
|
93
110
|
'<h2 id="goals-h" class="scroll__h">Goals</h2>' +
|
|
94
|
-
'<p class="scroll__lede">The
|
|
111
|
+
'<p class="scroll__lede">The open goals you’ve joined, and the work you can claim in each. Browse, join, and leave goals — and read their criteria — on the <a href="/goals">Goals page</a>.</p>' +
|
|
95
112
|
'<div id="goals-body"><div class="profile__placeholder">Loading your goals…</div></div>';
|
|
96
113
|
|
|
97
114
|
async function load() {
|
|
@@ -107,12 +124,18 @@
|
|
|
107
124
|
// paint. boot() no longer mounts widgets for a signed-out visitor; this
|
|
108
125
|
// keeps the widget itself harmless if it is ever mounted without a session
|
|
109
126
|
// (an expired cookie mid-visit), where the honest answer is to say nothing.
|
|
127
|
+
// Started now, awaited after the goal reads, so the two run side by side.
|
|
128
|
+
const feedP = getJSON(`${API}/tasks/claimable?group=goal`, { softAuth: true }).catch(() => null);
|
|
110
129
|
const gres = await getJSON(`${API}/goals`, { softAuth: true });
|
|
111
|
-
|
|
130
|
+
// task 1004755: OPEN goals only. An achieved goal has no work left in it, and
|
|
131
|
+
// listing it here (it took slots in the five-row cap) pushed the goals with
|
|
132
|
+
// work waiting off the card.
|
|
133
|
+
const candidates = (gres.goals || []).filter(isOpenGoal);
|
|
112
134
|
const details = (await Promise.all(candidates.map((g) =>
|
|
113
135
|
getJSON(`${API}/goals/${encodeURIComponent(g.id)}`).catch(() => null)))).filter(Boolean);
|
|
114
136
|
const joined = details.filter((d) => (d.members || []).some((m) => sameId(m.builder_id, myId)));
|
|
115
|
-
|
|
137
|
+
const feed = await feedP;
|
|
138
|
+
render(joined, claimableByGoal(feed && feed.by_goal));
|
|
116
139
|
} catch (err) {
|
|
117
140
|
const body = host.querySelector('#goals-body');
|
|
118
141
|
// Signed out is not a failure — it is simply nothing to show. Only a real
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// claimable-counts.js — how much claimable work sits in each goal (task 1004755).
|
|
2
|
+
//
|
|
3
|
+
// Backs `GET /tasks/claimable?group=goal`, which the hall Home goals card reads to say
|
|
4
|
+
// "N tasks you can claim" under each open goal it lists. It counts the SAME filtered
|
|
5
|
+
// feed the route returns rows from, so a count and claimable_total cannot disagree,
|
|
6
|
+
// and it returns no rows, so a Home card never downloads the feed to count it.
|
|
7
|
+
// A row with no goal is not counted.
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
function claimableCountsByGoal(rows) {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const t of Array.isArray(rows) ? rows : []) {
|
|
13
|
+
if (!t || t.goal_id == null) continue;
|
|
14
|
+
out[t.goal_id] = (out[t.goal_id] || 0) + 1;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { claimableCountsByGoal };
|
|
@@ -29,6 +29,7 @@ const api = require('../../../src/module-api');
|
|
|
29
29
|
// depRefusalRow: the ONE shape a dependency refusal carries, shared with the claim
|
|
30
30
|
// path so promote and claim describe a blocked task identically (task 1003553).
|
|
31
31
|
const { depRefusalRow } = require('../dead-deps.js');
|
|
32
|
+
const { claimableCountsByGoal } = require('../claimable-counts');
|
|
32
33
|
// gradeQualityWhere: the ONE definition of a task_grades row that is an actual
|
|
33
34
|
// quality verdict rather than an outage or an errored worker (task 1002652).
|
|
34
35
|
// The acclaim feed reads it so a grader outage never reaches a voter as 0.0.
|
|
@@ -207,16 +208,13 @@ module.exports = function buildTasksRouter() {
|
|
|
207
208
|
if (fit && !VALID_FITS.has(fit)) {
|
|
208
209
|
return res.fail('bad_fit', 400, { valid: Array.from(VALID_FITS) });
|
|
209
210
|
}
|
|
210
|
-
// task 1003278 —
|
|
211
|
-
// feed at the SOURCE instead of downloading it and reducing it in the browser.
|
|
212
|
-
// Same validators as GET /tasks: one vocabulary, two surfaces.
|
|
211
|
+
// task 1003278 — creator + staleness at the SOURCE, with GET /tasks's validators.
|
|
213
212
|
const claimCreatedBy = parseCreatedBy(req.query.created_by);
|
|
214
213
|
if (claimCreatedBy && claimCreatedBy.bad) return res.fail('bad_created_by', { status: 400, message: CREATED_BY_MSG });
|
|
215
214
|
const claimStaleDays = posInt(req.query.stale_days, 'bad_stale_days', 3650);
|
|
216
215
|
if (claimStaleDays && claimStaleDays.bad) return res.fail('bad_stale_days', { status: 400, message: 'stale_days must be a positive integer number of days.' });
|
|
217
|
-
// ?limit=N — opt-in, for a caller that renders N rows (goal 1000072
|
|
218
|
-
//
|
|
219
|
-
// the RETURNED feed, never the query — lifecycle/CLAUDE.md has the why.
|
|
216
|
+
// ?limit=N — opt-in, for a caller that renders N rows (goal 1000072). It slices the
|
|
217
|
+
// RETURNED feed, never the query — lifecycle/CLAUDE.md has the why.
|
|
220
218
|
const claimLimit = posInt(req.query.limit, 'bad_limit', 1000);
|
|
221
219
|
if (claimLimit && claimLimit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer (max 1000).' });
|
|
222
220
|
const [feed, active] = await Promise.all([
|
|
@@ -228,8 +226,11 @@ module.exports = function buildTasksRouter() {
|
|
|
228
226
|
createdBy: claimCreatedBy ?? null,
|
|
229
227
|
staleDays: claimStaleDays ?? null,
|
|
230
228
|
}),
|
|
231
|
-
db.listActiveClaims(),
|
|
229
|
+
req.query.group === 'goal' ? [] : db.listActiveClaims(),
|
|
232
230
|
]);
|
|
231
|
+
if (req.query.group === 'goal') { // counts only, no rows (task 1004755)
|
|
232
|
+
return res.json({ by_goal: claimableCountsByGoal(feed), claimable_total: feed.length });
|
|
233
|
+
}
|
|
233
234
|
const claimable = claimLimit ? feed.slice(0, claimLimit) : feed;
|
|
234
235
|
// V3.R75 (#294): tick the onboarding state machine for first_start. Fire
|
|
235
236
|
// and forget — the response should never wait on or fail because of the
|
|
@@ -237,9 +238,8 @@ module.exports = function buildTasksRouter() {
|
|
|
237
238
|
// graduation broadcast. Through the `onboarding` kernel port (BV1.R81 / ADR
|
|
238
239
|
// 0093 §1); ?. degrades to a no-op when the module is off.
|
|
239
240
|
seams.resolveOptional('onboarding')?.safeMarkStage(pool, req.builder.id, 'first_start', req.builder);
|
|
240
|
-
// `claimable_total`
|
|
241
|
-
//
|
|
242
|
-
// a second count query (that is how a total and its own list drift apart, ADR 0197).
|
|
241
|
+
// `claimable_total` keeps the limit honest: the length of the sliced array, never a
|
|
242
|
+
// second count query (ADR 0197).
|
|
243
243
|
res.json({ claimable, claimable_total: feed.length, active_claims: active });
|
|
244
244
|
});
|
|
245
245
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.141",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.141",
|
|
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.21.
|
|
3
|
+
"version": "1.21.141",
|
|
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/release-notes.json
CHANGED
|
@@ -9836,5 +9836,17 @@
|
|
|
9836
9836
|
"id": "1004752",
|
|
9837
9837
|
"text": "Provisioning's own tests now run in the automated checks, so they can't quietly break again."
|
|
9838
9838
|
}
|
|
9839
|
+
],
|
|
9840
|
+
"1.21.140": [
|
|
9841
|
+
{
|
|
9842
|
+
"id": "1004754",
|
|
9843
|
+
"text": "Stopped a planned change that would have let anyone who signs in to a hosted project manage it. The design record now says sharing a project needs the owner to grant access on purpose; that design is filed as its own task."
|
|
9844
|
+
}
|
|
9845
|
+
],
|
|
9846
|
+
"1.21.141": [
|
|
9847
|
+
{
|
|
9848
|
+
"id": "1004755",
|
|
9849
|
+
"text": "Your home page now lists only the goals you have joined that are still open, and under each one says how many tasks you can pick up. Finished goals no longer crowd them out."
|
|
9850
|
+
}
|
|
9839
9851
|
]
|
|
9840
9852
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.141'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// tests/hall_goals_open_first.mjs — the hall Home goals card leads with the OPEN
|
|
2
|
+
// goals the builder has joined, with the claimable work in each (task 1004755).
|
|
3
|
+
//
|
|
4
|
+
// The owner's acceptance test of criterion wa6-noise-limited found the card
|
|
5
|
+
// listing COMPLETED goals: it kept every non-archived goal, so achieved goals took
|
|
6
|
+
// slots in the five-row cap. This boots the real goals.js in a vm against a fake
|
|
7
|
+
// API and reads what it renders.
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/hall_goals_open_first.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import vm from 'node:vm';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { makeRunner } from './helpers.mjs';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { claimableCountsByGoal } = require('../modules/lifecycle/claimable-counts.js');
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals.js'), 'utf8');
|
|
24
|
+
const { test, summary } = makeRunner();
|
|
25
|
+
|
|
26
|
+
async function renderCard({ goals, members, claimable }) {
|
|
27
|
+
const body = { innerHTML: '' };
|
|
28
|
+
const host = { innerHTML: '', querySelector: () => body };
|
|
29
|
+
let widget = null;
|
|
30
|
+
const routes = (p) => {
|
|
31
|
+
if (p === '/api/bongos/me') return { builder: { id: 7 } };
|
|
32
|
+
if (p === '/api/bongos/goals') return { goals };
|
|
33
|
+
if (p === '/api/bongos/tasks/claimable?group=goal') {
|
|
34
|
+
return { by_goal: claimableCountsByGoal(claimable), claimable_total: claimable.length };
|
|
35
|
+
}
|
|
36
|
+
if (p.startsWith('/api/bongos/tasks/claimable')) throw new Error('the card must not download the claimable feed: ' + p);
|
|
37
|
+
const m = p.match(/^\/api\/bongos\/goals\/(\d+)$/);
|
|
38
|
+
if (m) {
|
|
39
|
+
const g = goals.find((x) => String(x.id) === m[1]);
|
|
40
|
+
return { goal: g, criteria: [], members: members[g.id] || [] };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
const window = {
|
|
45
|
+
OTB: {
|
|
46
|
+
escapeHtml: (s) => String(s),
|
|
47
|
+
contributeHallWidget: (w) => { widget = w; },
|
|
48
|
+
},
|
|
49
|
+
BongosClient: {
|
|
50
|
+
createClient: () => ({
|
|
51
|
+
request: async (_m, p) => { const data = routes(p); return { status: data ? 200 : 404, ok: !!data, data }; },
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
location: { assign() {} },
|
|
55
|
+
};
|
|
56
|
+
vm.runInNewContext(SRC, { window, console, Promise, Map, String, Array, Math, encodeURIComponent });
|
|
57
|
+
widget.mount(host);
|
|
58
|
+
for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r));
|
|
59
|
+
return body.innerHTML;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const me = [{ builder_id: 7 }];
|
|
63
|
+
|
|
64
|
+
await test('an achieved goal the builder joined is NOT listed; the open one is', async () => {
|
|
65
|
+
const html = await renderCard({
|
|
66
|
+
goals: [
|
|
67
|
+
{ id: 1, title: 'Finished goal', status: 'achieved' },
|
|
68
|
+
{ id: 2, title: 'Live goal', status: 'open' },
|
|
69
|
+
],
|
|
70
|
+
members: { 1: me, 2: me },
|
|
71
|
+
claimable: [],
|
|
72
|
+
});
|
|
73
|
+
assert.ok(html.includes('Live goal'), 'the open joined goal is shown');
|
|
74
|
+
assert.ok(!html.includes('Finished goal'), 'the completed goal is left off the card');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await test('achieved goals no longer take the five slots ahead of open ones', async () => {
|
|
78
|
+
const goals = [];
|
|
79
|
+
for (let i = 1; i <= 5; i++) goals.push({ id: i, title: `Done ${i}`, status: 'achieved' });
|
|
80
|
+
goals.push({ id: 9, title: 'Open nine', status: 'open' });
|
|
81
|
+
const members = Object.fromEntries(goals.map((g) => [g.id, me]));
|
|
82
|
+
const html = await renderCard({ goals, members, claimable: [] });
|
|
83
|
+
assert.ok(html.includes('Open nine'));
|
|
84
|
+
assert.ok(!/Done \d/.test(html));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await test('each open goal carries how much claimable work sits in it', async () => {
|
|
88
|
+
const html = await renderCard({
|
|
89
|
+
goals: [{ id: 2, title: 'Live goal', status: 'open' }, { id: 3, title: 'Quiet goal', status: 'open' }],
|
|
90
|
+
members: { 2: me, 3: me },
|
|
91
|
+
claimable: [{ id: 10, goal_id: 2 }, { id: 11, goal_id: 2 }, { id: 12, goal_id: null }],
|
|
92
|
+
});
|
|
93
|
+
assert.match(html, /Live goal[\s\S]*2 tasks you can claim/);
|
|
94
|
+
assert.match(html, /Quiet goal[\s\S]*Nothing claimable right now/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await test('a goal the builder has not joined is not listed', async () => {
|
|
98
|
+
const html = await renderCard({
|
|
99
|
+
goals: [{ id: 4, title: 'Someone else', status: 'open' }],
|
|
100
|
+
members: { 4: [{ builder_id: 99 }] },
|
|
101
|
+
claimable: [],
|
|
102
|
+
});
|
|
103
|
+
assert.ok(!html.includes('Someone else'));
|
|
104
|
+
assert.match(html, /joined any open goals/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await test('claimableCountsByGoal counts per goal and skips goal-less rows', () => {
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
JSON.parse(JSON.stringify(claimableCountsByGoal([{ goal_id: 2 }, { goal_id: '2' }, { goal_id: 5 }, { goal_id: null }, {}, null]))),
|
|
110
|
+
{ 2: 2, 5: 1 });
|
|
111
|
+
assert.deepEqual(claimableCountsByGoal(undefined), {});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await test('the route answers ?group=goal with counts and no rows', () => {
|
|
115
|
+
const route = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
116
|
+
const body = route.slice(route.indexOf("req.query.group === 'goal'"));
|
|
117
|
+
assert.ok(body.slice(0, 300).includes('res.json({ by_goal: claimableCountsByGoal(feed), claimable_total: feed.length })'));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
summary();
|