@bongos/core 1.20.58 → 1.20.60
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 +198 -108
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +10 -0
- package/clients/bongos-client/index.cjs +10 -0
- package/clients/bongos-client/index.d.ts +15 -2
- package/clients/bongos-client/index.mjs +10 -0
- package/docs/adr/0358-a-hall-wears-a-look-and-its-night-takes-a-tint.md +52 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +317 -6
- package/docs/api-reference.md +8 -3
- package/docs/architecture.md +1 -0
- package/docs/copy-inventory.md +339 -330
- package/docs/copy-registry.json +524 -439
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +729 -678
- package/migrations/core_268_builders_main_discipline.sql +35 -0
- package/modules/copy-desk/artist-stats.js +56 -0
- package/modules/copy-desk/module.json +1 -1
- package/modules/copy-desk/page-status.js +39 -0
- package/modules/copy-desk/routes/copy-desk.js +7 -16
- package/modules/copy-desk/tests/copy_no_cms.mjs +10 -1
- package/modules/economy/credits-by-craft.js +208 -0
- package/modules/economy/reward.js +8 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/hall-render.js +2 -2
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +5 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +8 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/ideator-stats.js +149 -0
- package/modules/ideas/module.json +2 -1
- package/modules/ideas/routes/inbox.js +7 -0
- package/modules/lifecycle/activity.js +5 -2
- package/modules/lifecycle/db-analytics.js +4 -2
- package/modules/lifecycle/db-tasks.js +49 -1
- package/modules/lifecycle/db.js +3 -2
- package/modules/lifecycle/page-tweak-reads.js +23 -5
- package/modules/provisioning/look.js +169 -0
- package/modules/provisioning/migrations/provisioning_034_project_logo.sql +35 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/provisioning.js +4 -3
- package/modules/provisioning/routes/look.js +204 -0
- package/modules/provisioning/routes/provisioning.js +5 -59
- package/modules/provisioning/settings-push.js +76 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/projects.html +499 -28
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +6 -3
- package/modules/ui-design/kit/serve.js +50 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/fitness.js +4 -0
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/bongos/db.js +44 -6
- package/src/bongos/role-stats.js +155 -0
- package/src/bongos/routes/builders.js +15 -2
- package/src/bongos/routes/me.js +11 -3
- package/src/bongos/routes.js +4 -1
- package/src/bongos/serve-internal.js +6 -0
- package/src/branding.js +26 -1
- package/src/hall-look.js +99 -0
- package/src/module-api.js +1 -1
- package/src/night-tint.js +26 -0
- package/tests/copy_desk_page_reads.mjs +1 -1
- package/tests/currency_label.mjs +7 -4
- package/tests/fixtures/page-ledger.mjs +1 -1
- package/tests/hall_tokens.mjs +10 -1
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/look_night_tint.mjs +169 -0
- package/tests/main_discipline.mjs +81 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/profile_role_stats.mjs +416 -0
- package/tests/projects_hub.mjs +5 -4
- package/tests/projects_hub_app_status.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +8 -7
- package/tests/projects_hub_look.mjs +255 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/provision_settings_apply.mjs +12 -4
- package/tests/provisioning_look.mjs +376 -0
- package/tests/provisioning_settings.mjs +10 -7
- package/tests/provisioning_settings_apply.mjs +4 -4
- package/tests/provisioning_settings_env.mjs +4 -0
- package/tests/role_stats_db.mjs +237 -0
- package/tests/tweak_replaces_artist_review.mjs +1 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_front_door.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +16 -15
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"steps": [["wait",900],["click","[data-disc-ask]"],["wait",200],["fill","#mDiscName","old-lantern"],["wait",100],["click","[data-disc-yes]"],["wait",900]],
|
|
160
160
|
"expect": {"text":[["#mDisc .addr","This project is being disconnected."],["#mName","old-lantern"]],"hidden":["#mDiscName","[data-disc-ask]"]} },
|
|
161
161
|
{ "name": "the wizard driver (task 1003346): name, type, about, home (fresh; the stub grants repo access, so the new repository is named from the project), address (the handle's verdict), the review as a ledger, then the filing — the done panel with the standup rail and the human steps", "auth": true, "width": 1440, "url": "/projects?view=new",
|
|
162
|
-
"steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]],
|
|
162
|
+
"steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]],
|
|
163
163
|
"expect": {"visible":["#panel-done","#liveProgress .prog","#liveProgress .prog .stages li.now","#humanSteps li"],"hidden":["#panel-review","#panel-name"],"text":[["#done-h","The drums are rolling"]],"attr":[["#panel-done","class","panel on"]]} },
|
|
164
164
|
{ "name": "the front door (task 1004417): Proceed is refused until the terms are ticked, the fork's demo door says the demo is not open yet, and Begin planet creation goes on to the name step with the rail back", "auth": true, "width": 1440, "url": "/projects?view=new",
|
|
165
165
|
"steps": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",200],["click","#panel-fork button[type=submit]"],["wait",400]],
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400]],
|
|
169
169
|
"expect": {"visible":["#panel-fork",".doorPill","#wizDemo","#panel-fork button[type=submit]"],"hidden":["#view-new .steps"]} },
|
|
170
170
|
{ "name": "the wizard driver at 390: the rail folds to two rows of three and the walk still reaches the review", "auth": true, "width": 390, "url": "/projects?view=new",
|
|
171
|
-
"steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]],
|
|
171
|
+
"steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400]],
|
|
172
172
|
"expect": {"visible":["#panel-review","#reviewList li","#doCreate"],"hidden":["#panel-address"],"text":[["#rev-h","Ready to roll"]]} }
|
|
173
173
|
]
|
|
174
174
|
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
|
|
23
23
|
"in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
|
|
24
24
|
"in-manage-card": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mCard", "#mCardPick .pick .pickPreview .cardBand", "#mCardPick [data-card-save]", "#mCardWhat summary"], "count": [["#mCardPick .pickCells[role=\"radiogroup\"]", 1], ["#mCardPick .pickCell", 5], ["#mCardPick .pickCell.on", 1]], "checked": "#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]", "attr": [["#mCardPick .cardBand", "data-own", ""]] } },
|
|
25
|
+
"in-manage-look": {"auth":true,"url":"/projects?view=manage&id=101","actions":[["wait",900],["focus","#mLkLook-grove"],["wait",200]],"expect":{"visible":["#mLook","#mLookPick .lookOpts","#mLkLook-grove","#mLkTint","#mLookSave",".lookPrev .mh.dark","#mLookWhat summary"]}},
|
|
25
26
|
"in-manage-kind": { "auth": true, "url": "/projects?view=manage&id=101", "expect": {"visible":["#mKind","#mKindPick .modBand","#mKindWhat summary"],"count":[["#mKindPick .modOpt",5],["#mKindPick .modOpt[aria-pressed=\"true\"]",0]],"text":[["#mKindPick .modOpt b","Game"],["#mKindPick .finePrint","This project has never said what kind it is. Saying so puts the word beside its name wherever it is listed; it changes nothing else."]]} },
|
|
26
27
|
"in-manage-invite": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mInvite", "#mInvite h2", "#mInviteWho", "#mInvite [data-invite-send]"] } },
|
|
27
28
|
"in-manage-invite-handoff": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["fill","#mInviteWho","octo-builder"],["click","#mInvite [data-invite-send]"],["wait",400]], "expect": {"visible":["#mInviteList a","#mInviteMsg"],"hidden":[]} },
|
|
@@ -49,9 +50,11 @@
|
|
|
49
50
|
"in-new-address": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-address"],"hidden":["#panel-home","#qWrap"]} },
|
|
50
51
|
"in-new-app": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-app","#appOpts","#appOptRender"],"hidden":["#panel-address","#panel-review","#qWrap"]} },
|
|
51
52
|
"in-new-app-render": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#appOptRender input"],["wait",300]], "expect": {"visible":["#panel-app","#renderSetup","#f-renderKey","#renderCheck"],"hidden":["#renderWs","#renderSvc","#renderNew"]} },
|
|
52
|
-
"in-new-
|
|
53
|
-
"in-new-
|
|
54
|
-
"in-new-
|
|
53
|
+
"in-new-look": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]],"expect":{"visible":["#panel-look","#wizLook","#wizLook-chrome-world","#wizTint","#wizLogo",".lookPrev .mh.light",".lookPrev .mh.dark"],"hidden":["#panel-app","#panel-review"]}},
|
|
54
|
+
"in-new-look-custom": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#wizLook-custom"],["wait",200]],"expect":{"visible":["#panel-look","#wizAccent","#wizAccentNote"],"hidden":["#panel-review"]}},
|
|
55
|
+
"in-new-review": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-review","#reviewList li"],"hidden":["#panel-address","#qWrap"]} },
|
|
56
|
+
"in-new-done": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]], "expect": {"visible":["#panel-done","#liveProgress .prog","#humanSteps li"],"hidden":["#panel-review","#qWrap"]} },
|
|
57
|
+
"in-new-resume-offer": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500],["click","#projTrig"],["wait",350],["click","#menuProjects [data-nav=\"new\"]"],["wait",500]], "expect": {"visible":["#panel-terms","#wizResume","#wizResumeGo"],"hidden":["#panel-done","#panel-name"],"text":[["#wizResumeName","Mercury"]]} },
|
|
55
58
|
"out-map-projects": { "auth": false, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "hidden": ["#menuProjects [data-nav=\"mine\"]"] } },
|
|
56
59
|
"card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://example.com\"]"], ["wait", 900]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art"]], "visible": ["#cardArt", "#cardFaces", "#cardBuildersFig", "#cardShippedFig", "#cardOpen", "#cardHall"], "text": [["#cardBuilders", "11"], ["#cardShipped", "0"], ["#cardRel", "you build here"], ["#cardRuns", "example.com"], ["#cardFaces .facesMore", "+6"]], "count": [["#cardFaces img", 5]], "href": [["#cardHall", "https://builders.example.com/"]] } },
|
|
57
60
|
"card-noart": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]"], ["wait", 600]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art", false]], "hidden": ["#cardArt", "#cardFaces", "#cardHall"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardOpen"], "text": [["#cardShipped", "30+"], ["#cardRel", "you build here"]] } },
|
|
@@ -201,6 +201,8 @@ const STUB_VOCAB = {
|
|
|
201
201
|
join_grant: ['view', 'apply', 'full'],
|
|
202
202
|
artist_gate: ['off', 'advisory', 'strict'],
|
|
203
203
|
spend_payer: ['none', 'project', 'builder'],
|
|
204
|
+
look: ['own', 'chrome-world', 'expedition', 'grove', 'blueprint', 'custom'], // the look + its night (task 1004421)
|
|
205
|
+
night_tint: ['on', 'off'],
|
|
204
206
|
planet_template: null, // the template set is long; shape is checked, not membership
|
|
205
207
|
planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
|
|
206
208
|
card_backdrop: ['nebula', 'dust', 'deepfield', 'chrome'],
|
|
@@ -224,6 +226,53 @@ function settingsPatch(res, id, body) {
|
|
|
224
226
|
: { queued: false, restart: false, message: 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.' };
|
|
225
227
|
return json(res, 200, { ok: true, saved: FLOW_RECEIPTS.settings.settings, planet: PLANETS.get(String(id)) || NO_PLANET, push });
|
|
226
228
|
}
|
|
229
|
+
// a project's look (task 1004421, ADR 0358): GET/PATCH …/look and PUT/DELETE …/logo,
|
|
230
|
+
// validated the way routes/look.js validates and REMEMBERED for the run, so the manage
|
|
231
|
+
// page repaints from what it saved. The accent is stored as sent — the real route nudges
|
|
232
|
+
// it, and the page shows its own copy of that nudge before it ever asks.
|
|
233
|
+
const LOOK_STATE = new Map();
|
|
234
|
+
const LOGO_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
|
|
235
|
+
function lookOf(id) {
|
|
236
|
+
return LOOK_STATE.get(String(id)) || { look: 'own', night_tint: 'on', look_accent: null, logo_url: null };
|
|
237
|
+
}
|
|
238
|
+
const LOOK_RECEIPT = { reachable: true, queued: true, restart: true, message: 'Saved. Your project will restart to pick this up — until then it keeps its current setting.' };
|
|
239
|
+
function lookRoute(req, res, rest) {
|
|
240
|
+
const m = rest.match(/^\/provisioning\/instances\/(\d+)\/(look|logo)$/);
|
|
241
|
+
if (!m) return false;
|
|
242
|
+
if (!AUTH) { json(res, 401, UNAUTH); return true; }
|
|
243
|
+
const id = m[1], cur = lookOf(id);
|
|
244
|
+
if (m[2] === 'look' && req.method === 'GET') { json(res, 200, { ok: true, look: cur, options: { look: STUB_VOCAB.look, night_tint: STUB_VOCAB.night_tint }, logo_max_bytes: 262144 }); return true; }
|
|
245
|
+
if (m[2] === 'look' && req.method === 'PATCH') {
|
|
246
|
+
readBody(req, (body) => {
|
|
247
|
+
const bad = Object.keys(body).find((k) => !['look', 'night_tint', 'accent'].includes(k))
|
|
248
|
+
|| (body.look !== undefined && !STUB_VOCAB.look.includes(body.look) && 'look')
|
|
249
|
+
|| (body.night_tint !== undefined && !STUB_VOCAB.night_tint.includes(body.night_tint) && 'night_tint');
|
|
250
|
+
if (bad) return json(res, 400, { error: { code: 'validation_failed', message: `bad field: ${bad}` } });
|
|
251
|
+
if (body.accent !== undefined && !/^#?[0-9a-f]{6}$/i.test(String(body.accent))) return json(res, 400, { error: { code: 'bad_accent', message: 'An accent is a colour written #rrggbb, for example #b48cf2.' } });
|
|
252
|
+
const next = { ...cur, ...(body.look ? { look: body.look } : {}), ...(body.night_tint ? { night_tint: body.night_tint } : {}), ...(body.accent ? { look_accent: String(body.accent).toLowerCase() } : {}) };
|
|
253
|
+
if (next.look === 'custom' && !next.look_accent) return json(res, 400, { error: { code: 'accent_required', message: 'Your own look needs an accent colour — send accent as #rrggbb.' } });
|
|
254
|
+
LOOK_STATE.set(id, next);
|
|
255
|
+
return json(res, 200, { ok: true, look: next, push: LOOK_RECEIPT, adjusted: null });
|
|
256
|
+
});
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
if (m[2] === 'logo' && req.method === 'PUT') {
|
|
260
|
+
readBody(req, (body) => {
|
|
261
|
+
if (!LOGO_TYPES.includes(body.content_type) || typeof body.image_b64 !== 'string' || !body.image_b64) return json(res, 400, { error: { code: 'logo_bad_type', message: 'A logo must be a PNG, JPEG or WebP image.' } });
|
|
262
|
+
const next = { ...cur, logo_url: 'data:' + body.content_type + ';base64,' + body.image_b64 };
|
|
263
|
+
LOOK_STATE.set(id, next);
|
|
264
|
+
return json(res, 200, { ok: true, look: next, push: LOOK_RECEIPT });
|
|
265
|
+
});
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (m[2] === 'logo' && req.method === 'DELETE') {
|
|
269
|
+
const next = { ...cur, logo_url: null };
|
|
270
|
+
LOOK_STATE.set(id, next);
|
|
271
|
+
json(res, 200, { ok: true, look: next, push: LOOK_RECEIPT });
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
227
276
|
// the row carries its planet: a saved pick for the run, else the fixture's own (or null)
|
|
228
277
|
function withPlanet(inst) {
|
|
229
278
|
const saved = PLANETS.get(String(inst.id));
|
|
@@ -433,6 +482,7 @@ const server = http.createServer((req, res) => {
|
|
|
433
482
|
// the manage flows (task 1003345): every door on a row and on the manage page only ASKS
|
|
434
483
|
// (ADR 0111) — the stub answers each ask with the route's receipt so the probes can drive
|
|
435
484
|
// attach / restart / teardown / revive / join end to end; nothing changes in the fixture
|
|
485
|
+
if (lookRoute(req, res, rest)) return;
|
|
436
486
|
if (req.method !== 'GET') {
|
|
437
487
|
const ps = rest.match(/^\/provisioning\/instances\/(\d+)\/settings$/);
|
|
438
488
|
if (ps && req.method === 'PATCH') return AUTH ? readBody(req, (body) => settingsPatch(res, ps[1], body)) : json(res, 401, UNAUTH);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.60",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.60",
|
|
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.20.
|
|
3
|
+
"version": "1.20.60",
|
|
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
|
@@ -8465,5 +8465,17 @@
|
|
|
8465
8465
|
"id": "1004423",
|
|
8466
8466
|
"text": "A project that is being founded now has a genesis home at the top of its hall. The founder settles a plan in one click (each stage's length, the starting government by its real name with a plain explanation, and how many passe"
|
|
8467
8467
|
}
|
|
8468
|
+
],
|
|
8469
|
+
"1.20.59": [
|
|
8470
|
+
{
|
|
8471
|
+
"id": "1004433",
|
|
8472
|
+
"text": "The profile now knows each person by their own work. An artist's page counts pages approved, how many pages of each part of the site they have tweaked, and what pages earned them; an ideator's counts thoughts filed, how many b"
|
|
8473
|
+
}
|
|
8474
|
+
],
|
|
8475
|
+
"1.20.60": [
|
|
8476
|
+
{
|
|
8477
|
+
"id": "1004421",
|
|
8478
|
+
"text": "Every project can now have its own look, so builders who work in several projects always know which hall they are in. Creating a project has a new Look step: four ready-made looks or your own colour, a switch to tint dark mode"
|
|
8479
|
+
}
|
|
8468
8480
|
]
|
|
8469
8481
|
}
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -566,6 +566,10 @@ const KERNEL_FILES = [
|
|
|
566
566
|
// config / identity (host context a module reads, never hardcodes)
|
|
567
567
|
'src/instance-config.js',
|
|
568
568
|
'src/branding.js',
|
|
569
|
+
// task 1004421 (ADR 0358): branding.js lays the chosen look over the pack through this
|
|
570
|
+
// file — the same identity/config category, requiring only instance-config and node
|
|
571
|
+
// built-ins, so it joins the roster rather than reading as a kernel→domain leak.
|
|
572
|
+
'src/hall-look.js',
|
|
569
573
|
'src/build-info.js',
|
|
570
574
|
// task 1003476 (goal 1000072): the project's stale-claim timer — host configuration
|
|
571
575
|
// a module reads rather than hardcodes, exactly the category instance-config.js and
|
|
@@ -155,6 +155,11 @@ const INTEGRATION = new Set([
|
|
|
155
155
|
// predicate still returns 0 so the bug itself stays executable. Same
|
|
156
156
|
// always-rolled-back transaction discipline as the entry above.
|
|
157
157
|
'session_earnings_mirror_db',
|
|
158
|
+
// task 1004433: the credits-by-craft split and the per-person earnings
|
|
159
|
+
// fragment against real SQL. Both are questions about which ledger rows a LIKE
|
|
160
|
+
// pattern and a LATERAL unnest reach, which a stubbed client cannot answer.
|
|
161
|
+
// One transaction, always rolled back.
|
|
162
|
+
'role_stats_db',
|
|
158
163
|
// RENAMED by ADR 0174 (goal 1000068) — these entries said governance_* until task
|
|
159
164
|
// 1003091. A curated set keyed by a stem that no longer exists silently stops
|
|
160
165
|
// covering its file: both of these dropped OUT of the integration set and INTO the
|
package/src/bongos/db.js
CHANGED
|
@@ -149,7 +149,27 @@ async function linkAnthropicAccount(builderId, { email, userId }) {
|
|
|
149
149
|
// updated builder row, or throws { code: 'invalid_discipline', bad } if any
|
|
150
150
|
// element is not offered by this instance.
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
// `mainDiscipline` (task 1004433, core_268) is the builder's own pick of MAIN
|
|
153
|
+
// role, which the canonical-order set cannot carry. Three answers:
|
|
154
|
+
// undefined not mentioned: the stored main role is KEPT while it is still in
|
|
155
|
+
// the new set, and cleared when the builder drops that craft (a main
|
|
156
|
+
// role they no longer practise would lead their profile otherwise);
|
|
157
|
+
// null cleared on purpose;
|
|
158
|
+
// a string set, and it must be one of the crafts in the new set
|
|
159
|
+
// (`invalid_main_discipline`): a main role is one of your roles.
|
|
160
|
+
// The write, spelled once so tests/role_stats_db.mjs runs the very statement
|
|
161
|
+
// against a real Postgres: $3 says whether the caller named a main role at all,
|
|
162
|
+
// $4 is that role (or null to clear it).
|
|
163
|
+
const UPDATE_BUILDER_DISCIPLINES_SQL =
|
|
164
|
+
`UPDATE builders
|
|
165
|
+
SET preferred_disciplines = $2,
|
|
166
|
+
main_discipline = CASE WHEN $3::boolean THEN $4::text
|
|
167
|
+
WHEN main_discipline = ANY($2::text[]) THEN main_discipline
|
|
168
|
+
ELSE NULL END
|
|
169
|
+
WHERE id = $1
|
|
170
|
+
RETURNING id, github_id, github_login, display_name, avatar_url, total_credits, karma, created_at, rank, preferred_disciplines, main_discipline`;
|
|
171
|
+
|
|
172
|
+
async function updateBuilderDisciplines(builderId, disciplines, { mainDiscipline } = {}) {
|
|
153
173
|
const { enabledDisciplines } = require('../modules');
|
|
154
174
|
const offered = enabledDisciplines();
|
|
155
175
|
const list = Array.isArray(disciplines) ? disciplines : [];
|
|
@@ -161,12 +181,28 @@ async function updateBuilderDisciplines(builderId, disciplines) {
|
|
|
161
181
|
throw err;
|
|
162
182
|
}
|
|
163
183
|
const normalized = offered.filter((d) => list.includes(d));
|
|
184
|
+
if (typeof mainDiscipline === 'string' && !normalized.includes(mainDiscipline)) {
|
|
185
|
+
const err = new Error(`main discipline ${mainDiscipline} is not one of the chosen disciplines`);
|
|
186
|
+
err.code = 'invalid_main_discipline';
|
|
187
|
+
err.bad = [mainDiscipline];
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
const setMain = mainDiscipline !== undefined;
|
|
164
191
|
const { rows } = await pool.query(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
192
|
+
UPDATE_BUILDER_DISCIPLINES_SQL,
|
|
193
|
+
[builderId, normalized, setMain, setMain ? mainDiscipline : null]
|
|
194
|
+
);
|
|
195
|
+
return rows[0] ?? null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The two role facts the profile needs beside the public builder row (task
|
|
199
|
+
// 1004433): the preferred set and the chosen main role. Its own read, rather
|
|
200
|
+
// than a column added to getBuilderById, so the kernel's hot identity read is
|
|
201
|
+
// untouched by a profile feature.
|
|
202
|
+
async function getBuilderRoleChoice(builderId) {
|
|
203
|
+
const { rows } = await pool.query(
|
|
204
|
+
`SELECT preferred_disciplines, main_discipline FROM builders WHERE id = $1`,
|
|
205
|
+
[builderId]
|
|
170
206
|
);
|
|
171
207
|
return rows[0] ?? null;
|
|
172
208
|
}
|
|
@@ -1065,6 +1101,8 @@ module.exports = {
|
|
|
1065
1101
|
getBuilderByGithubId,
|
|
1066
1102
|
hasInvitedAccessRequest,
|
|
1067
1103
|
updateBuilderDisciplines,
|
|
1104
|
+
UPDATE_BUILDER_DISCIPLINES_SQL,
|
|
1105
|
+
getBuilderRoleChoice,
|
|
1068
1106
|
RANKS,
|
|
1069
1107
|
setBuilderRank,
|
|
1070
1108
|
getLatestRankChangeForBuilder,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/role-stats.js — EACH ROLE BY ITS OWN WORK: the reads behind the
|
|
4
|
+
// role-shaped profile (task 1004433 / WA6.RP03, goal 1000095, criterion
|
|
5
|
+
// wa6-roles-read-true-in-public). The owner-approved mock is
|
|
6
|
+
// docs/design/mocks/profile-roles/ (task 1004431); RP04 to RP07 draw from this.
|
|
7
|
+
//
|
|
8
|
+
// WHAT IT ANSWERS. For one builder: which of the three crafts they are shown in,
|
|
9
|
+
// which one leads, and each craft's own headline numbers.
|
|
10
|
+
//
|
|
11
|
+
// engineer `shipped`, works shipped (unchanged from today), and engineering credits
|
|
12
|
+
// artist pages approved, "N of M tweaked" per surface, credits from pages,
|
|
13
|
+
// this week (copy-desk's studio tally, the `copy-desk.artistStats`
|
|
14
|
+
// port)
|
|
15
|
+
// ideator thoughts filed, ignited, Full Ideas, ratified by the board, total
|
|
16
|
+
// size across every idea stream with the historical-estimate
|
|
17
|
+
// marker, and the share still alive (the sky's own reads, the
|
|
18
|
+
// `ideas.ideatorStats` port)
|
|
19
|
+
//
|
|
20
|
+
// Plus `credits_by_craft`, the ledger split of total_credits (economy's
|
|
21
|
+
// `reward.creditsByCraft`), which is what makes an artist or an ideator who has
|
|
22
|
+
// never shipped a task read as what they are.
|
|
23
|
+
//
|
|
24
|
+
// THE OWNER'S RULES (2026-09-30), and where each lives:
|
|
25
|
+
// • one person, one profile; the builder CHOOSES their main role
|
|
26
|
+
// (builders.main_discipline, core_268). Until they do, the main role is the
|
|
27
|
+
// craft they have earned most in, then their first preferred craft, then
|
|
28
|
+
// engineering, and `main_basis` says which of those it is, so a surface can
|
|
29
|
+
// tell a choice from a guess.
|
|
30
|
+
// • any OTHER role shows only once it has earned credits. `shown` is the main
|
|
31
|
+
// role first, then each craft with credits, in preferred order. A craft not
|
|
32
|
+
// in `shown` carries no stats block (null): its tab and chip do not exist.
|
|
33
|
+
// • decision (a): the identity band, the tally and "By the numbers" stay
|
|
34
|
+
// public, so everything here is counts and sums. No idea title, no page
|
|
35
|
+
// wording, no task list leaves this read; those stay behind a sign-in on the
|
|
36
|
+
// Activity routes. The answer is the same for every viewer.
|
|
37
|
+
//
|
|
38
|
+
// A port that is absent (a vanilla instance without the module) gives that
|
|
39
|
+
// block `null` fields rather than zeros: "could not ask" is not "nothing there".
|
|
40
|
+
|
|
41
|
+
const seams = require('../module-seams');
|
|
42
|
+
|
|
43
|
+
const CRAFTS = Object.freeze(['engineer', 'artist', 'ideator']);
|
|
44
|
+
// A discipline as a craft, asked of economy (reward.craftForPreference), the
|
|
45
|
+
// module that decides which craft a credit belongs to. One table, so a builder's
|
|
46
|
+
// chips and their credits cannot disagree (`ui`, the design discipline, is the
|
|
47
|
+
// artist's craft there). With no economy module there are no credits to split
|
|
48
|
+
// either, and only the three craft names read as crafts.
|
|
49
|
+
function craftOf(discipline) {
|
|
50
|
+
const reward = seams.resolveOptional('reward');
|
|
51
|
+
if (reward && typeof reward.craftForPreference === 'function') return reward.craftForPreference(discipline);
|
|
52
|
+
return CRAFTS.includes(discipline) ? discipline : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function preferredCrafts(preferred) {
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const d of Array.isArray(preferred) ? preferred : []) {
|
|
58
|
+
const c = craftOf(d);
|
|
59
|
+
if (c && !out.includes(c)) out.push(c);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// resolveMainRole — pure. The chosen craft, else the earned one, else the
|
|
65
|
+
// preferred one, else engineering (the platform's default craft).
|
|
66
|
+
function resolveMainRole({ mainDiscipline = null, preferred = [], byCraft = null } = {}) {
|
|
67
|
+
const chosen = craftOf(mainDiscipline);
|
|
68
|
+
if (chosen) return { main: chosen, basis: 'chosen' };
|
|
69
|
+
if (byCraft) {
|
|
70
|
+
let best = null;
|
|
71
|
+
for (const c of CRAFTS) {
|
|
72
|
+
const n = Number(byCraft[c]) || 0;
|
|
73
|
+
if (n > 0 && (!best || n > best.n)) best = { c, n };
|
|
74
|
+
}
|
|
75
|
+
if (best) return { main: best.c, basis: 'earned' };
|
|
76
|
+
}
|
|
77
|
+
const pref = preferredCrafts(preferred);
|
|
78
|
+
if (pref.length) return { main: pref[0], basis: 'preferred' };
|
|
79
|
+
return { main: 'engineer', basis: 'default' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// shownCrafts — pure. Main first, then every craft with credits, in the
|
|
83
|
+
// builder's preferred order and then CRAFTS order.
|
|
84
|
+
function shownCrafts({ main, preferred = [], byCraft = null } = {}) {
|
|
85
|
+
const order = [...preferredCrafts(preferred), ...CRAFTS].filter((c, i, a) => a.indexOf(c) === i);
|
|
86
|
+
const out = [main];
|
|
87
|
+
for (const c of order) {
|
|
88
|
+
if (c === main) continue;
|
|
89
|
+
if (byCraft && Number(byCraft[c]) > 0) out.push(c);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// composeRoleStats — pure: the facts in, the profile's `roles` block out.
|
|
95
|
+
function composeRoleStats({ choice = null, byCraft = null, shippedCount = 0, artist = null, ideator = null } = {}) {
|
|
96
|
+
const preferred = (choice && choice.preferred_disciplines) || [];
|
|
97
|
+
const { main, basis } = resolveMainRole({ mainDiscipline: choice && choice.main_discipline, preferred, byCraft });
|
|
98
|
+
const shown = shownCrafts({ main, preferred, byCraft });
|
|
99
|
+
const credits = byCraft
|
|
100
|
+
? { engineer: byCraft.engineer, artist: byCraft.artist, ideator: byCraft.ideator, other: byCraft.other, total: byCraft.total }
|
|
101
|
+
: null;
|
|
102
|
+
return {
|
|
103
|
+
main,
|
|
104
|
+
main_basis: basis,
|
|
105
|
+
shown,
|
|
106
|
+
preferred: preferredCrafts(preferred),
|
|
107
|
+
credits_by_craft: credits,
|
|
108
|
+
engineer: shown.includes('engineer')
|
|
109
|
+
? { shipped: Number(shippedCount) || 0, credits: credits ? credits.engineer : null }
|
|
110
|
+
: null,
|
|
111
|
+
artist: shown.includes('artist') ? (artist || emptyArtist()) : null,
|
|
112
|
+
ideator: shown.includes('ideator') ? (ideator || emptyIdeator()) : null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The shapes a shown craft takes when its port could not be asked: every number
|
|
117
|
+
// null, so a renderer says "unknown", never "0".
|
|
118
|
+
function emptyArtist() {
|
|
119
|
+
return { pages_approved: null, pages: null, surfaces_touched: null, credits: null, this_week: null, surfaces: [], unavailable: true };
|
|
120
|
+
}
|
|
121
|
+
function emptyIdeator() {
|
|
122
|
+
return {
|
|
123
|
+
filed: null, ignited: null, full_ideas: null, ratified: null,
|
|
124
|
+
size: { total: null, paid: null, estimated: null, historical: null },
|
|
125
|
+
alive: { share: null, has_work: null, attributable: null },
|
|
126
|
+
unavailable: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// readRoleStats — the I/O half. The crafts are decided FIRST, from the ledger,
|
|
131
|
+
// so the heavier per-craft reads run only for a craft that will be shown.
|
|
132
|
+
async function readRoleStats(builderId, { shippedCount = 0, getRoleChoice } = {}) {
|
|
133
|
+
const reward = seams.resolveOptional('reward');
|
|
134
|
+
const [choice, byCraftMap] = await Promise.all([
|
|
135
|
+
getRoleChoice ? getRoleChoice(builderId) : null,
|
|
136
|
+
reward && typeof reward.creditsByCraft === 'function' ? reward.creditsByCraft([builderId]) : null,
|
|
137
|
+
]);
|
|
138
|
+
const byCraft = byCraftMap ? (byCraftMap.get(String(builderId)) || null) : null;
|
|
139
|
+
const preferred = (choice && choice.preferred_disciplines) || [];
|
|
140
|
+
const { main } = resolveMainRole({ mainDiscipline: choice && choice.main_discipline, preferred, byCraft });
|
|
141
|
+
const shown = shownCrafts({ main, preferred, byCraft });
|
|
142
|
+
|
|
143
|
+
const artistPort = shown.includes('artist') ? seams.resolveOptional('copy-desk.artistStats') : null;
|
|
144
|
+
const ideatorPort = shown.includes('ideator') ? seams.resolveOptional('ideas.ideatorStats') : null;
|
|
145
|
+
const [artist, ideator] = await Promise.all([
|
|
146
|
+
typeof artistPort === 'function' ? artistPort(builderId) : null,
|
|
147
|
+
typeof ideatorPort === 'function' ? ideatorPort(builderId) : null,
|
|
148
|
+
]);
|
|
149
|
+
return composeRoleStats({ choice, byCraft, shippedCount, artist, ideator });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Only the reader is exported: the pure halves above are reached through it
|
|
153
|
+
// (tests/profile_role_stats.mjs drives them via the real route), and an export
|
|
154
|
+
// nothing imports is dead surface to the knip ratchet.
|
|
155
|
+
module.exports = { readRoleStats };
|
|
@@ -33,6 +33,7 @@ const { validateOrRespond, LIMITS, parseId } = require('./_helpers');
|
|
|
33
33
|
// for the roster-visible checklist (read/shape) — never a direct require.
|
|
34
34
|
const { pool } = require('../pool');
|
|
35
35
|
const { publicReadRateLimitMiddleware } = require('../middleware/rate-limit');
|
|
36
|
+
const { readRoleStats } = require('../role-stats');
|
|
36
37
|
const seams = require('../../module-seams');
|
|
37
38
|
const { branding } = require('../../branding');
|
|
38
39
|
// The project's stale-claim timer (task 1003476, goal 1000072) — kernel config, so a
|
|
@@ -503,6 +504,11 @@ module.exports = function buildBuildersRouter() {
|
|
|
503
504
|
// monthly_budget_usd / system_role) are NEVER spread into the
|
|
504
505
|
// JSON. A leak-prevention unit test (tests/profile_route.mjs) asserts their
|
|
505
506
|
// absence + the 404-on-unknown-id behaviour.
|
|
507
|
+
// `roles` (task 1004433) is the one block that is NOT a re-presentation of the
|
|
508
|
+
// leaderboard: per-craft counts and sums. The owner made them public on
|
|
509
|
+
// 2026-09-30 (RP01 decision (a): the tally and By the numbers stay public, the
|
|
510
|
+
// trail, the earnings and every Activity tab need a sign-in), so the block
|
|
511
|
+
// carries counts only — no idea title, no page wording, no task list.
|
|
506
512
|
// THROTTLED since task 1002804. The global limiter meters WRITES only
|
|
507
513
|
// (rateLimitMiddleware returns early for GET) and the public read limiter is
|
|
508
514
|
// mounted under /public/*, which this is not — so this route answered at line
|
|
@@ -535,12 +541,17 @@ module.exports = function buildBuildersRouter() {
|
|
|
535
541
|
// the same indistinguishable answer cross_project already gives.
|
|
536
542
|
// Optional-chained so an older hub provider without the function degrades
|
|
537
543
|
// to null rather than throwing across a version window.
|
|
538
|
-
const
|
|
539
|
-
|
|
544
|
+
const shippedCount = await lc().countShippedTasksForBuilder(targetId);
|
|
545
|
+
// roles: each craft by its own work (task 1004433 / WA6.RP03) — which
|
|
546
|
+
// crafts show, which leads, and each one's headline numbers. Counts and
|
|
547
|
+
// sums only, the same for every viewer: the owner's decision (a) keeps the
|
|
548
|
+
// tally and By the numbers public and everything under them signed-in.
|
|
549
|
+
const [recentShips, achievements, crossProject, handle, roles] = await Promise.all([
|
|
540
550
|
lc().recentShippedTasksByBuilder(targetId, 5),
|
|
541
551
|
reward ? reward.getBuilderAchievements(targetId) : [],
|
|
542
552
|
hub ? hub.getPublicProfileExtras(b.github_id) : null,
|
|
543
553
|
hub && hub.getPublicHandle ? hub.getPublicHandle(b.github_id) : null,
|
|
554
|
+
readRoleStats(targetId, { shippedCount, getRoleChoice: db.getBuilderRoleChoice }),
|
|
544
555
|
]);
|
|
545
556
|
// WHITELIST — only the leaderboard-equivalent public field set. Never spread
|
|
546
557
|
// the raw row: it carries anthropic_email / anthropic_user_id /
|
|
@@ -573,6 +584,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
573
584
|
shipped_count: shippedCount,
|
|
574
585
|
recent_ships: recentShips,
|
|
575
586
|
achievements,
|
|
587
|
+
roles,
|
|
576
588
|
cross_project: crossProject,
|
|
577
589
|
handle: handle || null,
|
|
578
590
|
});
|
|
@@ -582,6 +594,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
582
594
|
shipped_count: shippedCount,
|
|
583
595
|
recent_ships: recentShips,
|
|
584
596
|
achievements,
|
|
597
|
+
roles,
|
|
585
598
|
});
|
|
586
599
|
} catch (err) {
|
|
587
600
|
log.error('[gds] GET /builders/:id/profile', err);
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -554,16 +554,24 @@ module.exports = function buildMeRouter() {
|
|
|
554
554
|
// profile. 422 invalid_discipline if any element is out of range.
|
|
555
555
|
// rank: any-builder — self-service profile edit; documented "no rank gate" in the
|
|
556
556
|
// handler comment above (a builder owns their own preference).
|
|
557
|
+
// `main_discipline` (optional, task 1004433): the builder's own MAIN role, one
|
|
558
|
+
// of the crafts in the same body; null clears it, absent keeps it (see
|
|
559
|
+
// db.updateBuilderDisciplines). 422 invalid_main_discipline otherwise.
|
|
557
560
|
router.patch('/me/disciplines', auth.requireBuilder, async (req, res) => {
|
|
558
561
|
if (validateOrRespond(req, res, {
|
|
559
562
|
preferred_disciplines: { required: true, type: 'array', maxItems: 16, itemsType: 'string' },
|
|
563
|
+
main_discipline: { type: 'string', maxLength: 32 },
|
|
560
564
|
})) return;
|
|
561
565
|
const input = req.body?.preferred_disciplines;
|
|
562
566
|
if (!Array.isArray(input)) {
|
|
563
567
|
return res.fail('preferred_disciplines must be an array', 400);
|
|
564
568
|
}
|
|
569
|
+
const body = req.body || {};
|
|
570
|
+
const mainOpt = Object.prototype.hasOwnProperty.call(body, 'main_discipline')
|
|
571
|
+
? { mainDiscipline: body.main_discipline === null ? null : String(body.main_discipline) }
|
|
572
|
+
: {};
|
|
565
573
|
try {
|
|
566
|
-
const builder = await db.updateBuilderDisciplines(req.builder.id, input);
|
|
574
|
+
const builder = await db.updateBuilderDisciplines(req.builder.id, input, mainOpt);
|
|
567
575
|
// The onboarding state machine carved into modules/onboarding/ (BV1.R81 /
|
|
568
576
|
// ADR 0093 §1): resolve the `onboarding` kernel port for the mark + echo —
|
|
569
577
|
// never a direct require of a module. Default-on (present in prod); degrades
|
|
@@ -587,8 +595,8 @@ module.exports = function buildMeRouter() {
|
|
|
587
595
|
const onboarding = onboardingPort ? onboardingPort.shapeForApi(state, { shipCount, discordLinked: !!discordLink, cliAuthed: !!cliAuthed }) : null;
|
|
588
596
|
res.json({ builder, onboarding });
|
|
589
597
|
} catch (err) {
|
|
590
|
-
if (err.code === 'invalid_discipline') {
|
|
591
|
-
return res.fail(
|
|
598
|
+
if (err.code === 'invalid_discipline' || err.code === 'invalid_main_discipline') {
|
|
599
|
+
return res.fail(err.code, 422, { bad: err.bad });
|
|
592
600
|
}
|
|
593
601
|
throw err;
|
|
594
602
|
}
|
package/src/bongos/routes.js
CHANGED
|
@@ -111,7 +111,10 @@ function buildGdsRouter() {
|
|
|
111
111
|
// 'completed'. The list is NOT trimmable to fit: modules/grading/grader-score.js runs
|
|
112
112
|
// the server-side permission-path pre-pass against it (SR-9 / SEC #863), so a short
|
|
113
113
|
// list would quietly weaken a security check rather than merely lose detail.
|
|
114
|
-
/^\/tasks\/\d+\/grade$/.test(req.path)
|
|
114
|
+
/^\/tasks\/\d+\/grade$/.test(req.path) ||
|
|
115
|
+
// task 1004421: a project's logo — up to 256 KB decoded, ~342 KB as base64.
|
|
116
|
+
// modules/provisioning/routes/look.js mounts its own 400 KB parser.
|
|
117
|
+
/^\/provisioning\/instances\/\d+\/logo$/.test(req.path)
|
|
115
118
|
) return next();
|
|
116
119
|
return globalJson(req, res, next);
|
|
117
120
|
});
|
|
@@ -43,6 +43,7 @@ const { buildStampedHtml } = require('../asset-version');
|
|
|
43
43
|
const { buildInfo } = require('../build-info');
|
|
44
44
|
const { buildBongosDownloads } = require('../bongos-downloads');
|
|
45
45
|
const { clientBranding, branding, applyBrandConditionals } = require('../branding');
|
|
46
|
+
const { nightTintCss } = require('../night-tint');
|
|
46
47
|
const { prelaunchGate } = require('./prelaunch-gate');
|
|
47
48
|
const { platformVisibilityGate } = require('./platform-visibility-gate');
|
|
48
49
|
const { schemaPending } = require('./schema-lag');
|
|
@@ -121,6 +122,11 @@ function themeMarkup(theme) {
|
|
|
121
122
|
if (decls.length) css.push(`:root{${decls.join('')}}`);
|
|
122
123
|
const backdrop = cssSafe(theme.backdrop);
|
|
123
124
|
if (backdrop) css.push(`body{background:${backdrop};}`);
|
|
125
|
+
// The night tint (ADR 0358): a hall wearing a look takes a hint of its accent into
|
|
126
|
+
// its dark grounds. Written as the sheet's own two dark twins, after the sheet, so
|
|
127
|
+
// equal specificity wins on order. clientBranding only sets it with a look, so a
|
|
128
|
+
// hall that chose none keeps the sheet's pure black.
|
|
129
|
+
if (theme.nightTint === true) css.push(nightTintCss());
|
|
124
130
|
if (css.length) out += `<style>${css.join('')}</style>`;
|
|
125
131
|
// esc() is lossless in an attribute: the HTML parser decodes the entities back
|
|
126
132
|
// before anything reads the URL, so the neutral pack's inline `data:image/svg+xml,
|