@bongos/core 1.19.179 → 1.19.181
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 +25 -15
- package/docs/adr/0174-government-terminology-ranks-not-roles.md +57 -0
- package/docs/adr/README.md +1 -0
- package/modules/economy/credits.js +73 -0
- package/modules/economy/reward.js +2 -0
- package/modules/lifecycle/db.js +13 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/idea_task_ship_credit.mjs +117 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.181",
|
|
6
|
+
"core_contract": "1.19.181",
|
|
7
|
+
"source_commit": "3335e6696cc6d8b3c0a4d800860f76287c1fcc03",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-16T21:
|
|
9
|
+
"built_at": "2026-08-16T21:10:51.840Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 308,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1277,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1606,
|
|
20
|
+
"tree_sha256": "411dfd39dd80ffce17841372dfaf756045f842fb17380c1caca33aa10251bc42",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1349,10 +1349,15 @@
|
|
|
1349
1349
|
"mode": "0000644",
|
|
1350
1350
|
"sha256": "1c3c44c292b01e57399631ad604faa0b60ccfd841aa7385f5f4ab2cfdca4157d"
|
|
1351
1351
|
},
|
|
1352
|
+
{
|
|
1353
|
+
"path": "docs/adr/0174-government-terminology-ranks-not-roles.md",
|
|
1354
|
+
"mode": "0000644",
|
|
1355
|
+
"sha256": "c645ce8202fa8e8faa59d3aa00a8167477b5223817b760a733b38864d7625c1d"
|
|
1356
|
+
},
|
|
1352
1357
|
{
|
|
1353
1358
|
"path": "docs/adr/README.md",
|
|
1354
1359
|
"mode": "0000644",
|
|
1355
|
-
"sha256": "
|
|
1360
|
+
"sha256": "79b6d2e5dee626f509ace71bcd133e707ae26b7cbe96a64eb93ebf868c3aabf3"
|
|
1356
1361
|
},
|
|
1357
1362
|
{
|
|
1358
1363
|
"path": "docs/api-reference.md",
|
|
@@ -2837,7 +2842,7 @@
|
|
|
2837
2842
|
{
|
|
2838
2843
|
"path": "modules/economy/credits.js",
|
|
2839
2844
|
"mode": "0000644",
|
|
2840
|
-
"sha256": "
|
|
2845
|
+
"sha256": "71248889315a29d2a1714ad55ffc685987d862b74e877a29ac2de010bea140f3"
|
|
2841
2846
|
},
|
|
2842
2847
|
{
|
|
2843
2848
|
"path": "modules/economy/migrations/economy_001_fix_gds_shipper_description.sql",
|
|
@@ -2852,7 +2857,7 @@
|
|
|
2852
2857
|
{
|
|
2853
2858
|
"path": "modules/economy/reward.js",
|
|
2854
2859
|
"mode": "0000644",
|
|
2855
|
-
"sha256": "
|
|
2860
|
+
"sha256": "59952daf3cb8cadc33f5249fb07552949d390ba37c71299117a2d503f83e422a"
|
|
2856
2861
|
},
|
|
2857
2862
|
{
|
|
2858
2863
|
"path": "modules/economy/routes/cost.js",
|
|
@@ -3527,7 +3532,7 @@
|
|
|
3527
3532
|
{
|
|
3528
3533
|
"path": "modules/lifecycle/db.js",
|
|
3529
3534
|
"mode": "0000644",
|
|
3530
|
-
"sha256": "
|
|
3535
|
+
"sha256": "3b7e1979609fa7ba55253e193596d8d0db169925574934083232057dcd1b3e6a"
|
|
3531
3536
|
},
|
|
3532
3537
|
{
|
|
3533
3538
|
"path": "modules/lifecycle/done-when.js",
|
|
@@ -4237,12 +4242,12 @@
|
|
|
4237
4242
|
{
|
|
4238
4243
|
"path": "package-lock.json",
|
|
4239
4244
|
"mode": "0000644",
|
|
4240
|
-
"sha256": "
|
|
4245
|
+
"sha256": "eb81d3478ad0a0cf69bc7595c726032d4e815e3a63c4c3c4a62ff56c5a8e5e0f"
|
|
4241
4246
|
},
|
|
4242
4247
|
{
|
|
4243
4248
|
"path": "package.json",
|
|
4244
4249
|
"mode": "0000644",
|
|
4245
|
-
"sha256": "
|
|
4250
|
+
"sha256": "c1255deacb68b9b3ea77218a7d5a9072b8203d1b0c4549d25f62eda4a12626a6"
|
|
4246
4251
|
},
|
|
4247
4252
|
{
|
|
4248
4253
|
"path": "public-docs/index.html",
|
|
@@ -5847,7 +5852,7 @@
|
|
|
5847
5852
|
{
|
|
5848
5853
|
"path": "src/module-api.js",
|
|
5849
5854
|
"mode": "0000644",
|
|
5850
|
-
"sha256": "
|
|
5855
|
+
"sha256": "4d1b546ca70ac1b708cab0aec7a1933bf2e877e3d4ce6f170fdb9c5702df5ce9"
|
|
5851
5856
|
},
|
|
5852
5857
|
{
|
|
5853
5858
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6994,6 +6999,11 @@
|
|
|
6994
6999
|
"mode": "0000644",
|
|
6995
7000
|
"sha256": "cd782459f1e4e6ca6df8a0349521aedcc6a867128240a78d2f00e15f0fd5f259"
|
|
6996
7001
|
},
|
|
7002
|
+
{
|
|
7003
|
+
"path": "tests/idea_task_ship_credit.mjs",
|
|
7004
|
+
"mode": "0000644",
|
|
7005
|
+
"sha256": "e7f0807491c094fddedd518380af73a98c7ac17ac9d42736d9ed9a7cd6d4bf51"
|
|
7006
|
+
},
|
|
6997
7007
|
{
|
|
6998
7008
|
"path": "tests/idea_templates.mjs",
|
|
6999
7009
|
"mode": "0000644",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 0174 — Governance is renamed government, its positions are ranks, and "role" is reserved for the three crafts
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-16
|
|
5
|
+
- **Task:** [#1003002](https://cloudbongos.com/builders#/task/1003002) (R01 of goal 1000068 — government terminology)
|
|
6
|
+
- **Amends:** ADR 0151 and ADR 0172 (the editable-rank-roles one) — **vocabulary only**. Every decision either ADR made survives unchanged.
|
|
7
|
+
- **Related:** ADR 0016 (trust boundary — server-enforced, uncached, per request), 0043 (git/ssh trust boundary + the rank floor on protected paths), 0086 (goals + scope walls), 0157 (Archon is rank and identity only)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
The word **role** currently names two unrelated things in this codebase, and a third thing is about to claim it.
|
|
12
|
+
|
|
13
|
+
1. **The permission system's positions.** `governance_roles` holds the four ladder tiers — Newcomer/`xenos`, Contributor/`thetes`, Trusted/`metic`, Owner/`archon` — plus any hand-composed permission bundles an Owner creates. Its key column is `role_key`. `governance_builder_roles` assigns them; `governance_role_permissions` grants to them. Throughout the module and its ADRs these are called *rank-roles*, which is itself a tell: the name needs a qualifier because the noun was already doing other work.
|
|
14
|
+
|
|
15
|
+
2. **The three crafts.** `tasks.discipline` and `builders.preferred_disciplines` carry `engineer` / `artist` / `ideator` (plus a `ui` value scoped for deletion, and `unclassified`) — migrations 014, 086, 176. Goal 1000066 is actively renaming these to **roles**: its criteria are literally `three-roles-only` and `users-hold-roles`, and its design treats Artist, Ideator, and Builder as the roles a session runs as.
|
|
16
|
+
|
|
17
|
+
3. **Discord's server roles.** `modules/discord/discord-role-sync.js` mirrors a builder's rank onto a Discord role badge. This is Discord's own noun for its own concept, reached through its API.
|
|
18
|
+
|
|
19
|
+
So goal 1000066 was on a collision course with the permission module: both would call their central entity a "role", in the same codebase, resolved on the same requests. The owner settled it by directive on 2026-08-16:
|
|
20
|
+
|
|
21
|
+
> *"Governance should be called government. Ranks are the positions in government. The name roles should be reserved for the three core roles of Cloud Bongos. Artist, Ideator, and Builder."*
|
|
22
|
+
|
|
23
|
+
Two facts made the rename cheap enough to do at full depth rather than as a label pass. The rank tiers' grants are ordinary `governance_role_permissions` rows (ADR 0172 established this when it made them editable), so nothing needs remodelling — only renaming. And the substantive authority work is already done and settled: ADR 0151 built the permission model, ADR 0157 fixed what Archon means, ADR 0172 made the tiers editable. There is no pending decision this rename would have to be sequenced around.
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
**1. Governance becomes government, at full depth.** `modules/governance/` → `modules/government/`; the kernel module key `governance` → `government`; `config/governance.neutral.json` and `config/governance.json` → `config/government.neutral.json` and `config/government.json`; the API from `/api/bongos/governance/*` to `/api/bongos/government/*`; the permission keys `governance.manage` → `government.manage` (~50 call sites) and `page.view.governance` → `page.view.government`. Half-measures were considered and rejected: a labels-only pass leaves the UI and the code disagreeing, which is the exact condition that produced this collision.
|
|
28
|
+
|
|
29
|
+
**2. The positions in the government are ranks. The entity IS the rank — there is no second noun.** `governance_roles` → `government_ranks`, `governance_builder_roles` → `government_builder_ranks`, `governance_role_permissions` → `government_rank_permissions`, and the `role_key` column → `rank_key`. "Position" is the word for explaining what a rank *is* in prose; it is not a code term, because introducing it as one would recreate the qualifier problem that "rank-role" already demonstrates. `rank-roles.js` → `ranks.js`, and its exports `planRankRoleSync` / `isSeededRankRole` → `planRankSync` / `isSeededRank`.
|
|
30
|
+
|
|
31
|
+
**3. The kernel port `governance.roleSync` becomes `government.rankSync`.** This is a cross-module seam, not an internal name: it is declared in `modules/governance/module.json` under `provides`, and resolved through `seams.resolveOptional` by `modules/lifecycle/db.js` and `src/bongos/db.js` on the rank-write path. It is also a transaction participant — a failed rotation rolls the rank write back — so the rename must land atomically across the provider and both consumers, never as a partial sweep.
|
|
32
|
+
|
|
33
|
+
**4. Hand-composed permission bundles stay in the one table, as custom ranks.** They keep living in `government_ranks`, distinguished exactly as today by `seeded_rank` and `is_system`.
|
|
34
|
+
|
|
35
|
+
This was the closest call in the directive, and it was decided against the alternative deliberately. The objection is real: a custom bundle is *not* a position on the ladder, it is assigned by hand rather than derived from `builders.rank`, and ADR 0172 §4 already treats the two kinds as different enough that it widened one and refused to widen the other. Splitting them into a second concept — "appointments" was the candidate — would make that existing asymmetry structural instead of implicit. The owner chose the single table anyway, because it keeps this migration a pure rename with no restructuring of a store that every authenticated request reads. Recorded here so the next reader inherits the trade-off rather than rediscovering it: if the Government tab reads wrong once the labels land, splitting is a follow-up goal, not a defect in this one.
|
|
36
|
+
|
|
37
|
+
**5. "Role" means a craft — Artist, Ideator, or Builder — and nothing else.** Enforced, not merely documented: a rule in `scripts/gds/fitness.js` fails the CI `unit` check if "role" reappears on the government side, or if "governance" reappears outside the historical record. The rule ships with its allowlist and a one-line reason per entry, because a check that passes through blanket suppression reads as evidence while proving nothing.
|
|
38
|
+
|
|
39
|
+
**6. Discord's server roles keep the word.** `modules/discord/discord-role-sync.js` and everything it touches are out of scope. That noun belongs to Discord's API; renaming our side of the boundary would make our code describe their system incorrectly. The fitness rule from Decision 5 scopes to the government surface, not to `modules/discord/`.
|
|
40
|
+
|
|
41
|
+
**Explicitly unchanged.** The ladder slugs `xenos`/`thetes`/`metic`/`archon` and their labels Newcomer/Contributor/Trusted/Owner. `builders.rank`. `requireRank`. Every floor, grant, and assignment semantic. ADR 0043's out-of-band enforcement, which reads the rank ladder and never consults this database. This ADR moves no authority; it renames the things that hold it.
|
|
42
|
+
|
|
43
|
+
## Consequences
|
|
44
|
+
|
|
45
|
+
**The scope wall has to be widened before the folder moves, not after.** The kernel module key is load-bearing in a way a folder name usually is not: `MODULE_GLOBS` maps keys to file globs, and every goal's `scope_modules` array names those keys — including goal 1000068's own. Move the folder first and the goal's wall stops matching the files its remaining tasks need to touch, blocking the work mid-goal. Hence R02 adds `government` alongside `governance` and migrates the stored arrays before R04 moves anything.
|
|
46
|
+
|
|
47
|
+
**The database rename and the constant rename must land in the same deploy.** `government_rank_permissions.permission_key` stores `governance.manage` as text, and `catalog.js` holds the same string as a code constant. If the rows change while the old code is still serving, every `governance.manage` check fails closed — and because that key is what gates the Government tab, the failure mode is the Owner locked out of the surface they would use to investigate. R03 and R05 are sequenced accordingly, and R03 takes a backup first.
|
|
48
|
+
|
|
49
|
+
**Two ADRs currently claim the number 0172**, so "ADR 0172" no longer identifies a decision on its own. This ADR amends `<redacted>.md`; the other is `<redacted>.md`. Filed as idea 1000753 with a proposed guard (fail CI on a duplicate ADR number); deliberately not fixed here, because renumbering an ADR mid-rename would put two independent reference churns in the same diff.
|
|
50
|
+
|
|
51
|
+
**ADR 0151 and ADR 0172 get amendment notes, not rewrites.** Their bodies stay in the old vocabulary. An ADR records what was decided and why at a point in time; editing the prose to match today's words would quietly erase the evidence that the words changed — which is the one thing a reader hitting "rank-role" in an old session log will need.
|
|
52
|
+
|
|
53
|
+
**Goal 1000066 can now proceed without ambiguity, and needs to.** This ADR frees the word; 1000066 spends it by renaming `tasks.discipline` and `builders.preferred_disciplines` to roles. Neither half is sufficient alone: until 1000066 lands, "role" is unclaimed rather than correctly claimed, and the fitness rule from Decision 5 only proves the government side is clean. The two goals are independent in their file sets and can run in parallel.
|
|
54
|
+
|
|
55
|
+
**Rejected: renaming the ladder slugs to match.** `xenos`/`thetes`/`metic`/`archon` are referenced by `builders.rank`, `requireRank`, the protected-path enforcers, the Discord role map, and years of session logs. The directive did not ask for it, and bundling it here would turn a bounded rename into a trust-boundary migration.
|
|
56
|
+
|
|
57
|
+
**Rejected: a compatibility shim kept indefinitely.** The old `/governance/*` route paths answer for exactly one release, logging each hit, and R05 records the removal release in this ADR's lineage. A permanent alias would mean both vocabularies stay correct forever, which is the state this ADR exists to end.
|
package/docs/adr/README.md
CHANGED
|
@@ -247,5 +247,6 @@ This keeps the decision history honest and traceable.
|
|
|
247
247
|
| 0172 | [**The sub-Archon rank roles are owner-editable; the Archon role is not; the substrate keys are non-grantable to everything** ([task 1002961](https://cloudbongos.com/builders#/task/1002961), goal 1000030). Amends ADR 0151 §2's "the four seeded rank-roles are immutable through this API" wall — the GRANT half only. Their grants were always ordinary `governance_role_permissions` rows (the `governance_002` seed writes them); the read-only rendering was route policy, not structure. Three walls now, where there was one: the **Archon** rank-role stays frozen as the LOCKOUT GUARD (it is the only role holding `governance.manage` by seed, so freezing it means no click sequence can revoke the permission needed to reach this tab); the three **sub-Archon** rank-roles become grant-editable, the seven **in-app** Archon powers included (owner's explicit choice, escalation consequence stated); and `substrate` becomes a declared catalog axis — `deploy.push_main`/`path.protected.modify`/`infra.ssh` are non-grantable to EVERY role forever, because ADR 0043 enforces them out of band, so a grant row confers nothing and the toggle would be the UI lying. Grantability goes per-role (`isGrantableTo(key, role)`); `isGrantable(key)` survives as the narrow custom-role question config.js strips against. **Custom roles are deliberately NOT widened** — a rank-role moves only by a rank write, a custom role is assignable by anyone holding `governance.manage`. Assignment stays rank-derived (R95b) untouched. Adds `POST /governance/roles/:roleKey/reset` (one transaction, filtered through `isGrantableTo`, refused on a custom role) because a drifted ~60-row rank has no way back by clicking. Rejected: substrate keys behind a warning (a toggle that succeeds and delegates nothing), an editable Archon role behind a confirm (a confirm is not a guard; real recovery is hand-written SQL on the droplet).](<redacted>.md) | governance / permissions |
|
|
248
248
|
| 0171 | [**Profile ↔ cross-project rollup: the join runs in-process on the hub; consent is fail-closed account state; the hide flag lives on the account, enforced in the read path** ([task 1002277](https://cloudbongos.com/builders#/task/1002277), R01 of goal 1000045). The C1 cross-project profile reads the platform-identity rollup through the module's **declared-but-never-registered `platform-identity.hub` port**: the module registers a real provider and core `GET /builders/:id/profile` `resolveOptional`s it beside `reward` — zero network, `null` (clean degrade) on every non-hub instance. Federated instances **link out** instead of inlining (amends profile-page spec Decision 1: an instance-side copy is either stale-on-hide or a per-view hub call). Consent: `profile_state` on `platform_identity_accounts`, **fail-closed** — `active` iff touched by a hub-session `/sso/authorize` (device-poll/check-in create `provisional`; backfill = all provisional, lazy upgrade; no core-`builders` join — type-unsafe vs `system:*` ids and a module-migration boundary breach), so the P03 owner decision can only ever widen, never claw back. The C2 hide flag is `hide_stats` on the account (NOT per-instance `behavior_prefs`), enforced **inside** the module's read functions — `listScoutingRollup` itself gains the `active AND NOT hide_stats` predicate so the live Metic-floor `/people`+`/scouting` surfaces honor it by construction. Hub profile responses go `no-store` (hide must beat the 60s cache). Publishing is gated on hardening the self-reported rollup (membership-row requirement, rank-vocab validation, disciplines caps, check-in→rollup sequencing). Found + filed: the `LATERAL unnest` fan-out inflating rollup sums (task 1002911), the unreserved `u` slug (task 1002912), the missing platform T&C artifact (task 1002913), criterion amendments (task 1002914). Rejected: instance→hub per-view pull (always-on dependency), hub→instance mirroring (stale-on-hide), storing the flag in builder-settings, backfilling `active` from hub `builders` rows.](<redacted>.md) | platform identity / profiles / privacy |
|
|
249
249
|
| 0170 | [**The featured projection tells time honestly: `created_at` in, `updated_at` out, owner from the project's own archon** ([task 1002778](https://cloudbongos.com/builders#/task/1002778), goal 1000061). The hub card's metadata pass added `created_at` to the public `/projects/featured` projection, labelled **Listed** — it is the moment the hub catalogued the row, never when the project began, and the C6 card test bans the word "Created". Two recorded deviations: (1) the task brief said `updated_at` was ready to add, but the column is bumped by every client re-registration AND the Archon's feature toggle, so as a public "last updated" it would report curation noise as project activity — it may sort, never project, and the test pins the SELECT list itself (`SELECT *` banned: the route is CORS-open rank:public and a star would leak `owner_github_id`/`status`). (2) The owner fact reads the project's OWN leaderboard archon row (login + avatar, type-checked and capped) from the payload project-stats already fetches — zero extra requests, the 2-fetch pin intact, the advisory `owner_github_id` column still unread per the design contract. Multi-archon: top-credits archon is named; a fallen-off-the-page archon renders as no owner shown, never a wrong one. Rejected: projecting `updated_at` as asked (it lies), reading `owner_github_id` (advisory, contract-banned), a third per-origin fetch for activity recency (moves the test pin for a fact nobody asked for yet).](<redacted>.md) | platform identity / public surface |
|
|
250
|
+
| 0174 | [**Governance is renamed government, its positions are ranks, and "role" is reserved for the three crafts** ([task 1003002](https://cloudbongos.com/builders#/task/1003002), R01 of goal 1000068). Owner directive, 2026-08-16. The word **role** named two unrelated things: the permission system's ladder tiers (`governance_roles`, the "rank-roles") and the three crafts (`tasks.discipline` = engineer/artist/ideator) — and goal 1000066 was actively claiming it for the crafts (`three-roles-only`, `users-hold-roles`), putting the two on a collision course. Resolved at full depth rather than as a label pass, because a labels-only fix leaves the UI and the code disagreeing, which is the condition that produced the collision. `modules/governance/` → `modules/government/`; the kernel module key, the config files, `/api/bongos/governance/*` → `/government/*`, and the permission keys `governance.manage` → `government.manage` (~50 sites) + `page.view.governance` → `page.view.government`. **The entity IS the rank — no second noun**: `governance_roles` → `government_ranks`, `role_key` → `rank_key`, `rank-roles.js` → `ranks.js`; "position" stays prose-only, since introducing it as a code term would recreate the qualifier problem "rank-role" already demonstrates. The cross-module kernel port `governance.roleSync` → `government.rankSync` must land atomically across its provider and both consumers (`modules/lifecycle/db.js`, `src/bongos/db.js`) because it is a transaction participant on the rank-write path. Hand-composed bundles stay in the one table as **custom ranks** — the owner's call over splitting them into a separate "appointments" concept, with the objection recorded (a custom bundle is not a ladder position, and ADR 0172 §4 already treats the two kinds as different). Enforced by a `fitness.js` rule, allowlist shipped with reasons. **Discord's server roles keep the word** — that noun belongs to Discord's API. Amends ADR 0151 + ADR 0172 (the editable-rank-roles one) in **vocabulary only**; ladder slugs, `builders.rank`, `requireRank`, and every floor/grant/assignment semantic are untouched. Rejected: renaming the ladder slugs (turns a bounded rename into a trust-boundary migration), a permanent compat shim (both vocabularies stay correct forever).](<redacted>.md) | government / permissions |
|
|
250
251
|
|
|
251
252
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
|
@@ -117,6 +117,8 @@ const IDEA_BONUS_FRACTION = 0.25; // 25% slice to the idea's original capturer
|
|
|
117
117
|
// (per-task-ship IC4, governance-pass IC5) will be siblings of this function.
|
|
118
118
|
const IDEA_FULL_PASS_CREDIT = 5; // owner: "gives 5 credits"
|
|
119
119
|
const IDEA_FULL_PASS_BAR = 60; // completeness 0-100; ADR 0172 proposed default
|
|
120
|
+
const IDEA_TASK_SHIP_BASE = 5; // stream 1: base credit per shipped task under an idea
|
|
121
|
+
const QUICK_IDEA_FACTOR = 0.6; // a quick/spark idea's grade factor (ADR 0172 default)
|
|
120
122
|
|
|
121
123
|
// =========================================================================
|
|
122
124
|
// Session token reward (flat cost-plus) — task 1005 (ADR 0054).
|
|
@@ -385,6 +387,74 @@ async function awardIdeaFullPassCredit(client, { ideaId, authorId, completenessS
|
|
|
385
387
|
: { credited: 0, reason: 'already_paid' };
|
|
386
388
|
}
|
|
387
389
|
|
|
390
|
+
// Stream 1 (ADR 0172 / goal 1000067 IC4): as each task descended from an idea
|
|
391
|
+
// SHIPS, pay the idea's author IDEA_TASK_SHIP_BASE credits, SCALED by the idea's
|
|
392
|
+
// grade — a Full Idea by its completeness, a Quick/Spark idea by a flat factor.
|
|
393
|
+
// The ideator lane, so it gates on config.ideatorCredit, never perTaskCredit.
|
|
394
|
+
//
|
|
395
|
+
// THIS REPLACES the ADR 0023 25% idea-promotion slice (maybeAwardIdeaPromotionBonus).
|
|
396
|
+
// The lifecycle ship hook (awardLandingCredit) now calls THIS instead of the
|
|
397
|
+
// slice, so there is one idea-credit-on-ship path, not two (ADR 0172 §4). The
|
|
398
|
+
// old function stays for its unit tests + any compat caller, but is no longer
|
|
399
|
+
// wired at ship.
|
|
400
|
+
//
|
|
401
|
+
// LINEAGE READ, NOT A CODE IMPORT. It reads the ideas-owned lineage + grade
|
|
402
|
+
// tables directly (ideas_task_lineage → idea_inbox → ideas_grades), exactly as
|
|
403
|
+
// maybeAwardIdeaPromotionBonus reads idea_inbox — a cross-table SQL read is
|
|
404
|
+
// allowed across the module boundary; a cross-module require is not.
|
|
405
|
+
//
|
|
406
|
+
// client — pg client / pool (transaction participant)
|
|
407
|
+
// taskId — the task that just shipped
|
|
408
|
+
// config — resolved reward policy; gates on config.ideatorCredit
|
|
409
|
+
//
|
|
410
|
+
// Returns { credited, reason, ideaId, authorId } — credited 0 with a reason for
|
|
411
|
+
// every no-pay path (no source idea, lane off, already paid). Idempotent per
|
|
412
|
+
// (idea, task) via reason='idea.credit.task_ship:<idea>:<task>', builder-scoped
|
|
413
|
+
// so the NOT EXISTS hits idx_credit_log_builder (the IC2 index lesson).
|
|
414
|
+
async function awardIdeaTaskShipCredit(client, { taskId, config = rewardConfig() } = {}) {
|
|
415
|
+
if (config && config.ideatorCredit === false) {
|
|
416
|
+
return { credited: 0, reason: 'ideator_lane_off' };
|
|
417
|
+
}
|
|
418
|
+
// Which idea does this task descend from, who authored it, and at what grade?
|
|
419
|
+
const { rows } = await client.query(
|
|
420
|
+
`SELECT l.idea_id,
|
|
421
|
+
i.captured_by,
|
|
422
|
+
COALESCE(g.grade, 'quick') AS grade,
|
|
423
|
+
g.completeness_score AS completeness
|
|
424
|
+
FROM ideas_task_lineage l
|
|
425
|
+
JOIN idea_inbox i ON i.id = l.idea_id
|
|
426
|
+
LEFT JOIN ideas_grades g ON g.idea_id = l.idea_id
|
|
427
|
+
WHERE l.task_id = $1`,
|
|
428
|
+
[taskId]
|
|
429
|
+
);
|
|
430
|
+
if (rows.length === 0 || rows[0].captured_by == null) {
|
|
431
|
+
return { credited: 0, reason: 'no_source_idea', ideaId: null, authorId: null };
|
|
432
|
+
}
|
|
433
|
+
const { idea_id: ideaId, captured_by: authorId, grade, completeness } = rows[0];
|
|
434
|
+
// Grade factor: a Full Idea earns in proportion to its completeness; a Quick
|
|
435
|
+
// or Spark idea (no completeness score) earns the flat quick factor. A full
|
|
436
|
+
// idea with no score yet also falls back to the quick factor rather than 0 —
|
|
437
|
+
// the work shipped, the idea is at least quick-grade.
|
|
438
|
+
const factor = (grade === 'full' && completeness != null && Number.isFinite(Number(completeness)))
|
|
439
|
+
? Math.max(0, Math.min(1, Number(completeness) / 100))
|
|
440
|
+
: QUICK_IDEA_FACTOR;
|
|
441
|
+
const delta = Math.round(IDEA_TASK_SHIP_BASE * factor);
|
|
442
|
+
if (delta <= 0) {
|
|
443
|
+
return { credited: 0, reason: 'zero_after_scaling', ideaId: Number(ideaId), authorId: Number(authorId) };
|
|
444
|
+
}
|
|
445
|
+
const reason = `idea.credit.task_ship:${ideaId}:${taskId}`;
|
|
446
|
+
const description = `Task #${taskId} shipped under idea #${ideaId} (${grade}${completeness != null ? ` ${Math.round(Number(completeness))}%` : ''}) → ${delta}c to its author (ideator lane stream 1, ADR 0172)`;
|
|
447
|
+
const { rowCount } = await client.query(
|
|
448
|
+
`INSERT INTO credit_log (builder_id, task_id, delta, reason, description)
|
|
449
|
+
SELECT $1, $2, $3, $4, $5
|
|
450
|
+
WHERE NOT EXISTS (SELECT 1 FROM credit_log WHERE builder_id = $1 AND reason = $4)`,
|
|
451
|
+
[authorId, taskId, delta, reason, description]
|
|
452
|
+
);
|
|
453
|
+
return rowCount > 0
|
|
454
|
+
? { credited: delta, reason: 'paid', ideaId: Number(ideaId), authorId: Number(authorId) }
|
|
455
|
+
: { credited: 0, reason: 'already_paid', ideaId: Number(ideaId), authorId: Number(authorId) };
|
|
456
|
+
}
|
|
457
|
+
|
|
388
458
|
// Award the flat cost-plus session token reward, idempotent per session_id
|
|
389
459
|
// (task 1005, ADR 0054). Called inside the same transaction as the session
|
|
390
460
|
// upsert (see upsertSessionRecordWithReward).
|
|
@@ -596,8 +666,11 @@ module.exports = {
|
|
|
596
666
|
insertMultipliedConfirmCredit,
|
|
597
667
|
maybeAwardIdeaPromotionBonus,
|
|
598
668
|
awardIdeaFullPassCredit,
|
|
669
|
+
awardIdeaTaskShipCredit,
|
|
599
670
|
IDEA_FULL_PASS_CREDIT,
|
|
600
671
|
IDEA_FULL_PASS_BAR,
|
|
672
|
+
IDEA_TASK_SHIP_BASE,
|
|
673
|
+
QUICK_IDEA_FACTOR,
|
|
601
674
|
awardSessionTokenReward,
|
|
602
675
|
NET_NEGATIVE_BONUS,
|
|
603
676
|
SUBTRACTOR_THRESHOLD,
|
|
@@ -26,6 +26,8 @@ module.exports = {
|
|
|
26
26
|
maybeAwardIdeaPromotionBonus: (...a) => credits.maybeAwardIdeaPromotionBonus(...a),
|
|
27
27
|
// ADR 0172 IC2 — the ideator credit lane (stream 2): a passing Full Idea pays its author, once.
|
|
28
28
|
awardIdeaFullPassCredit: (...a) => credits.awardIdeaFullPassCredit(...a),
|
|
29
|
+
// ADR 0172 IC4 — stream 1: pay the idea's author per shipped task, grade-scaled (replaces the 25% slice at ship).
|
|
30
|
+
awardIdeaTaskShipCredit: (...a) => credits.awardIdeaTaskShipCredit(...a),
|
|
29
31
|
awardNetNegativeBonus: (...a) => credits.awardNetNegativeBonus(...a),
|
|
30
32
|
awardSessionTokenReward: (...a) => credits.awardSessionTokenReward(...a),
|
|
31
33
|
// gamification hooks the lifecycle fires at claim/ship
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -3597,9 +3597,19 @@ async function awardLandingCredit(client, { taskId, creditedBuilderId, creditsRe
|
|
|
3597
3597
|
const credit = await reward.insertMultipliedConfirmCredit(
|
|
3598
3598
|
client, creditedBuilderId, taskId, creditsReward, kind, 'task.shipped'
|
|
3599
3599
|
);
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
)
|
|
3600
|
+
// Stream 1 of the ideator credit lane (ADR 0172 / IC4) REPLACES the ADR 0023
|
|
3601
|
+
// 25% idea-promotion slice here — one idea-credit-on-ship path, not two
|
|
3602
|
+
// (ADR 0172 §4). Unlike the slice (25% of the builder estimate, so zero under
|
|
3603
|
+
// cost-plus-only), this pays 5×grade-factor on the IDEATOR lane, so an idea
|
|
3604
|
+
// earns as its work ships even when per-task builder credit is withheld. It
|
|
3605
|
+
// reads the one-idea-many-tasks lineage (ideas_task_lineage, IC3), so a Full
|
|
3606
|
+
// Idea that became a whole goal pays on every one of its shipped tasks — the
|
|
3607
|
+
// old slice only saw the single promoted_to_task_id. `maybeAwardIdeaPromotionBonus`
|
|
3608
|
+
// stays on the port for its unit tests / any compat caller, but is no longer
|
|
3609
|
+
// wired at ship. Guarded so a null return (lane off, no source idea) is fine.
|
|
3610
|
+
const ideaBonus = reward.awardIdeaTaskShipCredit
|
|
3611
|
+
? await reward.awardIdeaTaskShipCredit(client, { taskId })
|
|
3612
|
+
: null;
|
|
3603
3613
|
return {
|
|
3604
3614
|
creditsAwarded: credit.creditsAwarded,
|
|
3605
3615
|
creditsRaw: credit.raw,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.181",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.181",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.181",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -785,8 +785,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
785
785
|
// landed since 1.19.177 with no explicit bump. run 31971769062. (task 1002620)
|
|
786
786
|
// 1.19.179 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
787
787
|
// landed since 1.19.178 with no explicit bump. run 31972209764. (task 1002620)
|
|
788
|
+
// 1.19.180 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
789
|
+
// landed since 1.19.179 with no explicit bump. run 31972568511. (task 1002620)
|
|
790
|
+
// 1.19.181 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
791
|
+
// landed since 1.19.180 with no explicit bump. run 31972686846. (task 1002620)
|
|
788
792
|
// ---------------------------------------------------------------------------
|
|
789
|
-
const CORE_VERSION = '1.19.
|
|
793
|
+
const CORE_VERSION = '1.19.181'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
790
794
|
|
|
791
795
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
792
796
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// tests/idea_task_ship_credit.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002998 (BV1.R174 / IC4, goal 1000067) — stream 1 of the ideator credit
|
|
4
|
+
// lane: as each task descended from an idea SHIPS, pay the idea's author
|
|
5
|
+
// 5 × grade-factor, once. ADR 0172. Replaces the ADR 0023 25% slice at ship.
|
|
6
|
+
//
|
|
7
|
+
// The done-when as assertions: a shipped task under a Full Idea pays
|
|
8
|
+
// 5 × completeness%; under a Quick idea pays 5 × the flat factor; a Full Idea
|
|
9
|
+
// that became a GOAL pays on EVERY one of its shipped tasks (the one-idea-many-
|
|
10
|
+
// tasks point); re-running never double-pays per (idea, task); cost-plus-only
|
|
11
|
+
// does NOT suppress it; a task with no source idea pays nothing.
|
|
12
|
+
//
|
|
13
|
+
// awardIdeaTaskShipCredit is exercised over a stub client whose credit_log is a
|
|
14
|
+
// set of reasons and whose lineage/idea/grade SELECT returns a scripted row —
|
|
15
|
+
// so the grade-factor math and the per-(idea,task) idempotency are proven
|
|
16
|
+
// without a DB.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/idea_task_ship_credit.mjs
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const credits = require('../modules/economy/credits.js');
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
async function test(name, fn) {
|
|
29
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
30
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// `source` is the {idea_id, captured_by, grade, completeness} the lineage SELECT
|
|
34
|
+
// returns for a task (null → no source idea). `credited` is the in-memory ledger.
|
|
35
|
+
function stubClient(source) {
|
|
36
|
+
const credited = [];
|
|
37
|
+
return {
|
|
38
|
+
credited,
|
|
39
|
+
async query(sql, params) {
|
|
40
|
+
if (/FROM ideas_task_lineage/i.test(sql)) {
|
|
41
|
+
return { rows: source ? [source] : [] };
|
|
42
|
+
}
|
|
43
|
+
if (/INSERT INTO credit_log/i.test(sql)) {
|
|
44
|
+
assert.match(sql, /builder_id = \$1 AND reason = \$4/, 'idempotency must be builder-scoped for the index');
|
|
45
|
+
const reason = params[3];
|
|
46
|
+
if (credited.some((r) => r.reason === reason)) return { rowCount: 0, rows: [] };
|
|
47
|
+
credited.push({ builder_id: params[0], task_id: params[1], delta: params[2], reason });
|
|
48
|
+
return { rowCount: 1, rows: [] };
|
|
49
|
+
}
|
|
50
|
+
return { rows: [] };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ON = { ideatorCredit: true };
|
|
56
|
+
|
|
57
|
+
await test('a shipped task under a FULL idea pays 5 × completeness%', async () => {
|
|
58
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
59
|
+
const r = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: ON });
|
|
60
|
+
assert.equal(r.credited, 4, '5 × 0.80 = 4');
|
|
61
|
+
assert.equal(r.reason, 'paid');
|
|
62
|
+
assert.equal(c.credited[0].builder_id, 3);
|
|
63
|
+
assert.equal(c.credited[0].reason, 'idea.credit.task_ship:7:100');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await test('a shipped task under a QUICK idea pays 5 × the flat factor', async () => {
|
|
67
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'quick', completeness: null });
|
|
68
|
+
const r = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: ON });
|
|
69
|
+
assert.equal(r.credited, Math.round(credits.IDEA_TASK_SHIP_BASE * credits.QUICK_IDEA_FACTOR)); // 5 × 0.6 = 3
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await test('a full idea with 100% pays the full base; a spark falls back to the quick factor', async () => {
|
|
73
|
+
const full = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 100 });
|
|
74
|
+
assert.equal((await credits.awardIdeaTaskShipCredit(full, { taskId: 100, config: ON })).credited, 5);
|
|
75
|
+
const spark = stubClient({ idea_id: 7, captured_by: 3, grade: 'spark', completeness: null });
|
|
76
|
+
assert.equal((await credits.awardIdeaTaskShipCredit(spark, { taskId: 100, config: ON })).credited, 3);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await test('one idea → many tasks: EACH shipped task pays (the whole point of stream 1)', async () => {
|
|
80
|
+
// same idea 7, three different tasks — three separate credits
|
|
81
|
+
for (const taskId of [100, 101, 102]) {
|
|
82
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
83
|
+
const r = await credits.awardIdeaTaskShipCredit(c, { taskId, config: ON });
|
|
84
|
+
assert.equal(r.credited, 4, `task ${taskId} pays`);
|
|
85
|
+
assert.equal(c.credited[0].reason, `idea.credit.task_ship:7:${taskId}`, 'distinct per task');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await test('re-running the same (idea, task) never double-pays', async () => {
|
|
90
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
91
|
+
const first = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: ON });
|
|
92
|
+
const second = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: ON });
|
|
93
|
+
assert.equal(first.credited, 4);
|
|
94
|
+
assert.equal(second.credited, 0);
|
|
95
|
+
assert.equal(second.reason, 'already_paid');
|
|
96
|
+
assert.equal(c.credited.length, 1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await test('COST-PLUS-ONLY does NOT suppress stream 1 — the ideator lane pays', async () => {
|
|
100
|
+
const policy = credits.rewardPolicy({ mode: 'cost-plus-only' });
|
|
101
|
+
assert.equal(policy.perTaskCredit, false);
|
|
102
|
+
const c = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
103
|
+
const r = await credits.awardIdeaTaskShipCredit(c, { taskId: 100, config: policy });
|
|
104
|
+
assert.equal(r.credited, 4, 'paid despite cost-plus-only');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await test('the lane opted out, or a task with no source idea, pays nothing', async () => {
|
|
108
|
+
const off = stubClient({ idea_id: 7, captured_by: 3, grade: 'full', completeness: 80 });
|
|
109
|
+
assert.equal((await credits.awardIdeaTaskShipCredit(off, { taskId: 100, config: { ideatorCredit: false } })).reason, 'ideator_lane_off');
|
|
110
|
+
const orphan = stubClient(null);
|
|
111
|
+
const r = await credits.awardIdeaTaskShipCredit(orphan, { taskId: 100, config: ON });
|
|
112
|
+
assert.equal(r.credited, 0);
|
|
113
|
+
assert.equal(r.reason, 'no_source_idea');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
console.log(`\nidea_task_ship_credit: ${passed} passed, ${failed} failed`);
|
|
117
|
+
if (failed > 0) process.exit(1);
|