@bongos/core 1.19.871 → 1.19.873
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 +167 -97
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0180-dependency-intake-and-drift-policy.md +6 -0
- package/docs/adr/0307-autobongos-a-deterministic-supervisor-and-agentic-workers.md +10 -0
- package/docs/api/openapi.json +61 -3
- package/docs/api-reference.md +7 -1
- package/docs/architecture.md +7 -7
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +190 -189
- package/docs/copy-registry.json +264 -255
- package/docs/design/hall-direction-v2.md +2 -2
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/README.md +4 -4
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/onboarding/primer.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/bongos-cli-release.md +4 -4
- package/docs/recipes/core-release-pipeline.md +3 -3
- package/docs/recipes/instance-deploy.md +2 -2
- package/docs/recipes/ops-gotchas.md +2 -2
- package/docs/recipes/private-npm-distribution.md +16 -21
- package/docs/recipes/search-index-ingestion.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +218 -0
- package/modules/dev-box/app/README.md +1 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/hall-ui/public/blockers.js +138 -34
- package/modules/hall-ui/public/builders.js +2 -1
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/collab.js +6 -3
- package/modules/hall-ui/public/dom-utils.js +46 -5
- package/modules/hall-ui/public/goals-page.js +7 -3
- package/modules/hall-ui/public/goals-render.js +4 -3
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/people.js +3 -3
- package/modules/hall-ui/public/profile-charts.js +5 -2
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/profile.js +6 -6
- package/modules/hall-ui/public/settings-specialities.js +2 -2
- package/modules/hall-ui/public/settings.html +4 -4
- package/modules/hall-ui/public/settings.js +2 -2
- package/modules/hall-ui/public/shell.js +70 -0
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/work.js +12 -14
- package/modules/provisioning/module.json +3 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/autobongos-loop.js +170 -0
- package/scripts/gds/autobongos-run.js +317 -0
- package/scripts/gds/autobongos-verify.js +377 -0
- package/scripts/gds/autonomy-gauge-statusline.js +124 -0
- package/scripts/gds/autonomy-gauge.js +84 -0
- package/scripts/gds/provision-repo.js +4 -2
- package/scripts/gds/run-unit-tests.js +120 -2
- package/scripts/gds/sequence.js +3 -0
- package/scripts/gds/start.js +16 -10
- package/src/bongos/core-update.js +147 -0
- package/src/bongos/routes/core-update.js +49 -0
- package/src/bongos/routes.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +3 -1
- package/tests/autobongos_loop.mjs +446 -0
- package/tests/autobongos_verify.mjs +325 -0
- package/tests/autonomy_gauge.mjs +344 -0
- package/tests/blockers_hall.mjs +20 -6
- package/tests/blockers_kit_pager.mjs +260 -0
- package/tests/blockers_page_ui.mjs +5 -4
- package/tests/box_ship_permissions.mjs +30 -0
- package/tests/core_update_banner.mjs +308 -0
- package/tests/eviction_policy.mjs +22 -1
- package/tests/git_guard.mjs +68 -0
- package/tests/goal_criterion_confirm_ui.mjs +7 -0
- package/tests/goal_invite_consent.mjs +7 -0
- package/tests/goal_sort_staleness.mjs +6 -2
- package/tests/hall_board_world.mjs +4 -1
- package/tests/hall_builders_page_pass.mjs +7 -0
- package/tests/hall_landing_boot.mjs +7 -0
- package/tests/hall_profile_world.mjs +20 -2
- package/tests/hall_record_world.mjs +7 -0
- package/tests/hall_roster_world.mjs +2 -1
- package/tests/hall_studio_world.mjs +7 -0
- package/tests/hook_shell_tools.mjs +23 -1
- package/tests/idea_detail_page.mjs +7 -0
- package/tests/idea_spark_hall.mjs +7 -0
- package/tests/integration_parked_ratchet.mjs +79 -0
- package/tests/module_contributions.mjs +134 -8
- package/tests/package_core.mjs +31 -0
- package/tests/people_directory_clip.mjs +7 -0
- package/tests/profile_activity.mjs +7 -0
- package/tests/profile_ui_cross_project.mjs +9 -1
- package/tests/provision_disconnect.mjs +7 -0
- package/tests/provision_restart.mjs +7 -0
- package/tests/provision_settings_apply.mjs +7 -0
- package/tests/sessions_kit_pager.mjs +7 -0
- package/tests/watch_applications_queue.mjs +7 -0
- package/tests/watch_roster_self_row.mjs +7 -0
- package/tests/watch_sealed_floor.mjs +7 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// blockers.js — the Blockers page: the full open-blocker queue,
|
|
2
|
-
// 1003456, goal 1000089)
|
|
1
|
+
// blockers.js — the Blockers page: the full open-blocker queue, the write half
|
|
2
|
+
// (task 1003456, goal 1000089), and server-offset paging (task 1003983).
|
|
3
3
|
//
|
|
4
4
|
// GET /blockers (modules/ideas/routes/blockers.js) is PUBLIC and had ZERO hall
|
|
5
5
|
// consumers before task 1003455 — the only blocker a builder could ever see was
|
|
@@ -30,6 +30,21 @@
|
|
|
30
30
|
// public, the roster/drachmae precedent: a page must not be gated tighter than
|
|
31
31
|
// the API it fronts. A signed-out visitor sees the queue and the File button;
|
|
32
32
|
// clicking it redirects to sign-in, same as every other hall write.
|
|
33
|
+
//
|
|
34
|
+
// SERVER-OFFSET PAGING (task 1003983): this used to fetch the whole queue at
|
|
35
|
+
// the max page size once and slice it client-side, which silently truncated
|
|
36
|
+
// past 200 open blockers and undercounted the headline pill from that same
|
|
37
|
+
// truncated array. It now asks the server for one page at a time via the
|
|
38
|
+
// kit's serverPager primitives (hall-ui/CLAUDE.md, the profile activity-log /
|
|
39
|
+
// session-ledger precedent), hand-wired rather than `makeLedger` because
|
|
40
|
+
// these rows are `.ov-row` `<li>`s, not a `<table>` — the same reason
|
|
41
|
+
// sessions.js wires them by hand, and this file keeps its two behaviours:
|
|
42
|
+
// every control dead while a fetch is in flight, and a failed fetch rolls
|
|
43
|
+
// the pager back to the last page that actually painted. `reload()` (fired
|
|
44
|
+
// after a file/resolve/link write, and by the overlay's
|
|
45
|
+
// `otb:blocker-resolved` event) re-fetches the CURRENT server page rather
|
|
46
|
+
// than the whole queue — the total and which rows belong on this page can
|
|
47
|
+
// both have moved.
|
|
33
48
|
(function () {
|
|
34
49
|
'use strict';
|
|
35
50
|
var kit = window.OTBKit;
|
|
@@ -83,7 +98,6 @@
|
|
|
83
98
|
};
|
|
84
99
|
var blockerHref = function (id) { return '#/blocker/' + encodeURIComponent(id); };
|
|
85
100
|
|
|
86
|
-
var all = [];
|
|
87
101
|
// Which rank a reviewer action (Resolve / Link) needs — mirrors the server's
|
|
88
102
|
// blocker.review floor (metic+archon), cosmetically only; the server holds
|
|
89
103
|
// the actual gate regardless of what this variable says.
|
|
@@ -148,47 +162,136 @@
|
|
|
148
162
|
}
|
|
149
163
|
|
|
150
164
|
var pager = kit.makePager('blockers', 'hall-blockers-per-page', { defaultPerPage: 25 });
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
165
|
+
// `total`/`shown` are the server's own count of the whole open set and how
|
|
166
|
+
// many rows the last fetch actually returned — the headline pill and the
|
|
167
|
+
// pager bar both read off these, never off a held array's length.
|
|
168
|
+
// `page`/`perPage` are the last values that actually PAINTED: the kit moves
|
|
169
|
+
// the pager first and calls back second, so a failed fetch has to put it
|
|
170
|
+
// back to these, the sessions.js contract — otherwise the bar goes on
|
|
171
|
+
// claiming a page whose rows never arrived.
|
|
172
|
+
var state = { total: 0, shown: 0, page: pager.page, perPage: pager.perPage };
|
|
173
|
+
// Nothing has painted yet on the very first load, so a failure there has no
|
|
174
|
+
// prior page to roll back to or keep on screen — that one case still falls
|
|
175
|
+
// through to notice()'s full-body replacement.
|
|
176
|
+
var painted = false;
|
|
177
|
+
var loading = false;
|
|
178
|
+
// Two page clicks (or a slow first load racing a fast second one) can be in
|
|
179
|
+
// flight at once; only the newest may paint (the profile activity-log idiom).
|
|
180
|
+
var inFlight = 0;
|
|
181
|
+
// The rows currently on screen — kept ONLY so a purely local state change
|
|
182
|
+
// (a row's action form opening/closing, the viewer's rank arriving) can
|
|
183
|
+
// repaint without asking the server again. Never sliced or paged from here.
|
|
184
|
+
var lastRows = [];
|
|
159
185
|
|
|
160
|
-
function
|
|
161
|
-
|
|
186
|
+
function renderPager() {
|
|
187
|
+
var clamped = kit.renderServerPage(pager, state.total, state.shown);
|
|
188
|
+
// The per-page control has no bounds of its own, so it tracks `loading`
|
|
189
|
+
// directly both ways. Prev/Next re-enabling is left to the kit, which
|
|
190
|
+
// knows whether there is a page on either side — only the DISABLING past
|
|
191
|
+
// that is ours.
|
|
192
|
+
var perPage = document.getElementById(pager.prefix + '-per-page');
|
|
193
|
+
if (perPage) perPage.disabled = loading;
|
|
194
|
+
if (loading) {
|
|
195
|
+
var prev = document.getElementById(pager.prefix + '-prev');
|
|
196
|
+
var next = document.getElementById(pager.prefix + '-next');
|
|
197
|
+
if (prev) prev.disabled = true;
|
|
198
|
+
if (next) next.disabled = true;
|
|
199
|
+
}
|
|
200
|
+
return clamped;
|
|
162
201
|
}
|
|
163
202
|
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
if (countPill) { countPill.textContent = String(all.length); countPill.hidden = all.length === 0; }
|
|
203
|
+
function paintHeadline() {
|
|
204
|
+
if (countPill) { countPill.textContent = String(state.total); countPill.hidden = state.total === 0; }
|
|
167
205
|
if (sub) {
|
|
168
|
-
sub.textContent =
|
|
169
|
-
? (
|
|
206
|
+
sub.textContent = state.total
|
|
207
|
+
? (state.total === 1 ? 'One thing' : state.total + ' things') + ' stuck on the owner — a credential, a '
|
|
170
208
|
+ 'purchase, a product call only they can make. Resolving one un-sticks every task waiting on it automatically.'
|
|
171
209
|
: 'Nothing is stuck on the owner right now.';
|
|
172
210
|
}
|
|
173
|
-
body.innerHTML = '<ul class="ov-rows" id="blockers-list"></ul>' + kit.pagerBarHtml(pager.prefix, { label: 'blockers' });
|
|
174
|
-
listEl = document.getElementById('blockers-list');
|
|
175
|
-
kit.wirePager(pager, rerender);
|
|
176
|
-
rerender();
|
|
177
211
|
}
|
|
178
212
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
213
|
+
function renderList(rows) {
|
|
214
|
+
var listEl = document.getElementById('blockers-list');
|
|
215
|
+
if (!listEl) return;
|
|
216
|
+
listEl.innerHTML = rows.length
|
|
217
|
+
? rows.map(rowHtml).join('')
|
|
218
|
+
: kit.emptyStateHtml('Nothing is stuck on the owner right now.');
|
|
183
219
|
}
|
|
184
220
|
|
|
185
|
-
|
|
186
|
-
|
|
221
|
+
// Repaint the rows already on screen — no server call. Used when only local
|
|
222
|
+
// UI state changed (a row's Resolve/Link form opened or closed, or the
|
|
223
|
+
// viewer's rank just arrived and rowHtml() needs to reconsider isReviewer()).
|
|
224
|
+
function rerender() { renderList(lastRows); }
|
|
225
|
+
|
|
226
|
+
// Fetch the page the pager currently points at. `reclamped` guards against a
|
|
227
|
+
// shrinking queue looping forever — one re-ask, never a spin.
|
|
228
|
+
function fetchPage(reclamped) {
|
|
229
|
+
var seq = ++inFlight;
|
|
230
|
+
loading = true;
|
|
231
|
+
renderPager();
|
|
232
|
+
var url = API + '/blockers?limit=' + encodeURIComponent(String(Number(pager.perPage) || 0))
|
|
233
|
+
+ '&offset=' + encodeURIComponent(String(kit.pageOffset(pager)));
|
|
234
|
+
return fetch(url, { headers: { Accept: 'application/json' } })
|
|
187
235
|
.then(function (r) {
|
|
188
236
|
if (!r.ok) throw new Error('http ' + r.status);
|
|
189
237
|
return r.json();
|
|
190
238
|
})
|
|
191
|
-
.then(function (data) {
|
|
239
|
+
.then(function (data) {
|
|
240
|
+
if (seq !== inFlight) return;
|
|
241
|
+
var rows = (data && Array.isArray(data.blockers)) ? data.blockers : [];
|
|
242
|
+
state.total = (data && data.page && Number.isFinite(data.page.total)) ? data.page.total : rows.length;
|
|
243
|
+
state.shown = rows.length;
|
|
244
|
+
lastRows = rows;
|
|
245
|
+
renderList(rows);
|
|
246
|
+
paintHeadline();
|
|
247
|
+
painted = true;
|
|
248
|
+
loading = false;
|
|
249
|
+
var clamped = renderPager();
|
|
250
|
+
// The page that just painted is now the one to roll back to if a
|
|
251
|
+
// LATER page-turn fails.
|
|
252
|
+
state.page = pager.page;
|
|
253
|
+
state.perPage = pager.perPage;
|
|
254
|
+
// The queue shrank under an open page (a blocker got resolved between
|
|
255
|
+
// loads) — the kit already pulled the page back into range, so go get
|
|
256
|
+
// the rows that page actually holds rather than leaving a range that
|
|
257
|
+
// disagrees with the list.
|
|
258
|
+
if (clamped && !reclamped) return fetchPage(true);
|
|
259
|
+
})
|
|
260
|
+
.catch(function () {
|
|
261
|
+
if (seq !== inFlight) return;
|
|
262
|
+
loading = false;
|
|
263
|
+
// Put the pager back to the last page that actually painted — the kit
|
|
264
|
+
// already moved it forward before this call ran, so left alone the
|
|
265
|
+
// bar would go on claiming a page whose rows never arrived.
|
|
266
|
+
pager.page = state.page;
|
|
267
|
+
pager.perPage = state.perPage;
|
|
268
|
+
if (!painted) {
|
|
269
|
+
notice('Couldn’t load the blocker queue right now.');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
renderPager();
|
|
273
|
+
if (window.OTB && typeof window.OTB.toast === 'function') {
|
|
274
|
+
window.OTB.toast('Could not load that page. Try again.');
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Re-fetch the CURRENT server page — after a file/resolve/link write (the
|
|
280
|
+
// total and which rows belong on this page can both have moved) and on the
|
|
281
|
+
// overlay's `otb:blocker-resolved` event.
|
|
282
|
+
function reload() {
|
|
283
|
+
return fetchPage().catch(function () { toast('Couldn’t refresh the queue — try reloading the page.'); });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function mountShell() {
|
|
287
|
+
body.innerHTML = '<ul class="ov-rows" id="blockers-list"></ul>' + kit.pagerBarHtml(pager.prefix, { label: 'blockers' });
|
|
288
|
+
kit.wirePager(pager, function () { fetchPage(); });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function notice(msg) {
|
|
292
|
+
body.innerHTML = '';
|
|
293
|
+
body.className = 'note';
|
|
294
|
+
body.textContent = msg;
|
|
192
295
|
}
|
|
193
296
|
|
|
194
297
|
// ---- filing (any signed-in builder) ----------------------------------------
|
|
@@ -330,6 +433,9 @@
|
|
|
330
433
|
} catch (_) { /* signed out or offline — viewer stays { rank: null }, isReviewer() false */ }
|
|
331
434
|
}
|
|
332
435
|
|
|
436
|
+
mountShell();
|
|
437
|
+
fetchPage();
|
|
438
|
+
|
|
333
439
|
loadViewer().then(function () {
|
|
334
440
|
if (fileArea) { fileArea.innerHTML = fileFormHtml(); wireFileForm(); }
|
|
335
441
|
// Boot race: GET /me and GET /blockers fire together, and rowHtml() reads
|
|
@@ -338,14 +444,12 @@
|
|
|
338
444
|
// with isReviewer() still false and nothing re-draws it once the real
|
|
339
445
|
// rank lands — a Metic/Archon would see no Resolve/Link buttons until an
|
|
340
446
|
// unrelated re-render (a pager click, a file, an otb:blocker-resolved).
|
|
341
|
-
// Re-render now that the viewer is known, but only if
|
|
342
|
-
//
|
|
447
|
+
// Re-render now that the viewer is known, but only if a page has actually
|
|
448
|
+
// painted yet (fetchPage may still be in flight — it will render
|
|
343
449
|
// correctly on its own the moment it resolves).
|
|
344
|
-
if (
|
|
450
|
+
if (painted) rerender();
|
|
345
451
|
});
|
|
346
452
|
|
|
347
|
-
fetchBlockers().catch(function () { notice('Couldn’t load the blocker queue right now.'); });
|
|
348
|
-
|
|
349
453
|
// A row's title opens the #/blocker/N overlay ON TOP of this page
|
|
350
454
|
// (task-detail.js), whose own Resolve panel fires this — refresh the queue
|
|
351
455
|
// in place rather than leaving a resolved row sitting there.
|
|
@@ -457,7 +457,8 @@
|
|
|
457
457
|
}
|
|
458
458
|
if (resp.onboarding) renderOnboarding(resp.onboarding, lastMeState && lastMeState.discord);
|
|
459
459
|
if (resp.builder) renderProfile(resp.builder);
|
|
460
|
-
|
|
460
|
+
const axis = window.OTB.roleAxisLabel();
|
|
461
|
+
toast(selected.length ? `${axis}: ${selected.map((d) => window.OTB.roleLabel(d)).join(', ')}.` : `${axis} cleared.`);
|
|
461
462
|
} catch (err) {
|
|
462
463
|
if (err && err.message === 'redirecting to auth') return;
|
|
463
464
|
console.error('[builders] pick discipline failed', err);
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
</div>
|
|
44
44
|
<ul class="ov-rows" id="cb-for-me"></ul>
|
|
45
45
|
<div class="empty-state cb-empty" id="cb-for-me-empty" hidden>
|
|
46
|
-
<p>Nobody is waiting on you. Requests reach you by
|
|
46
|
+
<p>Nobody is waiting on you. Requests reach you by role —
|
|
47
47
|
<a href="/builders/settings">set yours in Settings</a>.</p>
|
|
48
48
|
</div>
|
|
49
49
|
<button class="btn-ghost cb-more" id="cb-for-me-more" type="button" aria-expanded="false" aria-controls="cb-for-me" hidden></button>
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
changes, it does not gain a destination. -->
|
|
78
78
|
<section class="scroll cb-block" id="cb-mingle-section" aria-labelledby="cb-mingle-h" hidden>
|
|
79
79
|
<h2 class="scroll__h" id="cb-mingle-h">Introductions</h2>
|
|
80
|
-
<p class="scroll__lede" id="cb-mingle-note">Mingle pairs you with someone from another
|
|
80
|
+
<p class="scroll__lede" id="cb-mingle-note">Mingle pairs you with someone from another role. Saying no costs nothing.</p>
|
|
81
81
|
<ul class="ov-rows" id="cb-mingle"></ul>
|
|
82
82
|
<div class="empty-state cb-empty" id="cb-mingle-empty" hidden>
|
|
83
83
|
<p>No introductions right now. One appears here when a pairing round runs.</p>
|
|
@@ -329,9 +329,12 @@
|
|
|
329
329
|
|
|
330
330
|
const sub = $('#cb-sub');
|
|
331
331
|
if (sub) {
|
|
332
|
+
// task 1003936: the axis word comes from OTB.roleAxisLabel(), never typed.
|
|
333
|
+
// The VALUES are named through roleLabel() for the same reason the filters are.
|
|
334
|
+
const axis = window.OTB.roleAxisLabel().toLowerCase();
|
|
332
335
|
sub.textContent = crafts.length
|
|
333
|
-
? `Builders asking builders. Asks to ${crafts.join(', ')}
|
|
334
|
-
:
|
|
336
|
+
? `Builders asking builders. Asks to ${crafts.map((c) => `${window.OTB.roleLabel(c)}s`).join(', ')}, and to you by name, land in your block.`
|
|
337
|
+
: `Builders asking builders. With no ${axis} set, only asks addressed to you by name land in your block.`;
|
|
335
338
|
}
|
|
336
339
|
|
|
337
340
|
// The social half. Best-effort and LAST: an introductions read that fails
|
|
@@ -366,7 +369,7 @@
|
|
|
366
369
|
${faceHtml(face)}
|
|
367
370
|
<div class="ov-row__main">
|
|
368
371
|
<a class="ov-row__title" href="/builders/profile?id=${escapeHtml(String(o.id))}">${who}</a>
|
|
369
|
-
<p class="ov-row__sub">A ${escapeHtml(String(o.craft))} — you are a ${escapeHtml(String(o.myCraft))}. Mingle pairs across
|
|
372
|
+
<p class="ov-row__sub">A ${escapeHtml(String(o.craft))} — you are a ${escapeHtml(String(o.myCraft))}. Mingle pairs across roles.</p>
|
|
370
373
|
</div>
|
|
371
374
|
<div class="ov-row__right">
|
|
372
375
|
<div class="ov-row__actions">
|
|
@@ -155,21 +155,60 @@
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// How a kind is named in a picker — 'blocker-resolution' → 'Blocker resolution'.
|
|
158
|
-
// The same shape
|
|
158
|
+
// The same shape roleLabel takes below, and it reproduces every label the
|
|
159
159
|
// hand-written <option> list in ideas.html carried, so no copy changes.
|
|
160
160
|
function kindLabel(k) {
|
|
161
161
|
const s = String(k || '').replace(/-/g, ' ');
|
|
162
162
|
return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
165
|
+
// THE ONE USER-FACING WORD FOR THE tasks.discipline AXIS (task 1003936).
|
|
166
|
+
// Before this the hall shipped four words for one axis: Discipline on the Tasks
|
|
167
|
+
// board, Discipline AND Craft on the Goals page 77 lines apart, Craft in the
|
|
168
|
+
// Directory and Settings, craft in Collab, Type/lane on the builder-start card.
|
|
169
|
+
// A label repeated in six files is free to drift back, so the word lives HERE and
|
|
170
|
+
// every surface reads it — the same argument fillKindFilter above was written on.
|
|
171
|
+
//
|
|
172
|
+
// The word is ROLE, per ADR 0174 Decision 5 ("role" means a craft — and nothing
|
|
173
|
+
// else), confirmed by the owner on 2026-09-18 as the label that ships. Renaming
|
|
174
|
+
// the COLUMN or the API field is goal 1000066’s job and deliberately not done here.
|
|
175
|
+
const ROLE_AXIS_LABEL = 'Role';
|
|
176
|
+
function roleAxisLabel() { return ROLE_AXIS_LABEL; }
|
|
177
|
+
|
|
178
|
+
// A task with no role carries the DB default 'unclassified'. That is a word a
|
|
179
|
+
// column chose, not one a person would say (docs/voice.md V2), so a picker shows
|
|
180
|
+
// "No role yet" and a row tag renders NOTHING — roleIsUnset() is how a caller asks.
|
|
181
|
+
const ROLE_UNSET = 'unclassified';
|
|
182
|
+
function roleIsUnset(d) { return !d || String(d) === ROLE_UNSET; }
|
|
183
|
+
|
|
184
|
+
// NOT ESCAPE-SAFE: this returns a capitalised copy of its input, so a caller
|
|
185
|
+
// rendering it into markup must escapeHtml() the result, as every call site does.
|
|
186
|
+
// How a role is named wherever one is shown. An acronym stays upper-case
|
|
187
|
+
// ('ui' → 'UI'); anything else is sentence case, so a role a module adds later
|
|
188
|
+
// needs no new entry. Was disciplineLabel() — renamed with the axis.
|
|
189
|
+
function roleLabel(d) {
|
|
168
190
|
const s = String(d || '');
|
|
169
191
|
if (!s) return '';
|
|
192
|
+
if (s === ROLE_UNSET) return 'No role yet';
|
|
170
193
|
return s === 'ui' ? 'UI' : s.charAt(0).toUpperCase() + s.slice(1);
|
|
171
194
|
}
|
|
172
195
|
|
|
196
|
+
// The option list for any Role <select>, so no surface types the values or the
|
|
197
|
+
// labels again. Prefers the roles the SERVER serves (the same list
|
|
198
|
+
// db.updateBuilderDisciplines validates a PATCH against), and falls back to the
|
|
199
|
+
// four this tree ships when an older server sent none — never to an empty picker.
|
|
200
|
+
// `unset: true` appends the "No role yet" bucket for the boards that filter on it.
|
|
201
|
+
function roleOptions({ any = '— any —', unset = false } = {}) {
|
|
202
|
+
// disciplines() is the ONE reader of the served list, and it carries this
|
|
203
|
+
// file's only fallback enumeration — a second one here would be the very
|
|
204
|
+
// duplication this vocabulary exists to remove.
|
|
205
|
+
const list = disciplines();
|
|
206
|
+
const opts = [{ value: '', label: any }];
|
|
207
|
+
for (const d of list) opts.push({ value: d, label: roleLabel(d) });
|
|
208
|
+
if (unset) opts.push({ value: ROLE_UNSET, label: roleLabel(ROLE_UNSET) });
|
|
209
|
+
return opts;
|
|
210
|
+
}
|
|
211
|
+
|
|
173
212
|
// Module→core settings-panel contribution seam (BV1.R42, ADR 0083) — the
|
|
174
213
|
// browser twin of the server's 'profile.fields' seam (src/module-seams.js). A
|
|
175
214
|
// module that has left core registers a Settings panel from its own client
|
|
@@ -365,7 +404,9 @@
|
|
|
365
404
|
}
|
|
366
405
|
|
|
367
406
|
window.OTB = {
|
|
368
|
-
escapeHtml, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines,
|
|
407
|
+
escapeHtml, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines,
|
|
408
|
+
// task 1003936 — ONE word for the role axis, all six surfaces
|
|
409
|
+
roleAxisLabel, roleLabel, roleOptions, roleIsUnset, ROLE_UNSET,
|
|
369
410
|
fillKindFilter, kindLabel, // task 1003453 — one Kind filler, all three surfaces
|
|
370
411
|
errCode, errMessage, errDetails, // task 1002462 — ADR 0116 envelope readers
|
|
371
412
|
contributeSettingsPanel, settingsPanels,
|
|
@@ -963,8 +963,11 @@
|
|
|
963
963
|
// disciplines a goal's tasks carry, derived in loadRollups. Staleness
|
|
964
964
|
// buckets are the shared STALE_CHOICES from task-lib.js so this page and
|
|
965
965
|
// the Tasks board offer the same lenses.
|
|
966
|
-
|
|
967
|
-
|
|
966
|
+
// task 1003936 — this bar filters GOALS, the overflow bar below filters the
|
|
967
|
+
// TASKS inside one. Both are the same axis, so both take the word from
|
|
968
|
+
// OTB.roleAxisLabel() (ADR 0174: "role" is the CRAFT axis, never a goal
|
|
969
|
+
// membership, which stays a membership_kind — ADR 0177).
|
|
970
|
+
{ id: 'f-goal-role', label: window.OTB.roleAxisLabel(), options: window.OTB.roleOptions() }, // ADR 0174
|
|
968
971
|
{ id: 'f-goal-stale', label: 'Age', options: window.OTBTaskLib.STALE_CHOICES },
|
|
969
972
|
],
|
|
970
973
|
onChange: onListFilterChange,
|
|
@@ -1040,7 +1043,8 @@
|
|
|
1040
1043
|
// here — this copy had gone stale against the DB CHECK exactly as the
|
|
1041
1044
|
// board's had, so 'verify' work could not be filtered for on either.
|
|
1042
1045
|
{ id: 'f-goal-kind', label: 'Kind', options: [{ value: '', label: '— any —' }] },
|
|
1043
|
-
|
|
1046
|
+
// ADR 0174: the craft axis again, scoped to the tasks inside one goal.
|
|
1047
|
+
{ id: 'f-goal-discipline', label: `Task ${window.OTB.roleAxisLabel().toLowerCase()}`, options: window.OTB.roleOptions({ unset: true }) }, // ADR 0174
|
|
1044
1048
|
{ id: 'f-goal-newcomer', label: 'Newcomer', options: [{ value: '', label: '— any —' }, { value: 'yes', label: 'newcomer-friendly only' }] },
|
|
1045
1049
|
{ id: 'f-goal-fit', label: 'Run mode', options: [{ value: '', label: '— any —' },
|
|
1046
1050
|
{ value: 'parallel', label: 'parallel-safe only' },
|
|
@@ -318,8 +318,9 @@
|
|
|
318
318
|
const cpm = creditsPerMinute(t);
|
|
319
319
|
const cpmText = Number.isFinite(cpm) ? `${cpm.toFixed(2)} ${curLabel()}/min` : `— ${curLabel()}/min`;
|
|
320
320
|
const minutesText = t.est_minutes ? `${t.est_minutes}m` : '—';
|
|
321
|
-
const
|
|
322
|
-
|
|
321
|
+
const disciplineTag = window.OTB.roleIsUnset(t.discipline)
|
|
322
|
+
? ''
|
|
323
|
+
: `<span class="task__tag">${escapeHtml(window.OTB.roleLabel(t.discipline))}</span>`;
|
|
323
324
|
const newcomerTag = t.newcomer_friendly ? '<span class="task__tag task__tag--newcomer">newcomer</span>' : '';
|
|
324
325
|
const rankTag = rankTagHtml(t.requires_rank);
|
|
325
326
|
const touches = Array.isArray(t.touches) ? t.touches : [];
|
|
@@ -502,7 +503,7 @@
|
|
|
502
503
|
let list = state.work.todo.filter((t) => {
|
|
503
504
|
if (!matchesTodoSearch(t, sQuery)) return false;
|
|
504
505
|
if (kFilter && (t.kind || 'unclassified') !== kFilter) return false;
|
|
505
|
-
if (dFilter && (t.discipline ||
|
|
506
|
+
if (dFilter && (t.discipline || window.OTB.ROLE_UNSET) !== dFilter) return false;
|
|
506
507
|
if (nFilter === 'yes' && !t.newcomer_friendly) return false;
|
|
507
508
|
return true;
|
|
508
509
|
});
|
|
@@ -1013,7 +1013,7 @@
|
|
|
1013
1013
|
// a module-contributed craft (ui-design → ui) never appeared here at all.
|
|
1014
1014
|
action = `
|
|
1015
1015
|
<div class="onboarding-item__action onboarding-item__disc-pick">
|
|
1016
|
-
${OTB.disciplines().map((d) => discBtn(d, OTB.
|
|
1016
|
+
${OTB.disciplines().map((d) => discBtn(d, OTB.roleLabel(d))).join('')}
|
|
1017
1017
|
</div>`;
|
|
1018
1018
|
} else if (s.id === 'ship_three') {
|
|
1019
1019
|
action = `
|
|
@@ -196,13 +196,13 @@
|
|
|
196
196
|
crafts.sort();
|
|
197
197
|
kit.makeFilterBar({
|
|
198
198
|
mount: document.getElementById('people-filters'),
|
|
199
|
-
search: { id: 'people-q', label: 'Search', placeholder:
|
|
199
|
+
search: { id: 'people-q', label: 'Search', placeholder: `name or ${window.OTB.roleAxisLabel().toLowerCase()}` },
|
|
200
200
|
primary: [
|
|
201
201
|
{ id: 'people-sort', label: 'Sort', options: [
|
|
202
202
|
{ value: '', label: 'track record' }, { value: 'ships', label: 'most ships' },
|
|
203
203
|
{ value: 'credits', label: 'most ' + currencyLabel().toLowerCase() }, { value: 'karma', label: 'most karma' },
|
|
204
204
|
{ value: 'projects', label: 'most projects' }, { value: 'recent', label: 'most recently active' }] },
|
|
205
|
-
{ id: 'people-craft', label:
|
|
205
|
+
{ id: 'people-craft', label: window.OTB.roleAxisLabel(), options: [{ value: '', label: '— any —' }].concat(crafts.map((c) => ({ value: c, label: window.OTB.roleLabel(c) }))) },
|
|
206
206
|
],
|
|
207
207
|
onChange: function (vals) {
|
|
208
208
|
query = String(vals['people-q'] || '').trim().toLowerCase();
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
ledger = kit.makeLedger({
|
|
218
218
|
mount: body,
|
|
219
219
|
columns: ['Builder', { label: 'Projects', class: 'ledger__num' }, { label: 'Ships', class: 'ledger__num' },
|
|
220
|
-
{ label: currencyLabel(), class: 'ledger__num' }, { label: 'Karma', class: 'ledger__num' },
|
|
220
|
+
{ label: currencyLabel(), class: 'ledger__num' }, { label: 'Karma', class: 'ledger__num' }, `${window.OTB.roleAxisLabel()}s`, 'Last active'],
|
|
221
221
|
rowHtml: rowHtml,
|
|
222
222
|
// Reached only when the directory HAS builders and the lenses excluded them
|
|
223
223
|
// all — the empty-API case returns above with its own explanation.
|
|
@@ -128,7 +128,10 @@
|
|
|
128
128
|
function craftMix(rows, max = MIX_MAX) {
|
|
129
129
|
const counts = new Map();
|
|
130
130
|
for (const r of rows || []) {
|
|
131
|
-
|
|
131
|
+
// task 1003936: the bucket KEY stays the stored value, but what a person
|
|
132
|
+
// reads is OTB.roleLabel() — an unset role charted as "unclassified" was the
|
|
133
|
+
// database's word on a page (docs/voice.md V2).
|
|
134
|
+
const k = window.OTB.roleLabel((r && r.discipline) || window.OTB.ROLE_UNSET);
|
|
132
135
|
counts.set(k, (counts.get(k) || 0) + 1);
|
|
133
136
|
}
|
|
134
137
|
const sorted = [...counts.entries()].map(([label, n]) => ({ label, n })).sort((a, b) => b.n - a.n || a.label.localeCompare(b.label));
|
|
@@ -462,7 +465,7 @@
|
|
|
462
465
|
: `${fmtNum(paidFull.length ? paidFull[paidFull.length - 1].value : 0)} paid, from the credits ledger · ${span}`,
|
|
463
466
|
}),
|
|
464
467
|
mixChart({
|
|
465
|
-
id: 'chart-mix', title:
|
|
468
|
+
id: 'chart-mix', title: `Works by ${window.OTB.roleAxisLabel().toLowerCase()}`, mix,
|
|
466
469
|
footer: `${fmtNum(inWindow.length)} work${inWindow.length === 1 ? '' : 's'} · ${span}`,
|
|
467
470
|
}),
|
|
468
471
|
];
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
craft at a time, with goals and the idea trail. -->
|
|
106
106
|
<section class="scroll" id="works-scroll" aria-labelledby="works-h" data-group="works">
|
|
107
107
|
<h2 id="works-h" class="scroll__h">Works</h2>
|
|
108
|
-
<p class="scroll__lede" id="works-lede">Everything this builder has shipped. Seen one
|
|
108
|
+
<p class="scroll__lede" id="works-lede">Everything this builder has shipped. Seen one role at a time, with goals and ideas, under <a href="#activity">activity</a>.</p>
|
|
109
109
|
<table class="ledger" id="works-table">
|
|
110
110
|
<thead>
|
|
111
111
|
<tr>
|
|
@@ -130,8 +130,8 @@
|
|
|
130
130
|
the ideator view) every idea they filed with what became of it.
|
|
131
131
|
All three ledgers page server-side; profile.js fills them. -->
|
|
132
132
|
<section class="scroll" id="activity-scroll" aria-labelledby="activity-h" data-group="activity">
|
|
133
|
-
<h2 id="activity-h" class="scroll__h">Activity, by
|
|
134
|
-
<p class="scroll__lede" id="activity-lede">What this builder has done, seen one
|
|
133
|
+
<h2 id="activity-h" class="scroll__h">Activity, by role</h2>
|
|
134
|
+
<p class="scroll__lede" id="activity-lede">What this builder has done, seen one role at a time.</p>
|
|
135
135
|
<div id="activity-tabs"></div>
|
|
136
136
|
<div id="activity-body">
|
|
137
137
|
<div class="profile__placeholder" id="activity-placeholder">…</div>
|
|
@@ -305,7 +305,7 @@
|
|
|
305
305
|
`</div>`;
|
|
306
306
|
const discs = Array.isArray(cp.disciplines) ? cp.disciplines.filter((d) => typeof d === 'string') : [];
|
|
307
307
|
const metaBits = [];
|
|
308
|
-
if (discs.length) metaBits.push(
|
|
308
|
+
if (discs.length) metaBits.push(`${window.OTB.roleAxisLabel()}s: ${discs.map((d) => window.OTB.roleLabel(d)).join(', ')}`);
|
|
309
309
|
if (cp.last_active) metaBits.push(`Last active ${fmtDate(cp.last_active)}`);
|
|
310
310
|
const meta = metaBits.length ? `<p class="scroll__lede">${escapeHtml(metaBits.join(' · '))}</p>` : '';
|
|
311
311
|
const named = Array.isArray(cp.projects) ? cp.projects : [];
|
|
@@ -459,10 +459,10 @@
|
|
|
459
459
|
columns: [
|
|
460
460
|
{ label: '#', class: 'ledger__num' },
|
|
461
461
|
'Work',
|
|
462
|
-
{ label:
|
|
462
|
+
{ label: window.OTB.roleAxisLabel() },
|
|
463
463
|
{ label: 'Shipped', class: 'ledger__date' },
|
|
464
464
|
],
|
|
465
|
-
emptyMsg:
|
|
465
|
+
emptyMsg: `No completed works in this ${window.OTB.roleAxisLabel().toLowerCase()}.`,
|
|
466
466
|
fetchPage: async (offset, limit) => {
|
|
467
467
|
const res = await api.request('GET', `${API}/builders/${encodeURIComponent(builderId)}/activity`
|
|
468
468
|
+ `?section=tasks&limit=${limit}&offset=${offset}${role ? `&role=${encodeURIComponent(role)}` : ''}`);
|
|
@@ -477,7 +477,7 @@
|
|
|
477
477
|
return `<tr>
|
|
478
478
|
<td class="ledger__num">#${escapeHtml(String(t.id))}</td>
|
|
479
479
|
<td><a href="${taskHref(t.id)}">${escapeHtml(t.title || `Task ${t.id}`)}</a>${summary}${visual ? `<span style="display:block;margin-top:6px">${visual}</span>` : ''}</td>
|
|
480
|
-
<td
|
|
480
|
+
<td>${window.OTB.roleIsUnset(t.discipline) ? '' : `<span class="task__tag">${escapeHtml(window.OTB.roleLabel(t.discipline))}</span>`}</td>
|
|
481
481
|
<td class="ledger__date">${fmtDate(t.shipped_at)}</td>
|
|
482
482
|
</tr>`;
|
|
483
483
|
},
|
|
@@ -489,10 +489,10 @@
|
|
|
489
489
|
columns: [
|
|
490
490
|
{ label: '#', class: 'ledger__num' },
|
|
491
491
|
'Goal',
|
|
492
|
-
|
|
492
|
+
`${window.OTB.roleAxisLabel()}s`,
|
|
493
493
|
'Progress',
|
|
494
494
|
],
|
|
495
|
-
emptyMsg:
|
|
495
|
+
emptyMsg: `No goals created in this ${window.OTB.roleAxisLabel().toLowerCase()}.`,
|
|
496
496
|
fetchPage: async (offset, limit) => {
|
|
497
497
|
const res = await api.request('GET', `${API}/builders/${encodeURIComponent(builderId)}/activity`
|
|
498
498
|
+ `?section=goals&limit=${limit}&offset=${offset}${role ? `&role=${encodeURIComponent(role)}` : ''}`);
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
<details class="speciality-contract"${theirs ? ' open' : ''}>
|
|
70
70
|
<summary>${theirs ? 'What this adds to your sessions — written by someone else' : 'What this adds to your sessions'}</summary>
|
|
71
71
|
<pre class="speciality-contract__body">${escapeHtml(s.contract_md)}</pre>
|
|
72
|
-
${theirs ? '<p class="prow__hint">You did not write this. Read it before using the speciality — it reaches every session where you hold work in that
|
|
72
|
+
${theirs ? '<p class="prow__hint">You did not write this. Read it before using the speciality — it reaches every session where you hold work in that role, and it is passed to Claude as this author\'s stated preferences, not as instructions it must obey.</p>' : ''}
|
|
73
73
|
</details>`;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
const name = create.elements.name.value.trim();
|
|
254
254
|
const discipline = create.elements.discipline.value;
|
|
255
255
|
if (!name) { statusLine('Give it a name.'); return; }
|
|
256
|
-
if (!discipline) { statusLine(
|
|
256
|
+
if (!discipline) { statusLine(`Pick which ${window.OTB.roleAxisLabel().toLowerCase()} it sits under.`); return; }
|
|
257
257
|
try {
|
|
258
258
|
await call('POST', '/specialities', {
|
|
259
259
|
name,
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
|
|
124
124
|
<!-- CRAFT — discipline preferences (GET /me, PATCH /me/disciplines). -->
|
|
125
125
|
<section class="scroll" id="craft-scroll" aria-labelledby="craft-h" data-group="account" hidden>
|
|
126
|
-
<h2 id="craft-h" class="scroll__h">Preferred
|
|
126
|
+
<h2 id="craft-h" class="scroll__h">Preferred roles</h2>
|
|
127
127
|
<p class="scroll__lede">
|
|
128
128
|
The kinds of work you prefer. Task suggestions lean toward your
|
|
129
129
|
choices — pick one, several, or none.
|
|
@@ -278,9 +278,9 @@
|
|
|
278
278
|
<section class="scroll" id="specialities-scroll" aria-labelledby="specialities-h" data-group="preferences" hidden>
|
|
279
279
|
<h2 id="specialities-h" class="scroll__h">Your specialities</h2>
|
|
280
280
|
<p class="scroll__lede">
|
|
281
|
-
A
|
|
281
|
+
A role says how you work. A speciality says what you work on — and how you,
|
|
282
282
|
as someone who knows the subject, want to be engaged inside it. Whichever one
|
|
283
|
-
you are using reaches every session where you hold a claim in that
|
|
283
|
+
you are using reaches every session where you hold a claim in that role.
|
|
284
284
|
Train it by adding things you have written; it also picks things up on its own
|
|
285
285
|
as you work.
|
|
286
286
|
</p>
|
|
@@ -294,7 +294,7 @@
|
|
|
294
294
|
<input type="text" name="name" maxlength="80" required placeholder="Economics" />
|
|
295
295
|
</label>
|
|
296
296
|
<label class="speciality-field">
|
|
297
|
-
<span>
|
|
297
|
+
<span>Role it sits under</span>
|
|
298
298
|
<select name="discipline" required>
|
|
299
299
|
<option value="">Pick one…</option>
|
|
300
300
|
<option value="engineer">Engineer</option>
|
|
@@ -555,7 +555,7 @@
|
|
|
555
555
|
const follows = !data || data.recruiter_discoverable == null;
|
|
556
556
|
const grant = data && data.hide_stats
|
|
557
557
|
? 'Because your cross-project record is hidden, they will see your name and your picture, and nothing else.'
|
|
558
|
-
: 'They will see your name, your picture, your
|
|
558
|
+
: 'They will see your name, your picture, your roles, and whether you have been active in the last 30 days — never your credits, works shipped, karma, or the projects you build on.';
|
|
559
559
|
// The surprising half, and only where it applies: a private profile answers
|
|
560
560
|
// "not found" to every visitor, and this one switch is the exception.
|
|
561
561
|
const despitePrivate = isPublic ? ''
|
|
@@ -684,7 +684,7 @@
|
|
|
684
684
|
return (
|
|
685
685
|
`<label class="craft-chip" data-disc="${escapeHtml(d)}" data-on="${on ? '1' : '0'}">` +
|
|
686
686
|
`<input type="checkbox" class="craft-pref" value="${escapeHtml(d)}"${on ? ' checked' : ''}>` +
|
|
687
|
-
`${escapeHtml(window.OTB.
|
|
687
|
+
`${escapeHtml(window.OTB.roleLabel(d))}</label>`
|
|
688
688
|
);
|
|
689
689
|
}).join('');
|
|
690
690
|
}
|