@bongos/core 1.24.35 → 1.24.36
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 +101 -146
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0228-the-reading-room-owes-a-reference-content-rules-not-a-second-archetype.md +1 -1
- package/docs/copy-inventory.md +136 -192
- package/docs/copy-registry.json +186 -752
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/diagrams/README.md +1 -2
- package/docs/onboarding/primer.md +64 -22
- package/docs/page-inventory.json +7 -101
- package/docs/page-readings.json +947 -1415
- package/docs/recipes/ui-look-before-you-ship.md +2 -2
- package/modules/design-styles/skills/style/SKILL.md +2 -2
- package/modules/hall-ui/public/ai-studio.html +2 -2
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/builders.js +8 -6
- package/modules/hall-ui/public/city.html +2 -2
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +1 -1
- package/modules/hall-ui/public/diagram-viewer.js +5 -5
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/founding-mark.js +3 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +62 -1
- package/modules/hall-ui/public/hall-widgets.js +11 -1
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +4 -4
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-rules.html +2 -2
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/palette.js +15 -0
- package/modules/hall-ui/public/primer-live.js +188 -0
- package/modules/hall-ui/public/primer.html +11 -6
- package/modules/hall-ui/public/primer.js +95 -23
- package/modules/hall-ui/public/primer.states.json +54 -7
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +23 -121
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +14 -21
- package/modules/hall-ui/public/street.html +2 -2
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +8 -55
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/founding-mark.md +4 -4
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/hall-ui/records/reference-room.md +2 -0
- package/modules/onboarding/founding-mode.js +6 -2
- package/modules/ui-design/kit/render.js +1 -1
- package/modules/ui-design/styles/README.md +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/bongos/gen-hall-sitemap.js +3 -0
- package/scripts/hall-preview/server.js +6 -11
- package/src/bongos/serve-internal.js +28 -37
- package/src/module-api.js +1 -1
- package/tests/craft_mode_home.mjs +6 -2
- package/tests/founding_builders.mjs +6 -5
- package/tests/founding_mode.mjs +7 -4
- package/tests/hall_board_world.mjs +44 -1
- package/tests/hall_fullscreen.mjs +1 -1
- package/tests/hall_nav.mjs +24 -7
- package/tests/hall_page_gate_map.mjs +14 -5
- package/tests/hall_reading_room.mjs +426 -0
- package/tests/hall_reading_world.mjs +36 -30
- package/tests/nav_permission_atoms.mjs +3 -2
- package/tests/rank_tier_single_source.mjs +0 -1
- package/tests/ui_design_kit.mjs +1 -1
- package/modules/hall-ui/public/diagrams.css +0 -45
- package/modules/hall-ui/public/diagrams.html +0 -106
- package/modules/hall-ui/public/diagrams.states.json +0 -28
- package/modules/hall-ui/public/drachmae.html +0 -110
- package/modules/hall-ui/public/drachmae.js +0 -431
- package/modules/hall-ui/public/drachmae.states.json +0 -58
- package/modules/hall-ui/public/ranks-founding.states.json +0 -12
- package/modules/hall-ui/public/ranks.html +0 -83
- package/modules/hall-ui/public/ranks.js +0 -229
- package/modules/hall-ui/public/ranks.states.json +0 -15
- package/tests/hall_reference_world.mjs +0 -352
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
// drachmae.js — the in-hall reader for the "Drachmae & karma" doc (task 1472),
|
|
2
|
-
// rebuilt as the hall's first REFERENCE room (task 1003473, goal 1000081).
|
|
3
|
-
// Fetches the canonical markdown at /builders/drachmae-and-karma.md (mounted
|
|
4
|
-
// from docs/onboarding/drachmae-and-karma.md — single source of truth, also the
|
|
5
|
-
// one the primer links to) and renders it through the shared OTBMdReader, so
|
|
6
|
-
// builders can read exactly how they get paid without leaving the hall.
|
|
7
|
-
//
|
|
8
|
-
// WHAT A REFERENCE OWES THAT THE PRIMER DOES NOT. People arrive at this page to
|
|
9
|
-
// answer one question about how they get paid, so it is scanned and deep-linked
|
|
10
|
-
// rather than read start to finish. Three things follow, and they are the whole
|
|
11
|
-
// difference between this file and primer.js (the room itself — the layout, the
|
|
12
|
-
// glass contents, the card — is room.css's, shared):
|
|
13
|
-
// * the outline goes to h3 depth and the scrollspy lights the section AND
|
|
14
|
-
// its part (the trail), with the phone toggle naming where the reader is;
|
|
15
|
-
// * every h2/h3/h4 carries its own link (.room-anchor), which copies the
|
|
16
|
-
// address on click, and a deep link lands after the async render;
|
|
17
|
-
// * there is no progress hairline and no acknowledge gate: a reference has
|
|
18
|
-
// no end to reach.
|
|
19
|
-
//
|
|
20
|
-
// task 2174: the red-team bounty rate card lives here. It is a published reward
|
|
21
|
-
// SCHEDULE — reference material that reads the same for every builder — so it
|
|
22
|
-
// belongs with the rest of the reward model. The doc's §"Red-team / security
|
|
23
|
-
// bounty" describes it in prose; the LIVE numbers come from /public/bounty-table
|
|
24
|
-
// and are inserted as a real section (h2 + table) ahead of the closing links,
|
|
25
|
-
// so the outline and the scrollspy pick it up like any other section.
|
|
26
|
-
|
|
27
|
-
(() => {
|
|
28
|
-
'use strict';
|
|
29
|
-
|
|
30
|
-
const DOC_URL = '/builders/drachmae-and-karma.md';
|
|
31
|
-
// The doc lives at docs/onboarding/, so relative links resolve against it.
|
|
32
|
-
const DOC_DIR = 'docs/onboarding/';
|
|
33
|
-
const BOUNTY_URL = '/api/bongos/public/bounty-table';
|
|
34
|
-
// task 1003446: the treasury feed. NOT a new endpoint — this is the same
|
|
35
|
-
// /public/cost-summary the signed-out status page already draws as a
|
|
36
|
-
// cumulative line and the Work board already fetches to read ONE scalar off
|
|
37
|
-
// (`total_value_credits`). Its own route comment calls `daily[]` "the
|
|
38
|
-
// treasury chart's cumulative line"; the series was in the payload the whole
|
|
39
|
-
// time, and the stranger got the chart while the builder got the number.
|
|
40
|
-
const TREASURY_URL = '/api/bongos/public/cost-summary';
|
|
41
|
-
const TREASURY_SECTION_ID = 'the-treasury';
|
|
42
|
-
const TREASURY_SECTION_TITLE = 'The treasury, so far';
|
|
43
|
-
const BOUNTY_SECTION_ID = 'security-bounty-rates';
|
|
44
|
-
const BOUNTY_SECTION_TITLE = 'Security bounty rates';
|
|
45
|
-
// The section the live table is inserted AHEAD of: the doc's closing links.
|
|
46
|
-
// Matched by its opening words, so a retitled footer falls back to appending.
|
|
47
|
-
const CLOSING_H2 = /^see it/i;
|
|
48
|
-
// The y (viewport space) treated as "where the reader is looking": below the
|
|
49
|
-
// sticky top bar, where the eye lands reading down a page.
|
|
50
|
-
const READING_LINE_Y = 100;
|
|
51
|
-
const LINK_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.6 9.4a2.7 2.7 0 0 0 3.8 0l2.3-2.3a2.7 2.7 0 0 0-3.8-3.8L7.7 4.5"/><path d="M9.4 6.6a2.7 2.7 0 0 0-3.8 0L3.3 8.9a2.7 2.7 0 0 0 3.8 3.8l1.2-1.2"/></svg>';
|
|
52
|
-
|
|
53
|
-
let repoBlobBase = null;
|
|
54
|
-
let repoTreeBase = null;
|
|
55
|
-
|
|
56
|
-
function $(sel) { return document.querySelector(sel); }
|
|
57
|
-
|
|
58
|
-
// The shell's toast (style.css .toast[data-visible="1"]), the same contract
|
|
59
|
-
// primer.js and copy-on-click.js speak; this page loads no dom-utils.js.
|
|
60
|
-
function toast(msg) {
|
|
61
|
-
const el = $('#toast');
|
|
62
|
-
if (!el) return;
|
|
63
|
-
el.textContent = msg;
|
|
64
|
-
el.dataset.type = 'info';
|
|
65
|
-
el.dataset.visible = '1';
|
|
66
|
-
clearTimeout(toast._t);
|
|
67
|
-
toast._t = setTimeout(() => { el.dataset.visible = '0'; }, 2400);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function loadRepoInfoBases() {
|
|
71
|
-
try {
|
|
72
|
-
const r = await fetch('/api/bongos/public/repo-info', { credentials: 'same-origin' });
|
|
73
|
-
if (!r.ok) return;
|
|
74
|
-
const data = await r.json();
|
|
75
|
-
if (data && typeof data.blob_base === 'string') repoBlobBase = data.blob_base;
|
|
76
|
-
if (data && typeof data.tree_base === 'string') repoTreeBase = data.tree_base;
|
|
77
|
-
} catch (_) { /* links degrade to the original href */ }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// The live rate card as a real section of the article, placed ahead of the
|
|
81
|
-
// closing links so the doc still ends on "dig deeper". Returns false when the
|
|
82
|
-
// schedule can't be read: the prose section stays, and no empty table or
|
|
83
|
-
// outline entry that leads nowhere is rendered.
|
|
84
|
-
async function insertBountySection(article) {
|
|
85
|
-
let payload;
|
|
86
|
-
try {
|
|
87
|
-
const r = await fetch(BOUNTY_URL, { credentials: 'same-origin' });
|
|
88
|
-
if (!r.ok) throw new Error(`${BOUNTY_URL} → ${r.status}`);
|
|
89
|
-
payload = await r.json();
|
|
90
|
-
} catch (err) {
|
|
91
|
-
console.warn('[drachmae] bounty schedule unavailable:', err.message);
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
const rows = Array.isArray(payload && payload.table) ? payload.table : [];
|
|
95
|
-
if (!rows.length) return false;
|
|
96
|
-
|
|
97
|
-
const esc = window.OTBMdReader.escapeHtml;
|
|
98
|
-
const currency = payload.currency || 'credits';
|
|
99
|
-
const section = document.createElement('section');
|
|
100
|
-
section.innerHTML =
|
|
101
|
-
`<h2 id="${esc(BOUNTY_SECTION_ID)}">${esc(BOUNTY_SECTION_TITLE)}</h2>` +
|
|
102
|
-
`<p>Find a flaw in the system’s own walls and report it: rewards are paid in ` +
|
|
103
|
-
`${esc(currency)} by severity. These are the live numbers, read from the server ` +
|
|
104
|
-
`when you opened this page, so a tuned rate shows up here immediately.</p>` +
|
|
105
|
-
'<table><thead><tr><th>Severity</th>' +
|
|
106
|
-
`<th>${esc(currency.charAt(0).toUpperCase() + currency.slice(1))}</th>` +
|
|
107
|
-
'<th>What counts</th></tr></thead><tbody>' +
|
|
108
|
-
rows.map((r) => {
|
|
109
|
-
// `overridden` means this instance tuned the tier off its default.
|
|
110
|
-
const tuned = r.overridden ? ' <em>(tuned)</em>' : '';
|
|
111
|
-
return `<tr><td><strong>${esc(r.severity || '')}</strong></td>` +
|
|
112
|
-
`<td>${esc(String(r.drachmae ?? ''))}${tuned}</td>` +
|
|
113
|
-
`<td>${esc(r.blurb || '')}</td></tr>`;
|
|
114
|
-
}).join('') +
|
|
115
|
-
'</tbody></table>';
|
|
116
|
-
const closing = [...article.querySelectorAll('h2')].find((h) => CLOSING_H2.test(h.textContent.trim()));
|
|
117
|
-
if (closing) article.insertBefore(section, closing); else article.appendChild(section);
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ---- the treasury (task 1003446) -----------------------------------------
|
|
122
|
-
|
|
123
|
-
// One cumulative figure: the plot, the number it ends on, and the table twin
|
|
124
|
-
// that keeps it out of picture-only territory. `fmt` renders a value for a
|
|
125
|
-
// human; `rows` is the twin's own text, so the money and the credits can read
|
|
126
|
-
// differently without the chart knowing about either.
|
|
127
|
-
function treasuryFigure({ id, title, points, fmt, foot }) {
|
|
128
|
-
const kit = window.OTBKit;
|
|
129
|
-
const esc = window.OTBMdReader.escapeHtml;
|
|
130
|
-
if (!points.length) return '';
|
|
131
|
-
const last = points[points.length - 1];
|
|
132
|
-
const series = kit.timeStepSeriesHtml(points, {
|
|
133
|
-
x0: points[0].x,
|
|
134
|
-
x1: last.x,
|
|
135
|
-
yMax: last.value,
|
|
136
|
-
});
|
|
137
|
-
const { endX, endY } = series.geometry;
|
|
138
|
-
const day = (t) => new Date(t).toISOString().slice(0, 10);
|
|
139
|
-
// Every 7th day plus the last: a twin with one row per calendar day since
|
|
140
|
-
// May would be ~120 rows behind a disclosure nobody finishes reading.
|
|
141
|
-
const twinRows = points.filter((p, i) => i % 7 === 0 || i === points.length - 1);
|
|
142
|
-
return `<figure class="pchart" id="${esc(id)}">
|
|
143
|
-
<figcaption class="pchart__title">${esc(title)}</figcaption>
|
|
144
|
-
${kit.plotSvgHtml({
|
|
145
|
-
series: series.body,
|
|
146
|
-
// No readout is wired here, so no focus stop and no dead crosshair.
|
|
147
|
-
interactive: false,
|
|
148
|
-
ariaLabel: `${title}: ${fmt(last.value)} as of ${day(last.x)}`,
|
|
149
|
-
label: kit.plotValueLabelHtml(endX, endY - 8, fmt(last.value), 'end'),
|
|
150
|
-
leftTick: day(points[0].x),
|
|
151
|
-
rightTick: day(last.x),
|
|
152
|
-
})}
|
|
153
|
-
<details class="pchart__twin">
|
|
154
|
-
<summary>Read it as a table</summary>
|
|
155
|
-
<table class="ledger pchart__table"><thead><tr><th>Day</th><th>${esc(title)}</th></tr></thead><tbody>
|
|
156
|
-
${twinRows.map((p) => `<tr><td>${esc(day(p.x))}</td><td>${esc(fmt(p.value))}</td></tr>`).join('')}
|
|
157
|
-
</tbody></table>
|
|
158
|
-
</details>
|
|
159
|
-
<p class="pchart__foot">${esc(foot)}</p>
|
|
160
|
-
</figure>`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// The live treasury as a real section of the article, ahead of the rate card.
|
|
164
|
-
// Returns false when the summary can't be read: the prose stays and no empty
|
|
165
|
-
// panel or outline entry that leads nowhere is rendered — the same contract
|
|
166
|
-
// insertBountySection keeps.
|
|
167
|
-
async function insertTreasurySection(article) {
|
|
168
|
-
if (!window.OTBKit) {
|
|
169
|
-
console.warn('[drachmae] OTBKit missing — the treasury section needs the chart primitive');
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
let payload;
|
|
173
|
-
try {
|
|
174
|
-
const r = await fetch(TREASURY_URL, { credentials: 'same-origin' });
|
|
175
|
-
if (!r.ok) throw new Error(`${TREASURY_URL} → ${r.status}`);
|
|
176
|
-
payload = await r.json();
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.warn('[drachmae] cost summary unavailable:', err.message);
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
const esc = window.OTBMdReader.escapeHtml;
|
|
182
|
-
const kit = window.OTBKit;
|
|
183
|
-
const currency = (window.__BRANDING__ && window.__BRANDING__.currency
|
|
184
|
-
&& window.__BRANDING__.currency.label) || 'credits';
|
|
185
|
-
const Currency = currency.charAt(0).toUpperCase() + currency.slice(1);
|
|
186
|
-
const usd = (n) => `$${Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
|
|
187
|
-
const num = (n) => Number(n || 0).toLocaleString();
|
|
188
|
-
|
|
189
|
-
// The server already ran generate_series, so every calendar day is present
|
|
190
|
-
// and a flat run is a real flat run rather than a missing sample.
|
|
191
|
-
const spend = kit.cumulateByDay(payload.daily, { valueOf: (r) => r.amount_usd });
|
|
192
|
-
const earned = kit.cumulateByDay(payload.daily_credits, { valueOf: (r) => r.credits });
|
|
193
|
-
// Both series empty is a treasury with nothing in it yet — one honest line,
|
|
194
|
-
// not two empty plots.
|
|
195
|
-
if (!spend.length && !earned.length) return false;
|
|
196
|
-
|
|
197
|
-
const figures = [
|
|
198
|
-
treasuryFigure({
|
|
199
|
-
id: 'treasury-spend',
|
|
200
|
-
title: 'Spent, cumulative',
|
|
201
|
-
points: spend,
|
|
202
|
-
fmt: usd,
|
|
203
|
-
foot: 'Every recorded cost — API, infra, compute, art — added up day by day.',
|
|
204
|
-
}),
|
|
205
|
-
treasuryFigure({
|
|
206
|
-
id: 'treasury-earned',
|
|
207
|
-
title: `${Currency} awarded, cumulative`,
|
|
208
|
-
points: earned,
|
|
209
|
-
fmt: num,
|
|
210
|
-
foot: `What the project has paid builders, in ${esc(currency)}, day by day.`,
|
|
211
|
-
}),
|
|
212
|
-
].filter(Boolean).join('');
|
|
213
|
-
|
|
214
|
-
// Where the money went, as the kit's bar series rather than a fourth
|
|
215
|
-
// hand-rolled one. Categories arrive sorted biggest-first.
|
|
216
|
-
const byCategory = (payload.by_category || []).slice(0, 8);
|
|
217
|
-
const categories = byCategory.length
|
|
218
|
-
? `<h3 id="treasury-by-category">Where it went</h3>`
|
|
219
|
-
+ kit.barSeriesHtml(
|
|
220
|
-
byCategory.map((c) => ({ label: c.category, value: c.total_usd, valueLabel: usd(c.total_usd) })),
|
|
221
|
-
{ list: true, ariaLabel: 'Spend by category', valueWidth: '84px' },
|
|
222
|
-
)
|
|
223
|
-
: '';
|
|
224
|
-
|
|
225
|
-
const section = document.createElement('section');
|
|
226
|
-
section.innerHTML =
|
|
227
|
-
`<h2 id="${esc(TREASURY_SECTION_ID)}">${esc(TREASURY_SECTION_TITLE)}</h2>`
|
|
228
|
-
+ `<p>These are this project's real numbers, read from the server when you `
|
|
229
|
-
+ `opened this page. It has spent <strong>${esc(usd(payload.total_usd))}</strong> `
|
|
230
|
-
+ `and awarded <strong>${esc(num(payload.total_value_credits))} ${esc(currency)}</strong> `
|
|
231
|
-
+ `for the work that money bought.</p>`
|
|
232
|
-
+ `<div class="pcharts">${figures}</div>`
|
|
233
|
-
+ categories;
|
|
234
|
-
|
|
235
|
-
const bounty = article.querySelector(`#${BOUNTY_SECTION_ID}`);
|
|
236
|
-
const closing = [...article.querySelectorAll('h2')].find((h) => CLOSING_H2.test(h.textContent.trim()));
|
|
237
|
-
// Ahead of the rate card if it landed first, else ahead of the closing
|
|
238
|
-
// links, else appended: the doc keeps ending on "dig deeper" either way.
|
|
239
|
-
const before = (bounty && bounty.parentElement === article ? bounty : null) || closing;
|
|
240
|
-
if (before) article.insertBefore(section, before); else article.appendChild(section);
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// The outline is read from the RENDERED headings rather than the parser's raw
|
|
245
|
-
// text, so `*not*` and a [link](…) in a heading arrive as plain words, and so
|
|
246
|
-
// the live section inserted above is simply one more heading in order.
|
|
247
|
-
function collectHeadings(article) {
|
|
248
|
-
return [...article.querySelectorAll('h2[id], h3[id]')].map((el) => ({
|
|
249
|
-
level: Number(el.tagName.charAt(1)), id: el.id, text: el.textContent.trim(),
|
|
250
|
-
}));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// h2s as the list, each h3 nested under its part; no numerals (room.css turns
|
|
254
|
-
// the room's counter off for a reference). Returns the flat list in document
|
|
255
|
-
// order, each entry carrying its part's id and title, which is what the
|
|
256
|
-
// scrollspy walks and what the phone toggle names.
|
|
257
|
-
function renderToc(headings) {
|
|
258
|
-
const list = $('#primer-toc-list');
|
|
259
|
-
if (!list) return [];
|
|
260
|
-
if (headings.length === 0) { list.innerHTML = '<li class="primer-toc__placeholder">(no sections)</li>'; return []; }
|
|
261
|
-
const esc = window.OTBMdReader.escapeHtml;
|
|
262
|
-
const link = (h) => `<a href="#${esc(h.id)}" data-section="${esc(h.id)}">${esc(h.text)}</a>`;
|
|
263
|
-
const parts = [];
|
|
264
|
-
for (const h of headings) {
|
|
265
|
-
const last = parts[parts.length - 1];
|
|
266
|
-
if (h.level === 3 && last) last.subs.push(h);
|
|
267
|
-
else parts.push({ head: h, subs: [] });
|
|
268
|
-
}
|
|
269
|
-
list.innerHTML = parts.map((p) =>
|
|
270
|
-
`<li>${link(p.head)}${p.subs.length
|
|
271
|
-
? `<ol class="primer-toc__sub">${p.subs.map((s) => `<li>${link(s)}</li>`).join('')}</ol>`
|
|
272
|
-
: ''}</li>`
|
|
273
|
-
).join('');
|
|
274
|
-
const flat = [];
|
|
275
|
-
for (const p of parts) {
|
|
276
|
-
flat.push({ id: p.head.id, parent: null, part: p.head.text });
|
|
277
|
-
for (const s of p.subs) flat.push({ id: s.id, parent: p.head.id, part: p.head.text });
|
|
278
|
-
}
|
|
279
|
-
return flat;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// The active entry is the LAST heading whose top has scrolled past the
|
|
283
|
-
// reading line, so it stays put through a section's paragraphs. Its part, when
|
|
284
|
-
// the active entry is an h3, is the trail; the toggle's middle names the part.
|
|
285
|
-
function setupScrollspy(sections) {
|
|
286
|
-
if (sections.length === 0) return;
|
|
287
|
-
const links = new Map();
|
|
288
|
-
for (const a of document.querySelectorAll('.primer-toc__list a')) links.set(a.dataset.section, a);
|
|
289
|
-
if (links.size === 0) return;
|
|
290
|
-
const current = $('#primer-toc-current');
|
|
291
|
-
let last = null;
|
|
292
|
-
function update() {
|
|
293
|
-
let active = null;
|
|
294
|
-
for (const s of sections) {
|
|
295
|
-
const el = document.getElementById(s.id);
|
|
296
|
-
if (!el) continue;
|
|
297
|
-
if (el.getBoundingClientRect().top <= READING_LINE_Y) active = s; else break;
|
|
298
|
-
}
|
|
299
|
-
const id = active ? active.id : null;
|
|
300
|
-
if (id === last) return;
|
|
301
|
-
last = id;
|
|
302
|
-
const trail = active && active.parent;
|
|
303
|
-
for (const [sid, a] of links) {
|
|
304
|
-
a.classList.toggle('is-active', sid === id);
|
|
305
|
-
a.classList.toggle('is-trail', !!trail && sid === trail);
|
|
306
|
-
}
|
|
307
|
-
if (current) current.textContent = active ? active.part : '';
|
|
308
|
-
}
|
|
309
|
-
window.addEventListener('scroll', update, { passive: true });
|
|
310
|
-
window.addEventListener('resize', update, { passive: true });
|
|
311
|
-
update();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Every heading is an address. The link is the heading's own id, so it works
|
|
315
|
-
// with the browser's hash navigation and no script; the click additionally
|
|
316
|
-
// copies the absolute address, so a reader can hand a colleague the exact
|
|
317
|
-
// answer, which is the reason a reference is deep-linked at all.
|
|
318
|
-
function addAnchors(article) {
|
|
319
|
-
for (const h of article.querySelectorAll('h2[id], h3[id], h4[id]')) {
|
|
320
|
-
const a = document.createElement('a');
|
|
321
|
-
a.className = 'room-anchor';
|
|
322
|
-
a.href = `#${h.id}`;
|
|
323
|
-
a.setAttribute('aria-label', `Link to this section: ${h.textContent.trim()}`);
|
|
324
|
-
a.innerHTML = LINK_ICON;
|
|
325
|
-
h.appendChild(a);
|
|
326
|
-
}
|
|
327
|
-
article.addEventListener('click', (e) => {
|
|
328
|
-
const a = e.target.closest('a.room-anchor');
|
|
329
|
-
if (!a) return;
|
|
330
|
-
if (!(navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext)) return;
|
|
331
|
-
const url = new URL(a.getAttribute('href'), window.location.href).href;
|
|
332
|
-
navigator.clipboard.writeText(url).then(() => toast('Link copied.'), () => { /* the hash still navigated */ });
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// A deep link arrives before the article exists, so the browser's own jump
|
|
337
|
-
// finds nothing and its :target was resolved against nothing. Once the
|
|
338
|
-
// document and the live section are in the DOM the target is scrolled into
|
|
339
|
-
// view (scroll-margin-top clears the top bar) and marked (.is-target) until
|
|
340
|
-
// the next hash change hands the mark back to :target.
|
|
341
|
-
function landOnHash() {
|
|
342
|
-
let id = '';
|
|
343
|
-
// a malformed percent-escape in the fragment throws; there is nothing to land on
|
|
344
|
-
try { id = decodeURIComponent(window.location.hash.slice(1)); } catch (_) { return; }
|
|
345
|
-
const el = id && document.getElementById(id);
|
|
346
|
-
if (!el) return;
|
|
347
|
-
el.classList.add('is-target');
|
|
348
|
-
el.scrollIntoView({ block: 'start' });
|
|
349
|
-
window.addEventListener('hashchange', () => el.classList.remove('is-target'), { once: true });
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function setupTocToggle() {
|
|
353
|
-
const toc = $('#primer-toc');
|
|
354
|
-
const btn = $('#primer-toc-toggle');
|
|
355
|
-
if (!toc || !btn) return;
|
|
356
|
-
btn.addEventListener('click', () => {
|
|
357
|
-
const open = toc.classList.toggle('is-open');
|
|
358
|
-
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
359
|
-
});
|
|
360
|
-
toc.addEventListener('click', (e) => {
|
|
361
|
-
const a = e.target.closest('a[data-section]');
|
|
362
|
-
if (!a) return;
|
|
363
|
-
if (window.matchMedia('(max-width: 760px)').matches) {
|
|
364
|
-
toc.classList.remove('is-open');
|
|
365
|
-
btn.setAttribute('aria-expanded', 'false');
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// The three panels the article can resolve into (the primer's show()).
|
|
371
|
-
function show(which) {
|
|
372
|
-
for (const [id, on] of [['#primer-skeleton', which === 'loading'],
|
|
373
|
-
['#primer-article', which === 'article'],
|
|
374
|
-
['#primer-error', which === 'error']]) {
|
|
375
|
-
const el = $(id);
|
|
376
|
-
if (el) el.hidden = !on;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async function boot() {
|
|
381
|
-
const article = $('#primer-article');
|
|
382
|
-
if (!article || !window.OTBMdReader) return;
|
|
383
|
-
show('loading');
|
|
384
|
-
let md;
|
|
385
|
-
try {
|
|
386
|
-
const [mdRes] = await Promise.all([
|
|
387
|
-
fetch(DOC_URL, { credentials: 'same-origin' }),
|
|
388
|
-
loadRepoInfoBases(),
|
|
389
|
-
]);
|
|
390
|
-
if (!mdRes.ok) throw new Error(`${DOC_URL} → ${mdRes.status}`);
|
|
391
|
-
md = await mdRes.text();
|
|
392
|
-
} catch (err) {
|
|
393
|
-
console.error('[drachmae] failed to load doc', err);
|
|
394
|
-
article.removeAttribute('aria-busy');
|
|
395
|
-
show('error');
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
const resolveLinkHref = window.OTBMdReader.makeResolveLinkHref({
|
|
399
|
-
baseDir: DOC_DIR,
|
|
400
|
-
getBlobBase: () => repoBlobBase,
|
|
401
|
-
getTreeBase: () => repoTreeBase,
|
|
402
|
-
diagramsHref: '/builders/diagrams',
|
|
403
|
-
});
|
|
404
|
-
const parsed = window.OTBMdReader.parseMarkdown(md, resolveLinkHref);
|
|
405
|
-
// The doc's leading "# …" is the same page name .page-head--room already
|
|
406
|
-
// renders. Two <h1>s fails one-header-main-h1, and the page head is the one
|
|
407
|
-
// that exists before the fetch and survives its failure (task 1003307).
|
|
408
|
-
article.innerHTML = parsed.html.replace(/^\s*<h1\b[^>]*>[\s\S]*?<\/h1>\s*/, '');
|
|
409
|
-
article.removeAttribute('aria-busy');
|
|
410
|
-
show('article');
|
|
411
|
-
// Awaited before the outline is built so both live sections get their
|
|
412
|
-
// entries and scrollspy targets; the article itself is already on screen by
|
|
413
|
-
// now. The rate card goes in FIRST so the treasury can land ahead of it by
|
|
414
|
-
// id — the doc then reads prose, then what actually happened, then the
|
|
415
|
-
// published rates, then "dig deeper".
|
|
416
|
-
await insertBountySection(article);
|
|
417
|
-
await insertTreasurySection(article);
|
|
418
|
-
addAnchors(article);
|
|
419
|
-
setupTocToggle();
|
|
420
|
-
setupScrollspy(renderToc(collectHeadings(article)));
|
|
421
|
-
// After the fonts: a swap after the jump moves everything above the target
|
|
422
|
-
// (measured: 300px short on a phone, past it on a desktop).
|
|
423
|
-
(document.fonts ? document.fonts.ready : Promise.resolve()).then(landOnHash);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (document.readyState === 'loading') {
|
|
427
|
-
document.addEventListener('DOMContentLoaded', boot);
|
|
428
|
-
} else {
|
|
429
|
-
boot();
|
|
430
|
-
}
|
|
431
|
-
})();
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_": "Drachmae & karma — the hall's first REFERENCE room (task 1003473): the reading-room archetype (room.css) with the reference rules on (`room--reference`). Public (no page gate), so the signed-out render is the one most readers get. Measured at three widths in both modes rather than looked at once.",
|
|
3
|
-
"page": "/builders/drachmae",
|
|
4
|
-
"surface": "hall-ui",
|
|
5
|
-
"stub": {
|
|
6
|
-
"prefix": "/builders/"
|
|
7
|
-
},
|
|
8
|
-
"modeQuery": false,
|
|
9
|
-
"_feeds": "The kit's own stub serves neither the markdown nor the bounty feed, so against it the page renders its SKELETON and then its FAILURE note — states the floors must hold in. The POPULATED document, the outline, the anchors and the live table need the hall-preview harness (node scripts/hall-preview/server.js --port 4620 --fixture-me, then this file with --base-in/--base-out pointed at it): it serves docs/onboarding/drachmae-and-karma.md and the checked-in public__bounty-table.json. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
|
|
10
|
-
"ignoreRequests": [
|
|
11
|
-
"/api/(gds|bongos)/me$",
|
|
12
|
-
"/api/(gds|bongos)/me/",
|
|
13
|
-
"/api/(gds|bongos)/goals",
|
|
14
|
-
"/api/(gds|bongos)/inbox/",
|
|
15
|
-
"/api/(gds|bongos)/versions"
|
|
16
|
-
],
|
|
17
|
-
"ignoreConsole": [
|
|
18
|
-
"/api/(gds|bongos)/",
|
|
19
|
-
"\\b401\\b",
|
|
20
|
-
"\\b403\\b",
|
|
21
|
-
"\\b404\\b",
|
|
22
|
-
"\\b501\\b",
|
|
23
|
-
"Unauthorized",
|
|
24
|
-
"Forbidden",
|
|
25
|
-
"Not Found",
|
|
26
|
-
"Not Implemented",
|
|
27
|
-
"load failed"
|
|
28
|
-
],
|
|
29
|
-
"_expect": "read pins the article, so this file is run against the harness (the kit stub can only ever show the failure note). jump is the deep link: the browser's own hash jump finds nothing on a page whose document arrives after load, so drachmae.js lands it, and the outline's lit entry with its trail, plus the phone toggle naming the part, are what the shot proves.",
|
|
30
|
-
"states": {
|
|
31
|
-
"read": {
|
|
32
|
-
"auth": true,
|
|
33
|
-
"actions": [["wait", 600]],
|
|
34
|
-
"expect": { "visible": ["#primer-article", "#primer-toc"] }
|
|
35
|
-
},
|
|
36
|
-
"toc-open": {
|
|
37
|
-
"auth": true,
|
|
38
|
-
"actions": [["clickIfVisible", "#primer-toc-toggle"], ["wait", 250]],
|
|
39
|
-
"_": "the room's one disclosure. Below 760px the contents collapse behind this toggle; above it the toggle is not rendered at all, so clickIfVisible is the right verb and the state is a no-op at 1440.",
|
|
40
|
-
"expect": { "visible": ["#primer-toc"] }
|
|
41
|
-
},
|
|
42
|
-
"jump": {
|
|
43
|
-
"auth": true,
|
|
44
|
-
"url": "/builders/drachmae#how-karma-is-earned-and-lost",
|
|
45
|
-
"actions": [["wait", 700]],
|
|
46
|
-
"_": "a deep link to an h3 under Part 3. At 1440 the outline shows the entry lit and its part in the trail; at 390 the collapsed toggle reads the part's name.",
|
|
47
|
-
"expect": { "visible": ["#primer-article", "#primer-toc"] }
|
|
48
|
-
},
|
|
49
|
-
"out": {
|
|
50
|
-
"auth": false,
|
|
51
|
-
"actions": [["wait", 600]]
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
"reducedMotion": {
|
|
55
|
-
"auth": true,
|
|
56
|
-
"actions": [["wait", 600]]
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_": "The founding mark (task 1004424) on the Ranks page: the Founding builders ledger (builder, rank, mark) a founded project shows, from the kit fixture founding-builders-sample.json. The page itself is ranks.states.json's; only the founding read differs. The never-founded case is ranks.states.json, where the section stays hidden.",
|
|
3
|
-
"page": "/builders/ranks",
|
|
4
|
-
"surface": "hall-ui",
|
|
5
|
-
"stub": { "prefix": "/builders/", "fixtures": { "founding-builders": "founding-builders-sample" } },
|
|
6
|
-
"modeQuery": false,
|
|
7
|
-
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox/", "/api/(gds|bongos)/me/"],
|
|
8
|
-
"ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized", "404 \\(Not Found\\)", "goals load failed"],
|
|
9
|
-
"states": {
|
|
10
|
-
"founding": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#founding-scroll"] } }
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Ranks — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="The ranks a builder of {{worldName}} can hold, what each unlocks, and how to advance.">
|
|
8
|
-
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
-
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
-
</head>
|
|
13
|
-
<body data-page="ranks" data-page-title="Ranks">
|
|
14
|
-
|
|
15
|
-
<div class="app">
|
|
16
|
-
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
17
|
-
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
18
|
-
<div class="app-body">
|
|
19
|
-
<header class="app-topbar" id="app-topbar"></header>
|
|
20
|
-
<div class="app-content">
|
|
21
|
-
<!-- The page's one h1, for screen readers: no page shows its head (task
|
|
22
|
-
1004836), so its eyebrow and sub-line went in task 1004828. -->
|
|
23
|
-
<div class="page-head page-head--room">
|
|
24
|
-
<h1 class="page-head__h">Ranks</h1>
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<main id="hall-main">
|
|
28
|
-
<!-- Current rank summary — filled by ranks.js once /me resolves. -->
|
|
29
|
-
<section class="scroll" id="standing-scroll" aria-labelledby="standing-h">
|
|
30
|
-
<h2 id="standing-h" class="scroll__h">Where you stand</h2>
|
|
31
|
-
<div class="ranks-standing" id="standing-body">
|
|
32
|
-
<div class="hall-loading" role="status">Loading…</div>
|
|
33
|
-
</div>
|
|
34
|
-
</section>
|
|
35
|
-
|
|
36
|
-
<!-- The founding builders (task 1004424) — everyone who joined while the
|
|
37
|
-
project was founding, with their mark beside their rank. Hidden, and
|
|
38
|
-
never fetched into view, on a project that was never founded. -->
|
|
39
|
-
<section class="scroll" id="founding-scroll" aria-labelledby="founding-h" hidden>
|
|
40
|
-
<h2 id="founding-h" class="scroll__h">Founding builders</h2>
|
|
41
|
-
<p class="scroll__lede">The founding mark is numbered in the order people joined while the project was founded. It is kept for good and grants nothing.</p>
|
|
42
|
-
<div id="founding-body"></div>
|
|
43
|
-
</section>
|
|
44
|
-
|
|
45
|
-
<!-- The ladder — one card per rank. Current + next are shown in full;
|
|
46
|
-
ranks beyond the next keep their unlock details hidden until the
|
|
47
|
-
builder is one rank below them. -->
|
|
48
|
-
<section class="scroll" id="ladder-scroll" aria-labelledby="ladder-h">
|
|
49
|
-
<h2 id="ladder-h" class="scroll__h">The ranks, lowest to highest</h2>
|
|
50
|
-
<p class="scroll__lede">Each rank opens more of the system. Yours and the one above are shown in full; the rest open as you get closer.</p>
|
|
51
|
-
<ol class="ranks-ladder" id="ladder-body">
|
|
52
|
-
<li class="hall-loading" role="status">Loading…</li>
|
|
53
|
-
</ol>
|
|
54
|
-
</section>
|
|
55
|
-
|
|
56
|
-
<!-- How promotion works — filled in by ranks.js, tailored to the next rank. -->
|
|
57
|
-
<section class="scroll" id="rise-scroll" aria-labelledby="rise-h">
|
|
58
|
-
<h2 id="rise-h" class="scroll__h">How you advance</h2>
|
|
59
|
-
<div class="ranks-rise" id="rise-body">
|
|
60
|
-
<div class="hall-loading" role="status">Loading…</div>
|
|
61
|
-
</div>
|
|
62
|
-
</section>
|
|
63
|
-
</main>
|
|
64
|
-
|
|
65
|
-
<footer class="app-foot" id="app-foot"></footer>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
71
|
-
|
|
72
|
-
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
73
|
-
chrome exists before the page script runs. -->
|
|
74
|
-
<script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
|
|
75
|
-
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
76
|
-
<script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
|
|
77
|
-
<script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
|
|
78
|
-
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
79
|
-
<script src="/builders/palette.js" defer></script>
|
|
80
|
-
<script src="/builders/founding-mark.js?v=2026-09-30-founding"></script>
|
|
81
|
-
<script src="/builders/ranks.js?v=2026-09-30-founding"></script>
|
|
82
|
-
</body>
|
|
83
|
-
</html>
|