@bongos/core 1.24.2 → 1.24.4
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 +111 -51
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/config/branding.neutral.json +0 -1
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +11 -0
- package/docs/api/openapi.json +121 -4
- package/docs/api-reference.md +8 -1
- package/docs/branding-contract.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +7 -7
- package/modules/discord/ship-broadcast.js +7 -0
- package/modules/economy/credit-wording.js +93 -0
- package/modules/economy/credits.js +26 -12
- package/modules/economy/governor-docket.js +12 -6
- package/modules/economy/reward-policy.js +49 -3
- package/modules/economy/reward.js +5 -0
- package/modules/government/routes/government.js +7 -0
- package/modules/hall-ui/public/street-inside.js +1 -1
- package/modules/ideas/routes/inbox.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +6 -4
- package/modules/market-watch/ask.js +425 -0
- package/modules/market-watch/migrations/market_watch_003_ask_pool.sql +21 -0
- package/modules/market-watch/module.json +10 -3
- package/modules/market-watch/pollers/weekly-ask.js +89 -0
- package/modules/market-watch/public/hall-widget.js +271 -0
- package/modules/market-watch/public/weekly-ask.css +39 -0
- package/modules/market-watch/routes/ask.js +95 -0
- package/modules/onboarding/routes/access-requests.js +38 -10
- package/modules/platform-identity/account-logins.js +13 -0
- package/modules/platform-identity/routes/my-projects.js +10 -7
- package/modules/platform-identity/routes/sso.js +11 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +22 -0
- package/scripts/bongos/backfill-subagent-rewards.js +7 -7
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/scripts/hall-preview/server.js +13 -0
- package/src/bongos/auth-idp.js +17 -5
- package/src/bongos/db-kernel.js +14 -1
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +1 -1
- package/tests/account_logins.mjs +6 -4
- package/tests/backfill_subagent_rewards.mjs +7 -7
- package/tests/credit_grant.mjs +7 -7
- package/tests/credit_wording.mjs +154 -0
- package/tests/google_halls.mjs +211 -0
- package/tests/governor_docket.mjs +7 -3
- package/tests/hall_street_inside.mjs +2 -2
- package/tests/market_watch_ask.mjs +369 -0
- package/tests/market_watch_ask_db.mjs +122 -0
- package/tests/market_watch_data_db.mjs +4 -0
- package/tests/market_watch_module.mjs +9 -5
- package/tests/project_settings_page.mjs +1 -1
- package/tests/reward_cost_scale.mjs +200 -0
package/docs/api/openapi.json
CHANGED
|
@@ -197,6 +197,10 @@
|
|
|
197
197
|
"name": "llm-cache",
|
|
198
198
|
"description": "Endpoints under `/api/bongos/llm-cache`."
|
|
199
199
|
},
|
|
200
|
+
{
|
|
201
|
+
"name": "market-watch",
|
|
202
|
+
"description": "Endpoints under `/api/bongos/market-watch`."
|
|
203
|
+
},
|
|
200
204
|
{
|
|
201
205
|
"name": "me",
|
|
202
206
|
"description": "Endpoints under `/api/bongos/me`."
|
|
@@ -13952,6 +13956,86 @@
|
|
|
13952
13956
|
]
|
|
13953
13957
|
}
|
|
13954
13958
|
},
|
|
13959
|
+
"/market-watch/ask": {
|
|
13960
|
+
"get": {
|
|
13961
|
+
"operationId": "get_market_watch_ask",
|
|
13962
|
+
"tags": [
|
|
13963
|
+
"market-watch"
|
|
13964
|
+
],
|
|
13965
|
+
"summary": "GET /market-watch/ask",
|
|
13966
|
+
"description": "every member reads their own card; nobody else's (isMember in gate()).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
13967
|
+
"x-rank": "any-builder",
|
|
13968
|
+
"x-source": "modules/market-watch/routes/ask.js",
|
|
13969
|
+
"responses": {
|
|
13970
|
+
"200": {
|
|
13971
|
+
"description": "Success.",
|
|
13972
|
+
"content": {
|
|
13973
|
+
"application/json": {
|
|
13974
|
+
"schema": {
|
|
13975
|
+
"$ref": "#/components/schemas/GetMarketWatchAskResponse"
|
|
13976
|
+
}
|
|
13977
|
+
}
|
|
13978
|
+
}
|
|
13979
|
+
},
|
|
13980
|
+
"400": {
|
|
13981
|
+
"$ref": "#/components/responses/BadRequest"
|
|
13982
|
+
},
|
|
13983
|
+
"401": {
|
|
13984
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
13985
|
+
},
|
|
13986
|
+
"403": {
|
|
13987
|
+
"$ref": "#/components/responses/Forbidden"
|
|
13988
|
+
}
|
|
13989
|
+
},
|
|
13990
|
+
"security": [
|
|
13991
|
+
{
|
|
13992
|
+
"builderSession": []
|
|
13993
|
+
}
|
|
13994
|
+
]
|
|
13995
|
+
}
|
|
13996
|
+
},
|
|
13997
|
+
"/market-watch/ask/answer": {
|
|
13998
|
+
"post": {
|
|
13999
|
+
"operationId": "post_market_watch_ask_answer",
|
|
14000
|
+
"tags": [
|
|
14001
|
+
"market-watch"
|
|
14002
|
+
],
|
|
14003
|
+
"summary": "POST /market-watch/ask/answer",
|
|
14004
|
+
"description": "a member answers for themselves only (isMember in gate()).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
14005
|
+
"x-rank": "any-builder",
|
|
14006
|
+
"x-source": "modules/market-watch/routes/ask.js",
|
|
14007
|
+
"requestBody": {
|
|
14008
|
+
"required": false,
|
|
14009
|
+
"content": {
|
|
14010
|
+
"application/json": {
|
|
14011
|
+
"schema": {
|
|
14012
|
+
"$ref": "#/components/schemas/PostMarketWatchAskAnswerRequest"
|
|
14013
|
+
}
|
|
14014
|
+
}
|
|
14015
|
+
},
|
|
14016
|
+
"x-validated": true
|
|
14017
|
+
},
|
|
14018
|
+
"responses": {
|
|
14019
|
+
"200": {
|
|
14020
|
+
"description": "Success."
|
|
14021
|
+
},
|
|
14022
|
+
"400": {
|
|
14023
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
14024
|
+
},
|
|
14025
|
+
"401": {
|
|
14026
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
14027
|
+
},
|
|
14028
|
+
"403": {
|
|
14029
|
+
"$ref": "#/components/responses/Forbidden"
|
|
14030
|
+
}
|
|
14031
|
+
},
|
|
14032
|
+
"security": [
|
|
14033
|
+
{
|
|
14034
|
+
"builderSession": []
|
|
14035
|
+
}
|
|
14036
|
+
]
|
|
14037
|
+
}
|
|
14038
|
+
},
|
|
13955
14039
|
"/me": {
|
|
13956
14040
|
"get": {
|
|
13957
14041
|
"operationId": "get_me",
|
|
@@ -19493,7 +19577,7 @@
|
|
|
19493
19577
|
"project-settings"
|
|
19494
19578
|
],
|
|
19495
19579
|
"summary": "GET /project-settings",
|
|
19496
|
-
"description": "GET /project-settings — all five rows in ONE read. rank: project.settings.manage (metic+, government_010), the SAME floor as the page it serves (requireGovernmentPage). The first cut gated this any-builder on the reasoning that it is \"the project's own published terms\" — but the payload also carries reward-policy internals (marginPct,
|
|
19580
|
+
"description": "GET /project-settings — all five rows in ONE read. rank: project.settings.manage (metic+, government_010), the SAME floor as the page it serves (requireGovernmentPage). The first cut gated this any-builder on the reasoning that it is \"the project's own published terms\" — but the payload also carries reward-policy internals (marginPct, costScale, mode), a raw rejected project_settings value, and internal file/migration paths, and a xenos who cannot even load the page could still curl those. Matching the read floor to the page's (task 1003280 grade, hacker lens) closes that asymmetry; the two WRITES already gate on this same atom, so the whole surface is now one floor. One read, not five: the page draws five rows, so it asks for five rows. Fetching per-row would put the hall's own settings page in the shape goal 1000072's criterion names (hall-ui/CLAUDE.md, \"a card asks the server for the rows it draws\").\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `project.settings.manage` (all required).",
|
|
19497
19581
|
"x-rank": "metic+archon",
|
|
19498
19582
|
"x-source": "modules/lifecycle/routes/project-settings.js",
|
|
19499
19583
|
"x-permissions": [
|
|
@@ -31051,6 +31135,15 @@
|
|
|
31051
31135
|
"learnings"
|
|
31052
31136
|
]
|
|
31053
31137
|
},
|
|
31138
|
+
"GetMarketWatchAskResponse": {
|
|
31139
|
+
"type": "object",
|
|
31140
|
+
"properties": {
|
|
31141
|
+
"card": {}
|
|
31142
|
+
},
|
|
31143
|
+
"required": [
|
|
31144
|
+
"card"
|
|
31145
|
+
]
|
|
31146
|
+
},
|
|
31054
31147
|
"GetMeAchievementsResponse": {
|
|
31055
31148
|
"type": "object",
|
|
31056
31149
|
"properties": {
|
|
@@ -36533,6 +36626,30 @@
|
|
|
36533
36626
|
"stored"
|
|
36534
36627
|
]
|
|
36535
36628
|
},
|
|
36629
|
+
"PostMarketWatchAskAnswerRequest": {
|
|
36630
|
+
"type": "object",
|
|
36631
|
+
"properties": {
|
|
36632
|
+
"week_of": {
|
|
36633
|
+
"type": "string",
|
|
36634
|
+
"maxLength": 10
|
|
36635
|
+
},
|
|
36636
|
+
"picks": {
|
|
36637
|
+
"type": "array",
|
|
36638
|
+
"maxItems": 20
|
|
36639
|
+
},
|
|
36640
|
+
"names": {
|
|
36641
|
+
"type": "array",
|
|
36642
|
+
"maxItems": 6
|
|
36643
|
+
},
|
|
36644
|
+
"nothing": {
|
|
36645
|
+
"type": "boolean"
|
|
36646
|
+
},
|
|
36647
|
+
"builder_id": {
|
|
36648
|
+
"type": "integer"
|
|
36649
|
+
}
|
|
36650
|
+
},
|
|
36651
|
+
"additionalProperties": false
|
|
36652
|
+
},
|
|
36536
36653
|
"PostMeAnthropicLinkRequest": {
|
|
36537
36654
|
"type": "object",
|
|
36538
36655
|
"properties": {
|
|
@@ -40319,9 +40436,9 @@
|
|
|
40319
40436
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
40320
40437
|
}
|
|
40321
40438
|
},
|
|
40322
|
-
"x-endpoint-count":
|
|
40323
|
-
"x-schema-count":
|
|
40439
|
+
"x-endpoint-count": 636,
|
|
40440
|
+
"x-schema-count": 686,
|
|
40324
40441
|
"x-undocumented-bodies": 12,
|
|
40325
|
-
"x-response-schemas":
|
|
40442
|
+
"x-response-schemas": 441,
|
|
40326
40443
|
"x-generated-by": "scripts/bongos/gen-api-docs.js"
|
|
40327
40444
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 636 endpoints across 105 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -518,6 +518,13 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
518
518
|
| POST | `/api/bongos/llm-cache/lookup` | `metic+archon` | `cache_key`, `tokens_saved_est` | POST /llm-cache/lookup — hit/miss + atomic hit_count bump. |
|
|
519
519
|
| POST | `/api/bongos/llm-cache/store` | `metic+archon` | `spec`, `result_stdout`, `result_cost_usd` | POST /llm-cache/store — upsert a freshly-produced result. |
|
|
520
520
|
|
|
521
|
+
## `market-watch` (2)
|
|
522
|
+
|
|
523
|
+
| Method | Path | Rank | Body | Description |
|
|
524
|
+
|---|---|---|---|---|
|
|
525
|
+
| GET | `/api/bongos/market-watch/ask` | `any-builder` | — | every member reads their own card; nobody else's (isMember in gate()). |
|
|
526
|
+
| POST | `/api/bongos/market-watch/ask/answer` | `any-builder` | `week_of`, `picks`, `names`, `nothing`, `builder_id` | a member answers for themselves only (isMember in gate()). |
|
|
527
|
+
|
|
521
528
|
## `me` (54)
|
|
522
529
|
|
|
523
530
|
| Method | Path | Rank | Body | Description |
|
|
@@ -33,7 +33,7 @@ env override → instance config (config/branding.json) → neutral star
|
|
|
33
33
|
| `repo` | `{ owner, name }` GitHub binding. |
|
|
34
34
|
| `db.database` | DB name Bongos pool connects to when neither `DATABASE_URL` nor `PGHOST`/`PGDATABASE` is set (`src/bongos/pool.js`, ADR 0062 §5). `null` in the neutral starter = **fail loud** rather than silently target a host DB. |
|
|
35
35
|
| `currency` | `{ label, symbol }` — the reward unit label (e.g. "example"). Display only. |
|
|
36
|
-
| `reward` | `{ marginPct,
|
|
36
|
+
| `reward` | `{ marginPct, costScale, mode }` — the per-instance reward POLICY the economy module reads ([ADR 0054](adr/0054-session-token-reward-cost-plus.md) + ADR 0146, tasks 2009/1002270): the cost-plus margin percent (default `20`), the sizing factor `costScale` (default `1`; it sizes credits from cost and is not an exchange rate — a credit has no price, [ADR 0387](adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md)), and `mode` — `"cost-plus-and-estimate"` (default; pay both the per-task author estimate AND the cost-plus session reward) or `"cost-plus-only"` (suppress the per-task estimate payout; the cost-plus session reward is the sole equity reward). Two per-craft lanes sit beside `mode` and are not derived from it: `ideatorCredit` ([ADR 0172](adr/0172-per-craft-compensation-ideator-credit-lane.md)) and `artistCredit` ([ADR 0341](adr/0341-the-page-is-the-unit-of-tweak-mode.md) D10, an approved page tweak pays its artist), each default on and withheld only by an explicit `false`. A committed, code-reviewed change — not a live UI knob. Omit ⇒ code defaults (20% margin, scale 1, pay both streams, both lanes on). **`usdPeg` is the deprecated old name of `costScale`** (task 1004968): it is still read, with a one-line deprecation note in the server log, so no host pack breaks; when both are set `costScale` wins. The neutral pack sets neither key (the default `1` lives in code), so a host pack's old `usdPeg` is never shadowed by the deep-merge. Rename it at your next pack edit — support for the old key goes in a later release. |
|
|
37
37
|
| `models` | `{ main, subagentDefault, subagentRoutine }` — the per-instance subagent model-allocation DEFAULT the builder-settings module reads (task 2011): the tier for the main session, subagent-default work, and routine plumbing (tiers: `haiku`/`sonnet`/`opus`/`fable`). A per-builder override resolves over this. Omit ⇒ code defaults (`opus`/`opus`/`haiku`). |
|
|
38
38
|
| `tracks` | The configurable version-track set (replaces the hardcoded `product\|internal`). |
|
|
39
39
|
| `theme` | `{ palette, mode, ui }` — palette file ref + theme mode, plus `ui`, the WEB look `themeMarkup()` splices into every served page as a plain `:root` after the page's stylesheet: `colors` (the thirteen contract tokens — **light-mode values**; each stylesheet derives its own dark tier from them at `:root[data-mode="dark"]` + its `prefers-color-scheme` twin, which outrank the pack), `fonts` (`display`, `body`, `links`), `favicon`, and `backdrop` — a `body{background:…}` rule that outranks every sheet in **both** modes, so a dual-mode world sets it to `var(--bg)` (the neutral pack does since [task 1003298](https://cloudbongos.com/builders#/task/1003298), ADR 0196) rather than a literal. The neutral values are the chrome world's; `config/design-tokens.neutral.json` must mirror `colors` + `fonts` key for key (`tests/design_tokens_sync.mjs`), and the world's derived recipes are recorded in `DESIGN.md`. The ui-design module's **style library** (`modules/ui-design/styles/`, [ADR 0219](adr/0219-a-look-is-a-branding-pack-the-style-library.md)) ships ready-made `theme.ui` blocks (looks) an instance copies into this field; every served sheet reads only these fifteen, so a look re-themes every surface with no page edit. |
|
|
@@ -3266,5 +3266,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3266
3266
|
landed since 1.24.0 with no explicit bump. run 40985289858. (task 1002620)
|
|
3267
3267
|
1.24.2 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3268
3268
|
landed since 1.24.1 with no explicit bump. run 41000649913. (task 1002620)
|
|
3269
|
+
1.24.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3270
|
+
landed since 1.24.2 with no explicit bump. run 41004393103. (task 1002620)
|
|
3269
3271
|
---------------------------------------------------------------------------
|
|
3270
3272
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"adr": "0341 D3",
|
|
6
6
|
"reread": "node scripts/bongos/page-reader.js --stale",
|
|
7
7
|
"how": ["Generated by node scripts/bongos/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":48,"lines":5456,"placed":1683,"shared":427,"unplaced":3346,"omitted_data":
|
|
8
|
+
"counts": {"pages":48,"lines":5456,"placed":1683,"shared":427,"unplaced":3346,"omitted_data":3731},
|
|
9
9
|
"pages": [
|
|
10
10
|
{
|
|
11
11
|
"id": "builders:ai-studio",
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
"surface": "builders",
|
|
272
272
|
"title": "The city",
|
|
273
273
|
"states_read": ["city","fullscreen","switched","focus","pins","living","card","card-ask"],
|
|
274
|
-
"files_hash": "
|
|
274
|
+
"files_hash": "6a8a96e1f9f6084a",
|
|
275
275
|
"reading_hash": "77470c4eb3047a05",
|
|
276
276
|
"counts": {"lines":43,"placed":10,"shared":8,"unplaced":25},
|
|
277
277
|
"omitted": {"data":17},
|
|
@@ -3370,10 +3370,10 @@
|
|
|
3370
3370
|
"surface": "builders",
|
|
3371
3371
|
"title": "The street",
|
|
3372
3372
|
"states_read": ["street","fullscreen","card","switched","focus","here","inside-rules","inside-disputes","inside-people","inside-planning","inside-automation","inside-review","inside-release","inside-treasury","floor","amend","district","desk","desk-facts","desk-quiet","desk-people-builders","desk-people-tier","desk-disputes","desk-automation-goals","desk-release-core","desk-rules","desk-rules-category","desk-planning-priorities","desk-planning-ideas","desk-planning-blockers","pins","pins-pager","living","deck","deck-treasury","sitting","object","voted","admission","settled","blocker","blocker-more","report","report-more","core","core-more","recheck","recheck-more","idea","idea-more","queue","queue-more","automation","automation-more"],
|
|
3373
|
-
"files_hash": "
|
|
3374
|
-
"reading_hash": "
|
|
3373
|
+
"files_hash": "6180e15ad9dcf4d6",
|
|
3374
|
+
"reading_hash": "b5cd1724efa21f28",
|
|
3375
3375
|
"counts": {"lines":650,"placed":75,"shared":8,"unplaced":567},
|
|
3376
|
-
"omitted": {"data":
|
|
3376
|
+
"omitted": {"data":553},
|
|
3377
3377
|
"notes": ["sitting: an action failed (page.click: Timeout 6000ms exceeded.); read as it stood","object: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"board:47\"]); read as it stood","voted: an action failed (page.click: Timeout 6000ms exceeded.); read as it stood","admission: an action failed (page.click: Timeout 6000ms exceeded.); read as it stood","settled: an action failed (page.click: Timeout 6000ms exceeded.); read as it stood","blocker: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"blockers:11\"]); read as it stood","blocker-more: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"blockers:11\"]); read as it stood","report: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"redteam_reports:4\"]); read as it stood","report-more: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"redteam_reports:4\"]); read as it stood","core: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"core_update:1.21.20\"]); read as it stood","core-more: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"core_update:1.21.20\"]); read as it stood","recheck: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"module_rechecks:7\"]); read as it stood","recheck-more: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"module_rechecks:7\"]); read as it stood","idea: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"board:41\"]); read as it stood","idea-more: an action failed (page.evaluate: Error: dispatchClick: nothing matches .street-pin[data-matter=\"board:41\"]); read as it stood"],
|
|
3378
3378
|
"lines": [
|
|
3379
3379
|
{"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":868,"string_id":"87cee78ca134"},
|
|
@@ -3597,7 +3597,7 @@
|
|
|
3597
3597
|
{"key":"L0219","section":"Spending matters","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9","state":"inside-treasury"},
|
|
3598
3598
|
{"key":"L0220","section":"Spending matters","text":"{…} from the reserve?","placement":"unplaced","state":"inside-treasury"},
|
|
3599
3599
|
{"key":"L0221","section":"Spending matters","text":"{…} · today","placement":"unplaced","state":"inside-treasury"},
|
|
3600
|
-
{"key":"L0222","section":"Spending matters","text":"
|
|
3600
|
+
{"key":"L0222","section":"Spending matters","text":"7 builders earned 5,5… · today","placement":"unplaced","state":"inside-treasury"},
|
|
3601
3601
|
{"key":"L0223","section":"Spending matters","text":"{…} · 4w","placement":"unplaced","state":"inside-treasury","occurrences":2},
|
|
3602
3602
|
{"key":"L0224","section":"Spending matters","text":"the record","placement":"placed","file":"modules/hall-ui/public/street.html","line":192,"string_id":"4bde7b562cf4","state":"inside-treasury"},
|
|
3603
3603
|
{"key":"L0225","section":"Spending matters","text":"1 matter on the docket · 16 on the {…}","placement":"unplaced","state":"inside-treasury"},
|
|
@@ -3832,7 +3832,7 @@
|
|
|
3832
3832
|
{"key":"L0454","section":"Priorities","text":"standing law","placement":"unplaced","state":"desk-planning-priorities"},
|
|
3833
3833
|
{"key":"L0455","section":"Waiting on {…}","text":"Waiting on {…}","placement":"placed","file":"modules/hall-ui/public/goals-lib.js","line":91,"string_id":"b18832e219dc","state":"inside-planning"},
|
|
3834
3834
|
{"key":"L0456","section":"Waiting on {…}","text":"9 waiting · oldest 3 weeks","placement":"unplaced","state":"inside-planning"},
|
|
3835
|
-
{"key":"L0457","section":"Waiting on {…}","text":"waiting","placement":"placed","file":"modules/hall-ui/public/work.js","line":
|
|
3835
|
+
{"key":"L0457","section":"Waiting on {…}","text":"waiting","placement":"placed","file":"modules/hall-ui/public/work.js","line":438,"string_id":"8186b562553a","state":"inside-planning"},
|
|
3836
3836
|
{"key":"L0458","section":"Waiting on {…}","text":"on {…}","placement":"unplaced","state":"inside-planning"},
|
|
3837
3837
|
{"key":"L0459","section":"Waiting on {…}","text":"WA","placement":"unplaced","state":"inside-planning"},
|
|
3838
3838
|
{"key":"L0460","section":"Waiting on {…}","text":"Working area 2…","placement":"unplaced","state":"inside-planning"},
|
|
@@ -96,6 +96,13 @@ async function postShipBroadcast(task, deps = {}) {
|
|
|
96
96
|
|
|
97
97
|
const postWebhook = deps.postWebhook || require('./webhook').postWebhook;
|
|
98
98
|
const log = deps.log || console.log;
|
|
99
|
+
// task 1004969 (ADR 0385 decision 9): an AI-written summary that calls credits
|
|
100
|
+
// or models an investment is FLAGGED in the log and posted unchanged — recorded,
|
|
101
|
+
// never blocked or rewritten. No economy module, no check.
|
|
102
|
+
const reward = deps.reward !== undefined ? deps.reward : require('../../src/module-api').resolveOptional('reward');
|
|
103
|
+
if (reward && typeof reward.flagInvestmentWording === 'function') {
|
|
104
|
+
result.wording_flags = reward.flagInvestmentWording(summary, { where: 'ship summary', ref: `task ${task.id}`, log }).length;
|
|
105
|
+
}
|
|
99
106
|
const pageTweak = task.source === PAGE_TWEAK_SOURCE;
|
|
100
107
|
const pageArtist = pageTweak && sec === false ? await pageArtistName(task, deps) : null;
|
|
101
108
|
const targets = chooseBroadcastTargets({ securitySensitive: sec, taskId: task.id, summary, kind: task.kind, pageTweak, pageArtist });
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/economy/credit-wording.js — finds investment wording next to credits
|
|
4
|
+
// or models (task 1004969; ADR 0385 decision 9).
|
|
5
|
+
//
|
|
6
|
+
// WHY. Credits are a contribution meter, not money and not a stake (ADR 0385,
|
|
7
|
+
// ADR 0387), and a module or a model is not something a builder invests in.
|
|
8
|
+
// Wording that says otherwise keeps coming back — in code comments, onboarding
|
|
9
|
+
// docs and AI-written ship summaries — and every time it does the project reads
|
|
10
|
+
// as if it were selling a security. This is the one check for it.
|
|
11
|
+
//
|
|
12
|
+
// THE RULE. A sentence is flagged when an investment word (investment, return,
|
|
13
|
+
// appreciation, passive income, equity, ownership) sits within NEAR_WORDS words
|
|
14
|
+
// of a credit word or a model word. A sentence that DENIES it ("credits are not
|
|
15
|
+
// an investment", "never equity") is the wording we want, so a negation shortly
|
|
16
|
+
// before the investment word clears it.
|
|
17
|
+
//
|
|
18
|
+
// It only FINDS. Callers decide what to do: the wording test fails on a hit, and
|
|
19
|
+
// the server publish paths log a flag and publish the text unchanged — never a
|
|
20
|
+
// silent rewrite, never a block. PURE: no I/O, so it is safe on any path.
|
|
21
|
+
|
|
22
|
+
const NEAR_WORDS = 12;
|
|
23
|
+
const NEGATION_WORDS = 4;
|
|
24
|
+
|
|
25
|
+
const ANCHOR_RE = /^(credits?|drachmae|models?)$/i;
|
|
26
|
+
const NEGATION_RE = /^(not|never|no|nor|isn't|aren't|without|neither)$/i;
|
|
27
|
+
|
|
28
|
+
// One entry per investment idea; `words` is the phrase as lower-case tokens.
|
|
29
|
+
// "return" is the noun only: "returns" alone, or "return on"/"a return" — the
|
|
30
|
+
// verb ("the task returns to ready") is everywhere and says nothing about money.
|
|
31
|
+
const TERMS = [
|
|
32
|
+
{ term: 'investment', match: (w) => /^invest(ment|ments|ing|or|ors|ed|s)?$/.test(w[0]) && 1 },
|
|
33
|
+
{ term: 'return', match: (w, prev) => {
|
|
34
|
+
if (w[0] === 'return' && w[1] === 'on') return 2;
|
|
35
|
+
if (/^returns?$/.test(w[0]) && /^(a|any|financial|guaranteed|expected|your|their|high|higher)$/.test(prev || '')) return 1;
|
|
36
|
+
return 0;
|
|
37
|
+
} },
|
|
38
|
+
{ term: 'appreciation', match: (w) => (w[0] === 'appreciation' ? 1 : (/^appreciat(e|es|ing)$/.test(w[0]) && w[1] === 'in' && w[2] === 'value' ? 3 : 0)) },
|
|
39
|
+
{ term: 'passive income', match: (w) => (w[0] === 'passive' && w[1] === 'income' ? 2 : 0) },
|
|
40
|
+
{ term: 'equity', match: (w) => (w[0] === 'equity' ? 1 : 0) },
|
|
41
|
+
{ term: 'ownership', match: (w) => (w[0] === 'ownership' ? 1 : 0) },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function sentences(text) {
|
|
45
|
+
return String(text == null ? '' : text).split(/(?<=[.!?;])\s+|\n\s*\n|\n(?=\s*[-*|#>])/);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tokens(sentence) {
|
|
49
|
+
return (sentence.toLowerCase().match(/[a-z][a-z']*/g) || []).map((t) => t.replace(/'s$/, ''));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Every offending sentence in `text`: [{ term, phrase, anchor, sentence }]. Pure.
|
|
53
|
+
function findInvestmentWording(text) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const raw of sentences(text)) {
|
|
56
|
+
const sentence = raw.replace(/\s+/g, ' ').trim();
|
|
57
|
+
if (!sentence) continue;
|
|
58
|
+
const w = tokens(sentence);
|
|
59
|
+
const anchors = [];
|
|
60
|
+
w.forEach((t, i) => { if (ANCHOR_RE.test(t)) anchors.push(i); });
|
|
61
|
+
if (!anchors.length) continue;
|
|
62
|
+
for (let i = 0; i < w.length; i++) {
|
|
63
|
+
for (const T of TERMS) {
|
|
64
|
+
const len = T.match(w.slice(i, i + 3), w[i - 1]);
|
|
65
|
+
if (!len) continue;
|
|
66
|
+
const near = anchors.find((a) => Math.abs(a - i) <= NEAR_WORDS);
|
|
67
|
+
if (near === undefined) continue;
|
|
68
|
+
const negated = w.slice(Math.max(0, i - NEGATION_WORDS), i).some((t) => NEGATION_RE.test(t));
|
|
69
|
+
if (negated) continue;
|
|
70
|
+
out.push({ term: T.term, phrase: w.slice(i, i + len).join(' '), anchor: w[near], sentence });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The server publish paths' half: find, write one log line per hit, return the
|
|
78
|
+
// hits. The text is published unchanged whatever this finds, and it never
|
|
79
|
+
// throws — a wording check must not be the reason a summary or an idea is lost.
|
|
80
|
+
function flagInvestmentWording(text, { where = 'text', ref = null, log = console.warn } = {}) {
|
|
81
|
+
try {
|
|
82
|
+
const hits = findInvestmentWording(text);
|
|
83
|
+
for (const h of hits) {
|
|
84
|
+
log(`[credit-wording] flagged ${where}${ref == null ? '' : ` ${ref}`}: "${h.phrase}" near "${h.anchor}" in "${h.sentence.slice(0, 200)}" `
|
|
85
|
+
+ '(ADR 0385 decision 9; published unchanged)');
|
|
86
|
+
}
|
|
87
|
+
return hits;
|
|
88
|
+
} catch (_) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { findInvestmentWording, flagInvestmentWording, NEAR_WORDS, NEGATION_WORDS };
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// (db.applyGrade/confirmTask/upsertSessionRecordWithReward) resolves these
|
|
10
10
|
// through the `reward` kernel PORT rather than importing this module.
|
|
11
11
|
|
|
12
|
-
const { pool, branding, resolveOptional } = require('../../src/module-api');
|
|
12
|
+
const { pool, branding, resolveOptional, logger } = require('../../src/module-api');
|
|
13
13
|
const { priceModelUsage, PRICES } = require('../../src/module-api').llmPricing; // cost-plus session reward (task 1005)
|
|
14
14
|
const achievements = require('./achievements');
|
|
15
15
|
|
|
@@ -143,12 +143,13 @@ const IDEA_GOVERNANCE_PASS_FLOOR = 30; // owner: floor ~30 — see the note on
|
|
|
143
143
|
// Both accrue to builders.total_credits (the contribution meter); ADR 0054 (+ its
|
|
144
144
|
// 2026-06-15 amendment) records the scale and why the cents form existed.
|
|
145
145
|
//
|
|
146
|
-
// REWARD_MARGIN_PCT (and the 1:1
|
|
146
|
+
// REWARD_MARGIN_PCT (and the 1:1 sizing factor, `costScale` — formerly `usdPeg`,
|
|
147
|
+
// task 1004968) are the code-default reward
|
|
147
148
|
// POLICY. They stay a code-reviewed default — not a live UI knob — for the same
|
|
148
149
|
// reasons KIND_MULTIPLIERS does: a policy decision deserves a commit trail and
|
|
149
150
|
// the grader reads it deterministically. But the reward policy is also
|
|
150
151
|
// PER-INSTANCE config (task 2009 / BONGOS-V1): an instance overrides the margin
|
|
151
|
-
// + the
|
|
152
|
+
// + the sizing factor in `config/branding.json` `reward.{marginPct,costScale}`
|
|
152
153
|
// (still a committed, code-reviewed change — just in the instance's config, not
|
|
153
154
|
// the vendored core module). `rewardConfig()` resolves the instance value over
|
|
154
155
|
// these defaults; the pure helper keeps its code defaults so tests stay
|
|
@@ -161,8 +162,13 @@ const IDEA_GOVERNANCE_PASS_FLOOR = 30; // owner: floor ~30 — see the note on
|
|
|
161
162
|
// (ADR 0146) and every existing caller is unchanged.
|
|
162
163
|
const {
|
|
163
164
|
REWARD_MARGIN_PCT, REWARD_MODE_DEFAULT, REWARD_MODE_COST_PLUS_ONLY, IDEA_DESCENT_DECAY, rewardPolicy,
|
|
165
|
+
rewardPolicyDeprecation,
|
|
164
166
|
} = require('./reward-policy');
|
|
165
167
|
|
|
168
|
+
// The usdPeg deprecation note prints ONCE per process, not once per award: this
|
|
169
|
+
// resolver runs on every ship, and a line per ship would bury the logs.
|
|
170
|
+
let usdPegNoteShown = false;
|
|
171
|
+
|
|
166
172
|
// Per-instance reward policy, resolved over the code defaults. Stays HERE, not in
|
|
167
173
|
// reward-policy.js: the branding read must go through the module port (ADR 0083),
|
|
168
174
|
// and a file the CLI can share may not import the port. Best-effort — a branding
|
|
@@ -170,6 +176,11 @@ const {
|
|
|
170
176
|
function rewardConfig() {
|
|
171
177
|
let r = {};
|
|
172
178
|
try { r = (branding().reward) || {}; } catch { r = {}; }
|
|
179
|
+
const note = rewardPolicyDeprecation(r);
|
|
180
|
+
if (note && !usdPegNoteShown) {
|
|
181
|
+
usdPegNoteShown = true;
|
|
182
|
+
logger('economy').warn(note);
|
|
183
|
+
}
|
|
173
184
|
return rewardPolicy(r);
|
|
174
185
|
}
|
|
175
186
|
|
|
@@ -205,22 +216,25 @@ function rewardConfig() {
|
|
|
205
216
|
// absent/any-non-false → the default TRUE (a pre-ADR-0172 config keeps paying).
|
|
206
217
|
|
|
207
218
|
// Pure helper — exported for tests. Returns { trueCostUsd, marginPct, drachmae,
|
|
208
|
-
// description }. drachmae = round(cost × (1+margin) ×
|
|
219
|
+
// description }. drachmae = round(cost × (1+margin) × costScale); at the 1:1 scale
|
|
209
220
|
// one drachma is earned per dollar of cost-plus (task 1138) — sized by USD, not
|
|
210
221
|
// worth USD (ADR 0387), e.g.
|
|
211
222
|
// "$15.00 true cost × 1.20 = 18 drachmae (token reward)"
|
|
212
|
-
// NOTE a sub-$0.42 session (at the 1:1
|
|
223
|
+
// NOTE a sub-$0.42 session (at the 1:1 scale) rounds to 0 drachmae — intended:
|
|
213
224
|
// sub-dollar sessions earn nothing. Kept pure (code-default params); the call
|
|
214
225
|
// site resolves per-instance policy via rewardConfig() and passes it in.
|
|
215
|
-
|
|
226
|
+
// A non-1 factor is written into the description as "× N scale" (it said
|
|
227
|
+
// "× N peg" before task 1004968; only NEW rows carry the new word — no existing
|
|
228
|
+
// credit_log row is rewritten). At the default 1 the description is unchanged.
|
|
229
|
+
function computeSessionTokenReward(trueCostUsd, marginPct = REWARD_MARGIN_PCT, costScale = 1) {
|
|
216
230
|
const cost = Number(trueCostUsd) || 0;
|
|
217
231
|
const pct = Number(marginPct) || 0;
|
|
218
|
-
const
|
|
232
|
+
const scale = Number.isFinite(Number(costScale)) && Number(costScale) > 0 ? Number(costScale) : 1;
|
|
219
233
|
const mult = 1 + pct / 100;
|
|
220
|
-
const drachmae = Math.round(cost * mult *
|
|
221
|
-
const
|
|
234
|
+
const drachmae = Math.round(cost * mult * scale);
|
|
235
|
+
const scaleNote = scale !== 1 ? ` × ${scale} scale` : '';
|
|
222
236
|
const description =
|
|
223
|
-
`$${cost.toFixed(2)} true cost × ${mult.toFixed(2)}${
|
|
237
|
+
`$${cost.toFixed(2)} true cost × ${mult.toFixed(2)}${scaleNote} = ${drachmae} drachmae (token reward)`;
|
|
224
238
|
return { trueCostUsd: cost, marginPct: pct, drachmae, description };
|
|
225
239
|
}
|
|
226
240
|
|
|
@@ -1026,8 +1040,8 @@ async function awardSessionTokenReward(
|
|
|
1026
1040
|
const extraRaw = Number(extraCostUsd);
|
|
1027
1041
|
const extra = Number.isFinite(extraRaw) ? Math.max(0, extraRaw) : 0;
|
|
1028
1042
|
const trueCostUsd = Math.round((pricedCostUsd + extra) * 10000) / 10000;
|
|
1029
|
-
const { marginPct,
|
|
1030
|
-
const { drachmae, description } = computeSessionTokenReward(trueCostUsd, marginPct,
|
|
1043
|
+
const { marginPct, costScale } = rewardConfig();
|
|
1044
|
+
const { drachmae, description } = computeSessionTokenReward(trueCostUsd, marginPct, costScale);
|
|
1031
1045
|
|
|
1032
1046
|
// Nothing priced (no real tokens) — record nothing, report cleanly.
|
|
1033
1047
|
if (!(drachmae > 0)) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// modules/government/docket.js).
|
|
6
6
|
//
|
|
7
7
|
// WHY. Inside the Treasury building three floors are the project's real
|
|
8
|
-
//
|
|
8
|
+
// lines — credits earned for shipped work, the AI grader, and hosting —
|
|
9
9
|
// and its record drawers are those same lines month by month. They come from
|
|
10
10
|
// the two ledgers this module owns (credit_log, cost_log) through ONE bounded
|
|
11
11
|
// read, cost.treasuryMonths. The ledgers are already published unauthenticated
|
|
@@ -28,22 +28,28 @@ function usd(n) {
|
|
|
28
28
|
const v = Math.round(Number(n) || 0);
|
|
29
29
|
return `$${v.toLocaleString('en-US')}`;
|
|
30
30
|
}
|
|
31
|
+
// Credits are a contribution meter, not money (ADR 0385, ADR 0387): the rewards
|
|
32
|
+
// line counts them and never wears a $ sign (task 1005006).
|
|
33
|
+
function credits(n) {
|
|
34
|
+
const v = Math.round(Number(n) || 0);
|
|
35
|
+
return `${v.toLocaleString('en-US')} ${v === 1 ? 'credit' : 'credits'}`;
|
|
36
|
+
}
|
|
31
37
|
function monthKey(now) { const d = new Date(now); return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`; }
|
|
32
38
|
function monthWord(key) { const m = Number(String(key).slice(5, 7)); return MONTHS[m - 1] || String(key); }
|
|
33
39
|
|
|
34
40
|
// One spending line's floor from the month rows. Pure.
|
|
35
|
-
function lineFloor(months, now, field, { what, meta }) {
|
|
41
|
+
function lineFloor(months, now, field, { what, meta, fmt = usd }) {
|
|
36
42
|
const rows = Array.isArray(months) ? months : [];
|
|
37
43
|
const cur = rows.find((r) => r.month === monthKey(now));
|
|
38
44
|
const prev = rows.find((r) => r.month < monthKey(now));
|
|
39
45
|
const v = cur ? Number(cur[field]) || 0 : 0;
|
|
40
46
|
const lines = [[what, meta]];
|
|
41
|
-
if (prev) lines.push([`${
|
|
42
|
-
return { count:
|
|
47
|
+
if (prev) lines.push([`${fmt(prev[field])} in ${monthWord(prev.month)}.`, 'last month']);
|
|
48
|
+
return { count: fmt(v), line: `${fmt(v)} this month`, lines, hot: false };
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
function rewardsFloor(months, now) {
|
|
46
|
-
const f = lineFloor(months, now, 'rewards_credits', { what: '
|
|
52
|
+
const f = lineFloor(months, now, 'rewards_credits', { what: 'Credits earned for shipped work. They count contribution; they are not money.', meta: 'each ship', fmt: credits });
|
|
47
53
|
const cur = (months || []).find((r) => r.month === monthKey(now));
|
|
48
54
|
const n = cur ? cur.rewarded_builders : 0;
|
|
49
55
|
return { ...f, line: `${f.count} this month · ${n} ${n === 1 ? 'builder' : 'builders'}` };
|
|
@@ -56,7 +62,7 @@ function treasuryHistory(months) {
|
|
|
56
62
|
const out = [];
|
|
57
63
|
for (const r of months || []) {
|
|
58
64
|
const at = `${r.month}-01T00:00:00.000Z`;
|
|
59
|
-
if (r.rewards_credits > 0) out.push({ id: `rewards:${r.month}`, title:
|
|
65
|
+
if (r.rewards_credits > 0) out.push({ id: `rewards:${r.month}`, title: `${r.rewarded_builders} ${r.rewarded_builders === 1 ? 'builder' : 'builders'} earned ${credits(r.rewards_credits)}`, at, outcome: 'rewards' });
|
|
60
66
|
if (r.grading_usd > 0) out.push({ id: `grading:${r.month}`, title: `AI grading cost ${usd(r.grading_usd)}`, at, outcome: 'grading' });
|
|
61
67
|
if (r.hosting_usd > 0) out.push({ id: `hosting:${r.month}`, title: `Hosting cost ${usd(r.hosting_usd)}`, at, outcome: 'hosting' });
|
|
62
68
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// (task 1003676).
|
|
3
3
|
//
|
|
4
4
|
// What it is: the reward defaults and the PURE function that resolves a branding
|
|
5
|
-
// pack's `reward` block over them — margin,
|
|
5
|
+
// pack's `reward` block over them — margin, cost scale, mode, whether the per-task
|
|
6
6
|
// credit and the ideator lane are on.
|
|
7
7
|
//
|
|
8
8
|
// Why it is its own file, and why it imports NOTHING: this is the one piece of
|
|
@@ -50,13 +50,56 @@ const REWARD_MODE_COST_PLUS_ONLY = 'cost-plus-only';
|
|
|
50
50
|
// code path to disable separately.
|
|
51
51
|
const IDEA_DESCENT_DECAY = 0.5;
|
|
52
52
|
|
|
53
|
+
// THE SIZING FACTOR IS `costScale` (task 1004968). It multiplies a session's
|
|
54
|
+
// cost-plus figure into credits and is 1 by default. It used to be called
|
|
55
|
+
// `usdPeg`, which read as an exchange rate between a credit and a dollar —
|
|
56
|
+
// exactly what ADR 0387 rules out (a credit has no redemption price). The OLD
|
|
57
|
+
// key is still accepted so no host pack breaks before every hosted pin has moved
|
|
58
|
+
// past this release (ADR 0161); when both are set, `costScale` wins. Removing
|
|
59
|
+
// `usdPeg` support is a later task. The formula itself does not change, and no
|
|
60
|
+
// existing credit_log row is touched — this only renames how a pack spells it.
|
|
61
|
+
// config/branding.neutral.json carries NEITHER key, on purpose: a host pack
|
|
62
|
+
// deep-merges over neutral, so a neutral `costScale: 1` would beat a host's old
|
|
63
|
+
// `usdPeg` and silently reset its awards. The default 1 lives here instead.
|
|
64
|
+
const LEGACY_SCALE_KEY = 'usdPeg';
|
|
65
|
+
const USD_PEG_DEPRECATION =
|
|
66
|
+
'branding reward.usdPeg is deprecated: rename it to reward.costScale (same value, same awards; ADR 0387).';
|
|
67
|
+
|
|
68
|
+
function validScale(v) {
|
|
69
|
+
if (v == null) return NaN;
|
|
70
|
+
const n = Number(v);
|
|
71
|
+
return Number.isFinite(n) && n > 0 ? n : NaN;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// PURE. Which sizing factor a raw block carries: costScale if it is valid, else
|
|
75
|
+
// the legacy usdPeg, else the default 1. An INVALID costScale falls through to
|
|
76
|
+
// usdPeg rather than to 1, so a typo in the new key cannot silently change the
|
|
77
|
+
// awards of a pack that still carries a working old one.
|
|
78
|
+
function resolveCostScale(rawReward) {
|
|
79
|
+
const r = rawReward || {};
|
|
80
|
+
const fromNew = validScale(r.costScale);
|
|
81
|
+
if (Number.isFinite(fromNew)) return fromNew;
|
|
82
|
+
const fromOld = validScale(r[LEGACY_SCALE_KEY]);
|
|
83
|
+
if (Number.isFinite(fromOld)) return fromOld;
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// PURE. The one-line deprecation note for a pack that still spells the factor the
|
|
88
|
+
// old way, or null. Only when costScale is absent: a pack that has moved and kept
|
|
89
|
+
// the old key beside it is not using it, because costScale wins.
|
|
90
|
+
function rewardPolicyDeprecation(rawReward) {
|
|
91
|
+
const r = rawReward || {};
|
|
92
|
+
if (r[LEGACY_SCALE_KEY] != null && r.costScale == null) return USD_PEG_DEPRECATION;
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
53
96
|
// PURE. Resolves a raw branding `reward` block over the code defaults. Every field
|
|
54
97
|
// is validated rather than trusted: a malformed pack degrades to the defaults
|
|
55
98
|
// instead of poisoning an award.
|
|
56
99
|
function rewardPolicy(rawReward) {
|
|
57
100
|
const r = rawReward || {};
|
|
58
101
|
const marginPct = Number.isFinite(Number(r.marginPct)) ? Number(r.marginPct) : REWARD_MARGIN_PCT;
|
|
59
|
-
const
|
|
102
|
+
const costScale = resolveCostScale(r);
|
|
60
103
|
const mode = r.mode === REWARD_MODE_COST_PLUS_ONLY ? REWARD_MODE_COST_PLUS_ONLY : REWARD_MODE_DEFAULT;
|
|
61
104
|
const perTaskCredit = mode !== REWARD_MODE_COST_PLUS_ONLY;
|
|
62
105
|
// Default-on, opt-out-only: only an explicit `false` withholds the ideator lane.
|
|
@@ -77,7 +120,7 @@ function rewardPolicy(rawReward) {
|
|
|
77
120
|
const ideaDescentDecay = Number.isFinite(rawDecay) && rawDecay >= 0 && rawDecay < 1
|
|
78
121
|
? rawDecay
|
|
79
122
|
: IDEA_DESCENT_DECAY;
|
|
80
|
-
return { marginPct,
|
|
123
|
+
return { marginPct, costScale, mode, perTaskCredit, ideatorCredit, artistCredit, ideaDescentDecay };
|
|
81
124
|
}
|
|
82
125
|
|
|
83
126
|
module.exports = {
|
|
@@ -85,5 +128,8 @@ module.exports = {
|
|
|
85
128
|
REWARD_MODE_DEFAULT,
|
|
86
129
|
REWARD_MODE_COST_PLUS_ONLY,
|
|
87
130
|
IDEA_DESCENT_DECAY,
|
|
131
|
+
USD_PEG_DEPRECATION,
|
|
132
|
+
resolveCostScale,
|
|
133
|
+
rewardPolicyDeprecation,
|
|
88
134
|
rewardPolicy,
|
|
89
135
|
};
|
|
@@ -24,6 +24,7 @@ const ideaSizeHistorical = require('./idea-size-historical');
|
|
|
24
24
|
const ideaBrightnessLib = require('./idea-brightness');
|
|
25
25
|
const creditsByCraftLib = require('./credits-by-craft');
|
|
26
26
|
const goalReward = require('./goal-reward');
|
|
27
|
+
const creditWording = require('./credit-wording');
|
|
27
28
|
|
|
28
29
|
module.exports = {
|
|
29
30
|
// credit allocation — transaction-participant: caller passes its tx `client`
|
|
@@ -147,4 +148,8 @@ module.exports = {
|
|
|
147
148
|
wouldExtendStreak: (...a) => streak.wouldExtendStreak(...a),
|
|
148
149
|
// task 1004853: /me's batched form — one streak + one task query per request.
|
|
149
150
|
streakPreviewsForTasks: (...a) => streak.streakPreviewsForTasks(...a),
|
|
151
|
+
// task 1004969 (ADR 0385 decision 9): investment wording next to credits or
|
|
152
|
+
// models. The publish paths flag and log it; they never block or rewrite.
|
|
153
|
+
findInvestmentWording: (...a) => creditWording.findInvestmentWording(...a),
|
|
154
|
+
flagInvestmentWording: (...a) => creditWording.flagInvestmentWording(...a),
|
|
150
155
|
};
|