@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.
Files changed (98) hide show
  1. package/.bongos-core.json +101 -146
  2. package/.claude/skills/merge-mode/SKILL.md +1 -1
  3. package/docs/adr/0228-the-reading-room-owes-a-reference-content-rules-not-a-second-archetype.md +1 -1
  4. package/docs/copy-inventory.md +136 -192
  5. package/docs/copy-registry.json +186 -752
  6. package/docs/module-api-changelog.md +2 -0
  7. package/docs/onboarding/diagrams/README.md +1 -2
  8. package/docs/onboarding/primer.md +64 -22
  9. package/docs/page-inventory.json +7 -101
  10. package/docs/page-readings.json +947 -1415
  11. package/docs/recipes/ui-look-before-you-ship.md +2 -2
  12. package/modules/design-styles/skills/style/SKILL.md +2 -2
  13. package/modules/hall-ui/public/ai-studio.html +2 -2
  14. package/modules/hall-ui/public/atlas.html +2 -2
  15. package/modules/hall-ui/public/blockers.html +2 -2
  16. package/modules/hall-ui/public/board-room.html +2 -2
  17. package/modules/hall-ui/public/builders.js +8 -6
  18. package/modules/hall-ui/public/city.html +2 -2
  19. package/modules/hall-ui/public/collab.html +2 -2
  20. package/modules/hall-ui/public/commands.html +2 -2
  21. package/modules/hall-ui/public/copy-desk.html +2 -2
  22. package/modules/hall-ui/public/craft-mode.js +1 -1
  23. package/modules/hall-ui/public/deploy.html +2 -2
  24. package/modules/hall-ui/public/diagram-viewer.css +1 -1
  25. package/modules/hall-ui/public/diagram-viewer.js +5 -5
  26. package/modules/hall-ui/public/fleet.html +2 -2
  27. package/modules/hall-ui/public/founding-mark.js +3 -2
  28. package/modules/hall-ui/public/gate.html +2 -2
  29. package/modules/hall-ui/public/goal-map.html +2 -2
  30. package/modules/hall-ui/public/goals.html +2 -2
  31. package/modules/hall-ui/public/government.html +2 -2
  32. package/modules/hall-ui/public/hall-render.js +62 -1
  33. package/modules/hall-ui/public/hall-widgets.js +11 -1
  34. package/modules/hall-ui/public/idea.html +2 -2
  35. package/modules/hall-ui/public/ideas.html +2 -2
  36. package/modules/hall-ui/public/index.html +4 -4
  37. package/modules/hall-ui/public/market-watch-item.html +2 -2
  38. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  39. package/modules/hall-ui/public/market-watch.html +2 -2
  40. package/modules/hall-ui/public/modules.html +2 -2
  41. package/modules/hall-ui/public/my-agents.html +2 -2
  42. package/modules/hall-ui/public/palette.js +15 -0
  43. package/modules/hall-ui/public/primer-live.js +188 -0
  44. package/modules/hall-ui/public/primer.html +11 -6
  45. package/modules/hall-ui/public/primer.js +95 -23
  46. package/modules/hall-ui/public/primer.states.json +54 -7
  47. package/modules/hall-ui/public/profile.html +2 -2
  48. package/modules/hall-ui/public/project-settings.html +2 -2
  49. package/modules/hall-ui/public/roadmap.html +2 -2
  50. package/modules/hall-ui/public/room.css +23 -121
  51. package/modules/hall-ui/public/roster.html +2 -2
  52. package/modules/hall-ui/public/sessions.html +2 -2
  53. package/modules/hall-ui/public/settings.html +2 -2
  54. package/modules/hall-ui/public/shell.js +14 -21
  55. package/modules/hall-ui/public/street.html +2 -2
  56. package/modules/hall-ui/public/studio.html +2 -2
  57. package/modules/hall-ui/public/style.css +8 -55
  58. package/modules/hall-ui/public/task.html +2 -2
  59. package/modules/hall-ui/public/thinking.html +2 -2
  60. package/modules/hall-ui/public/tweak-editor.html +2 -2
  61. package/modules/hall-ui/public/watch.html +2 -2
  62. package/modules/hall-ui/public/work.html +2 -2
  63. package/modules/hall-ui/records/founding-mark.md +4 -4
  64. package/modules/hall-ui/records/reading-rooms.md +2 -0
  65. package/modules/hall-ui/records/reference-room.md +2 -0
  66. package/modules/onboarding/founding-mode.js +6 -2
  67. package/modules/ui-design/kit/render.js +1 -1
  68. package/modules/ui-design/styles/README.md +2 -2
  69. package/package-lock.json +2 -2
  70. package/package.json +1 -1
  71. package/release-notes.json +6 -0
  72. package/scripts/bongos/gen-hall-sitemap.js +3 -0
  73. package/scripts/hall-preview/server.js +6 -11
  74. package/src/bongos/serve-internal.js +28 -37
  75. package/src/module-api.js +1 -1
  76. package/tests/craft_mode_home.mjs +6 -2
  77. package/tests/founding_builders.mjs +6 -5
  78. package/tests/founding_mode.mjs +7 -4
  79. package/tests/hall_board_world.mjs +44 -1
  80. package/tests/hall_fullscreen.mjs +1 -1
  81. package/tests/hall_nav.mjs +24 -7
  82. package/tests/hall_page_gate_map.mjs +14 -5
  83. package/tests/hall_reading_room.mjs +426 -0
  84. package/tests/hall_reading_world.mjs +36 -30
  85. package/tests/nav_permission_atoms.mjs +3 -2
  86. package/tests/rank_tier_single_source.mjs +0 -1
  87. package/tests/ui_design_kit.mjs +1 -1
  88. package/modules/hall-ui/public/diagrams.css +0 -45
  89. package/modules/hall-ui/public/diagrams.html +0 -106
  90. package/modules/hall-ui/public/diagrams.states.json +0 -28
  91. package/modules/hall-ui/public/drachmae.html +0 -110
  92. package/modules/hall-ui/public/drachmae.js +0 -431
  93. package/modules/hall-ui/public/drachmae.states.json +0 -58
  94. package/modules/hall-ui/public/ranks-founding.states.json +0 -12
  95. package/modules/hall-ui/public/ranks.html +0 -83
  96. package/modules/hall-ui/public/ranks.js +0 -229
  97. package/modules/hall-ui/public/ranks.states.json +0 -15
  98. package/tests/hall_reference_world.mjs +0 -352
@@ -1,229 +0,0 @@
1
- // public-builders/ranks.js — the ranks page (/builders/ranks).
2
- //
3
- // Read-only. Fetches /api/bongos/me to learn the viewer's rank, then renders the
4
- // rank ladder with the "reveal only as far as the next rank" rule: the current
5
- // rank and the one directly above it show their full unlocks; higher ranks
6
- // keep their unlock details hidden until the builder is one rank below them
7
- // (named, but details withheld). On a 401 it redirects into the web sign-in
8
- // flow, exactly like the hall index — the page personalises to the viewer, so
9
- // it needs a session.
10
- //
11
- // The rank model is the four live ranks of ADR 0018 + ADR 0034:
12
- // xenos < thetes < metic < archon. RANK_UNLOCKS is mirrored from builders.js /
13
- // docs/canonical-permissions.md.
14
-
15
- (() => {
16
- 'use strict';
17
-
18
- const API = '/api/bongos';
19
- // Generated typed client (task 1996 / R09), served as window.BongosClient by
20
- // /lib/bongos-client.js. baseUrl:'' passes the full `${API}/...` URLs below
21
- // through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
22
- const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
23
-
24
- // escapeHtml / $ / rankLabel / toast now come from the shared window.OTB
25
- // toolkit (dom-utils.js, loaded first). Thin wrappers keep this file's exact
26
- // variants ('—' rankLabel empty; a 2600ms clearing toast). badge() +
27
- // RANK_UNLOCKS + RANK_LADDER stay local below.
28
- const { escapeHtml, $ } = window.OTB;
29
- const rankLabel = (rank) => window.OTB.rankLabel(rank);
30
- const toast = (msg) => window.OTB.toast(msg, { duration: 2600, clear: true });
31
- const curLabel = (caps) => { const l = (window.__BRANDING__ && window.__BRANDING__.currency && window.__BRANDING__.currency.label) || 'credits'; return caps ? l.charAt(0).toUpperCase() + l.slice(1) : l; };
32
-
33
- // Lowest → highest. Index is the rung number.
34
- const RANK_LADDER = ['xenos', 'thetes', 'metic', 'archon'];
35
-
36
- // Plain title + one-line character for each rank.
37
- const RANK_TITLE = {
38
- xenos: 'The newcomer',
39
- thetes: 'The proven builder',
40
- metic: 'The trusted builder',
41
- archon: 'The steward',
42
- };
43
- const RANK_BLURB = {
44
- xenos: 'A newcomer with a deliberately small surface: onboarding, safe starter tasks, and your own standing.',
45
- thetes: 'A builder who has shipped their first work — the day-to-day surfaces are open.',
46
- metic: 'A trusted builder: triage, reviews, planning sessions, and security reporting are open.',
47
- archon: 'The steward of the whole system — approvals, the roster, budgets, and the Archon pages.',
48
- };
49
-
50
- // What each rank unlocks. Mirror of builders.js RANK_UNLOCKS — sourced from
51
- // docs/canonical-permissions.md. Keep the two in sync.
52
- const RANK_UNLOCKS = {
53
- xenos: [
54
- 'Read every public page.',
55
- 'Claim and ship tasks an Archon has flagged newcomer-friendly.',
56
- `Earn ${curLabel()} for shipped work and karma for ratified ideas.`,
57
- ],
58
- thetes: [
59
- 'Claim and ship any task in the general queue — no longer only newcomer-friendly work.',
60
- `Keep earning ${curLabel()} and karma for every clean ship.`,
61
- 'No triage, blockers, planning, or task-shaping yet — those levers open at Metic.',
62
- ],
63
- metic: [
64
- 'Claim and ship any task in the ready queue.',
65
- 'Triage the idea inbox; resolve blockers; run planning sessions.',
66
- 'Propose new version criteria for Archon ratification.',
67
- ],
68
- archon: [
69
- 'Create, promote, and edit tasks; reshape the dependency graph.',
70
- 'Grant or revoke ranks; close versions; confirm ships past the grader.',
71
- 'Adjudicate vulnerability reports and override requests.',
72
- ],
73
- };
74
-
75
- // How a builder rises FROM their current rank. Keyed by current rank; the
76
- // value describes the climb to the next rung. Archon is the top — no climb.
77
- // NB: these strings INTENTIONALLY contain markup (<strong>) and are injected
78
- // via innerHTML below without escaping. That is safe only because they are
79
- // author-controlled constants. Never source RISE_FROM from server/user data
80
- // without switching the render site to escapeHtml — it would become XSS.
81
- const RISE_FROM = {
82
- xenos:
83
- `Claim and ship the newcomer-friendly tasks an Archon has opened to newcomers. Each clean ship earns ${curLabel()}; each ratified idea earns karma. On your <strong>third</strong> clean ship you are promoted automatically to <strong>Thetes</strong> — out of the newcomer sandbox, with the whole queue open to you. No fee, no application; the third ship does it.`,
84
- thetes:
85
- 'You have the run of the queue. Promotion to <strong>Metic</strong> — triage, blockers, planning, the shaping of work — is granted by a standing Archon alone, to a builder whose judgment has earned the project’s trust. Keep shipping; rank is granted, never bought.',
86
- metic:
87
- 'The <strong>Archon</strong> rank is granted rarely, by a standing Archon alone, to a Metic who has shaped the project’s direction and earned its deepest trust. There is no checklist — only the weight of what you have built.',
88
- archon:
89
- 'You hold the highest rank. There is no rank above it — only the care of everything below.',
90
- };
91
-
92
- function badge(rank) {
93
- const variant = RANK_LADDER.includes(rank) ? `rank-badge--${rank}` : 'rank-badge--unknown';
94
- return `<span class="rank-badge ${variant}">${escapeHtml(rankLabel(rank))}</span>`;
95
- }
96
-
97
- function unlockList(rank) {
98
- const items = (RANK_UNLOCKS[rank] || []).map((u) => `<li>${escapeHtml(u)}</li>`).join('');
99
- return `<ul class="rank-card__list">${items}</ul>`;
100
- }
101
-
102
- // Render one revealed rank. `state` is 'attained' | 'current' | 'next'.
103
- function revealedCard(rank, state) {
104
- const statusText = {
105
- attained: 'Attained',
106
- current: 'You are here',
107
- next: 'Next rank',
108
- }[state];
109
- return `
110
- <li class="rank-card rank-card--${state}">
111
- <div class="rank-card__head">
112
- ${badge(rank)}
113
- <span class="rank-card__status rank-card__status--${state}">${statusText}</span>
114
- </div>
115
- <p class="rank-card__title">${escapeHtml(RANK_TITLE[rank] || rankLabel(rank))}</p>
116
- <p class="rank-card__blurb">${escapeHtml(RANK_BLURB[rank] || '')}</p>
117
- <p class="rank-card__unlocks-label">What it unlocks</p>
118
- ${unlockList(rank)}
119
- </li>`;
120
- }
121
-
122
- // Render one hidden rank — named, but its details withheld until the builder
123
- // is one rank below it. (Per Lars: show the name, keep the unlocks a mystery.)
124
- function sealedCard(rank) {
125
- return `
126
- <li class="rank-card rank-card--sealed">
127
- <div class="rank-card__head">
128
- ${badge(rank)}
129
- <span class="rank-card__status rank-card__status--sealed">Hidden</span>
130
- </div>
131
- <p class="rank-card__title">${escapeHtml(RANK_TITLE[rank] || rankLabel(rank))}</p>
132
- <p class="rank-card__blurb rank-card__blurb--sealed">A higher rank. Its details are hidden until you reach the rank directly below it.</p>
133
- </li>`;
134
- }
135
-
136
- function render(builder) {
137
- const rank = (builder && RANK_LADDER.includes(builder.rank)) ? builder.rank : 'xenos';
138
- const curIdx = RANK_LADDER.indexOf(rank);
139
- const nextIdx = curIdx < RANK_LADDER.length - 1 ? curIdx + 1 : -1;
140
- // Reveal everything up to and including the next rung; seal what's above.
141
- const revealUpTo = Math.max(curIdx, nextIdx);
142
-
143
- // No header sub-line: no page shows its head (task 1004836), and "Where
144
- // you stand" below already names the rank and its title (task 1004828).
145
-
146
- // ---- "where you stand" summary ----
147
- const standing = $('#standing-body');
148
- if (standing) {
149
- const nextLine = nextIdx >= 0
150
- ? `The rank above yours is ${badge(RANK_LADDER[nextIdx])}. See below what it grants.`
151
- : 'You hold the highest rank.';
152
- standing.innerHTML = `
153
- <div class="ranks-standing__badge">${badge(rank)}</div>
154
- <div class="ranks-standing__text">
155
- <p class="ranks-standing__title">${escapeHtml(RANK_TITLE[rank] || rankLabel(rank))}</p>
156
- <p class="ranks-standing__blurb">${escapeHtml(RANK_BLURB[rank] || '')}</p>
157
- <p class="ranks-standing__next">${nextLine}</p>
158
- </div>`;
159
- }
160
-
161
- // ---- the ladder ----
162
- const ladder = $('#ladder-body');
163
- if (ladder) {
164
- ladder.innerHTML = RANK_LADDER.map((r, i) => {
165
- if (i > revealUpTo) return sealedCard(r);
166
- if (i === curIdx) return revealedCard(r, 'current');
167
- if (i === nextIdx) return revealedCard(r, 'next');
168
- return revealedCard(r, 'attained');
169
- }).join('');
170
- }
171
-
172
- // ---- how you advance ----
173
- const rise = $('#rise-body');
174
- if (rise) {
175
- rise.innerHTML = `<p class="ranks-rise__body">${RISE_FROM[rank] || RISE_FROM.xenos}</p>`;
176
- }
177
- }
178
-
179
- async function load() {
180
- let payload;
181
- try {
182
- const res = await api.request('GET', `${API}/me`);
183
- if (res.status === 401) {
184
- window.location.assign(`${API}/auth/web/start`);
185
- return;
186
- }
187
- if (!res.ok) throw new Error(`me → ${res.status}`);
188
- payload = res.data;
189
- } catch (err) {
190
- console.error('[ranks] failed to load standing', err);
191
- // The message sits where the rank would have been — the head that used
192
- // to carry it is hidden (task 1004828).
193
- const standing = $('#standing-body');
194
- if (standing) standing.innerHTML = '<div class="profile__placeholder">Could not load your rank. Reload to try again.</div>';
195
- toast('Could not load the ranks.');
196
- return;
197
- }
198
- // /me may return the builder under `.builder` or at the top level.
199
- const builder = (payload && payload.builder) ? payload.builder : payload;
200
- render(builder || {});
201
- renderFounding();
202
- }
203
-
204
- // ---- the founding builders (task 1004424) ----
205
- // A ledger of everyone who joined while the project was founding: builder, rank,
206
- // and the mark. The section stays hidden when nobody carries one, so a project
207
- // that was never founded shows today's page.
208
- function renderFounding() {
209
- const section = $('#founding-scroll');
210
- const body = $('#founding-body');
211
- if (!section || !body || !window.OTBFounding) return;
212
- window.OTBFounding.load().then((list) => {
213
- if (!list.length) return;
214
- const rows = list.map((m) => {
215
- const name = escapeHtml(m.display_name || m.github_login || 'builder');
216
- return `<tr>
217
- <td><a class="founding-name" href="/profile?id=${encodeURIComponent(m.builder_id)}">${name}</a>
218
- <span class="founding-line">${escapeHtml(window.OTBFounding.joinedLine(m))}</span></td>
219
- <td>${badge(m.rank)}</td>
220
- <td>${window.OTBFounding.chipHtml(m)}</td>
221
- </tr>`;
222
- }).join('');
223
- body.innerHTML = `<table class="ledger"><thead><tr><th>Builder</th><th>Rank</th><th>Mark</th></tr></thead><tbody>${rows}</tbody></table>`;
224
- section.hidden = false;
225
- });
226
- }
227
-
228
- load();
229
- })();
@@ -1,15 +0,0 @@
1
- {
2
- "_": "The hall's Ranks page as the SECOND surface through modules/ui-design/kit/render.js (task 1003320): the hall serves under /builders/, signed out and in.",
3
- "page": "/builders/ranks",
4
- "surface": "hall-ui",
5
- "stub": { "prefix": "/builders/" },
6
- "modeQuery": false,
7
- "_feeds": "The hall's shell polls goals + inbox + profile feeds on every page; the stub fixtures only the public feeds, so those 404s are declared here, not hidden by the kit. (The 24px-floor and h1 findings this file used to carry in the open are FIXED, not silenced: the badge grew to 24px in task 1003304, and the page got its <h1> with the reading-room re-skin in task 1003306. The render is clean; nothing here ignores a floor.)",
8
- "ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox/", "/api/(gds|bongos)/me/"],
9
- "ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized", "404 \\(Not Found\\)", "goals load failed"],
10
- "states": {
11
- "out": { "auth": false },
12
- "in": { "auth": true }
13
- },
14
- "reducedMotion": { "auth": true }
15
- }
@@ -1,352 +0,0 @@
1
- // tests/hall_reference_world.mjs — the REFERENCE room in the chrome world:
2
- // drachmae, and what the reading-room archetype owes a second document
3
- // (task 1003473, goal 1000081).
4
- //
5
- // tests/hall_reading_world.mjs pins the reading rooms (the primer, the diagrams
6
- // index, the deck, the ladder); this is the same job for the one document
7
- // people SCAN rather than read. Everything here is a fact that is silent in a
8
- // browser: the page still renders, it just renders the primer's instruments on
9
- // a document that has no end to reach, or an outline that stops one level
10
- // short of the questions people came with.
11
- //
12
- // What it locks:
13
- // * ONE ROOM, ONE MODIFIER. drachmae opts into `room room--reference` and
14
- // loads room.css alone. primer.css holds only the acknowledge gate and
15
- // was loaded here for nothing; the reference rules live in room.css §8, so
16
- // the next scanned document takes them by one body class.
17
- // * THE LINEAR-READ INSTRUMENTS ARE OFF. No progress hairline, no section
18
- // counters, no "About a five-minute read" — and the primer keeps all three.
19
- // * THE OUTLINE GOES TO h3 DEPTH and lights the entry AND its part.
20
- // * EVERY HEADING IS AN ADDRESS. `.room-anchor` on h2/h3/h4, a 24px box of
21
- // its own, copying the absolute link on click; a deep link lands after the
22
- // async render, or the browser's own jump finds nothing.
23
- // * THE PARSER REACHES h4, and a heading that carries a link slugs by its
24
- // text. The economy doc's six earning sources are `####`, and the shared
25
- // reader stopped at `###`, so they rendered as paragraphs with a literal
26
- // "####" — the largest defect on the page, and invisible to every floor.
27
- // * NESTED EMPHASIS RENDERS. "**It's the API *list* price**" showed its
28
- // asterisks because the bold rule refused an inner `*`.
29
- // * THE LIVE TABLE IS A SECTION AHEAD OF THE CLOSING LINKS, not an appendix
30
- // after "dig deeper".
31
- //
32
- // Reproduce the renders these came from (the populated document needs the
33
- // fixture harness; the kit stub serves neither the markdown nor the bounty feed):
34
- // node scripts/hall-preview/server.js --port 4620 --fixture-me
35
- // node modules/ui-design/kit/render.js --states modules/hall-ui/public/drachmae.states.json \
36
- // --base-in http://builders.localhost:4620 --base-out http://builders.localhost:4620
37
-
38
- import { test } from 'node:test';
39
- import assert from 'node:assert/strict';
40
- import fs from 'node:fs';
41
- import path from 'node:path';
42
- import vm from 'node:vm';
43
- import { fileURLToPath } from 'node:url';
44
- import { realOTB } from './helpers.mjs';
45
-
46
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
47
- const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
48
- const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
49
-
50
- const roomCss = read('room.css');
51
- const mdReaderJs = read('md-reader.js');
52
- const drachmaeHtml = read('drachmae.html');
53
- const drachmaeJs = read('drachmae.js');
54
- const drachmaeStates = JSON.parse(read('drachmae.states.json'));
55
- const primerHtml = read('primer.html');
56
- const primerJs = read('primer.js');
57
-
58
- const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
59
- const dehtmlcomment = (html) => html.replace(/<!--[\s\S]*?-->/g, '');
60
- const dejscomment = (js) => js.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
61
- // The body of the first rule matching `selector {`, comments stripped. Takes a
62
- // PLAIN selector and escapes it here.
63
- const rule = (css, selector) => {
64
- const esc = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
65
- const m = decomment(css).match(new RegExp(`(^|[},])\\s*${esc}\\s*\\{([^}]*)\\}`, 'm'));
66
- return m ? m[2] : null;
67
- };
68
-
69
- // md-reader.js is a browser IIFE that only touches `window` and `location`.
70
- function loadReader() {
71
- const sandbox = { window: { OTB: realOTB() }, location: { hostname: 'builders.example.com', href: 'https://builders.example.com/drachmae' } };
72
- vm.createContext(sandbox);
73
- vm.runInContext(mdReaderJs, sandbox, { filename: 'md-reader.js' });
74
- return sandbox.window.OTBMdReader;
75
- }
76
-
77
- // ---- 1. one room, one modifier ----------------------------------------------
78
-
79
- test('drachmae opts into the room and its reference rules, and loads the archetype sheet alone', () => {
80
- assert.match(drachmaeHtml, /<body class="room room--reference"/, 'the reference is the room with one modifier, not a second archetype');
81
- assert.match(drachmaeHtml, /href="\/builders\/room\.css"/, 'the archetype is loaded');
82
- assert.doesNotMatch(dehtmlcomment(drachmaeHtml), /primer\.css/,
83
- 'primer.css holds only the acknowledge gate, which a reference has no use for');
84
- assert.match(drachmaeHtml, /class="page-head page-head--room"/, 'the head keeps the room voice');
85
- });
86
-
87
- test('the reference rules live in room.css, keyed on the body modifier', () => {
88
- const css = decomment(roomCss);
89
- assert.match(css, /\.room--reference \.primer-article h2::before,\s*\.room--reference \.primer-toc__list a::before\s*\{\s*content:\s*none/,
90
- 'the counters are off on a reference');
91
- assert.ok(rule(roomCss, '.room--reference .primer-toc__sub'), 'the nested contents have a rule');
92
- assert.ok(rule(roomCss, '.room--reference .primer-toc__list a.is-trail'), 'the part of the lit entry is the trail');
93
- assert.match(rule(roomCss, '.primer-article h2::before') || '', /counter-increment/,
94
- 'and the primer keeps its counter — the modifier turns it off, the archetype still draws it');
95
- });
96
-
97
- // ---- 2. the linear-read instruments are off ---------------------------------
98
-
99
- test('the reference carries no progress hairline, no acknowledge gate and no read time', () => {
100
- const html = dehtmlcomment(drachmaeHtml);
101
- assert.doesNotMatch(html, /primer-progress/, 'no progress hairline: a reference has no end to reach');
102
- assert.doesNotMatch(dejscomment(drachmaeJs), /primer-progress|setupProgressBar/, 'and the script drives none');
103
- assert.doesNotMatch(html, /primer-ack/, 'no acknowledge gate');
104
- assert.doesNotMatch(html, /minute read/i, 'the sub line says what kind of document this is, not how long it takes');
105
- });
106
-
107
- test('the primer keeps all three — the modifier is drachmae\'s, not the room\'s', () => {
108
- assert.doesNotMatch(primerHtml, /room--reference/);
109
- assert.match(primerHtml, /primer-progress__bar/);
110
- assert.match(primerJs, /function setupProgressBar/, 'the primer\'s gate is fed by its progress');
111
- });
112
-
113
- // ---- 3. the outline -----------------------------------------------------------
114
-
115
- test('the outline is read from the rendered h2s AND h3s and nests them', () => {
116
- assert.match(drachmaeJs, /querySelectorAll\('h2\[id\], h3\[id\]'\)/, 'the contents are collected from the DOM, to h3 depth');
117
- assert.match(drachmaeJs, /class="primer-toc__sub"/, 'h3s nest under their part');
118
- assert.match(drachmaeJs, /classList\.toggle\('is-trail'/, 'the spy marks the lit entry\'s part');
119
- assert.match(drachmaeHtml, /id="primer-toc-current"/, 'the phone toggle has a middle that names the part');
120
- assert.match(drachmaeJs, /current\.textContent = active \? active\.part : ''/, 'and the spy writes it');
121
- });
122
-
123
- test('the toggle\'s chevron is drawn, and the contents are named for a reference', () => {
124
- assert.doesNotMatch(dehtmlcomment(drachmaeHtml), /▾/, 'no glyph standing in for an icon');
125
- assert.match(drachmaeHtml, /<svg class="primer-toc__chevron"/);
126
- assert.match(drachmaeHtml, /id="primer-toc-heading">On this page</, 'a reference is "on this page", not "in this guide"');
127
- });
128
-
129
- // ---- 4. every heading is an address -------------------------------------------
130
-
131
- test('every h2, h3 and h4 gets an anchor that is a real link and copies the address', () => {
132
- assert.match(drachmaeJs, /querySelectorAll\('h2\[id\], h3\[id\], h4\[id\]'\)/);
133
- assert.match(drachmaeJs, /className = 'room-anchor'/);
134
- assert.match(drachmaeJs, /a\.href = `#\$\{h\.id\}`/, 'the anchor is the heading\'s own id, so it works with no script');
135
- assert.match(drachmaeJs, /navigator\.clipboard\.writeText\(url\)/, 'the click copies the absolute address');
136
- const anchor = rule(roomCss, '.room--reference .primer-article .room-anchor');
137
- assert.ok(anchor, 'the anchor has its own rule, scoped to the reference, at higher specificity than the prose link');
138
- assert.match(anchor, /width:\s*24px/); assert.match(anchor, /height:\s*24px/);
139
- assert.match(anchor, /text-decoration:\s*none/);
140
- assert.ok(rule(roomCss, '.room--reference .primer-article :is(:target, .is-target) > .room-anchor'),
141
- 'the one the reader arrived at takes the accent — by :target, or by the mark the landing sets when :target resolved against nothing');
142
- });
143
-
144
- test('a deep link lands after the async render, the live section and the fonts', () => {
145
- assert.match(drachmaeJs, /function landOnHash\(\)[\s\S]*classList\.add\('is-target'\)[\s\S]*scrollIntoView/);
146
- const src = dejscomment(drachmaeJs);
147
- const inserted = src.indexOf('await insertBountySection(article)');
148
- const landed = src.indexOf('.then(landOnHash)');
149
- assert.ok(inserted > -1 && landed > inserted, 'landOnHash runs once the rate card has taken its place, or the offsets it lands on are wrong');
150
- assert.match(src, /document\.fonts\.ready[\s\S]*\.then\(landOnHash\)/, 'and after the fonts: a swap after the jump moves everything above the target');
151
- });
152
-
153
- test('on a phone the collapsed outline rides under the top bar, so the toggle can name the part from anywhere', () => {
154
- const m = decomment(roomCss).match(/@media \(max-width: 760px\)\s*\{\s*\.room--reference \.primer-toc\s*\{([^}]*)\}/);
155
- assert.ok(m, 'the reference re-sticks the contents at phone width (the archetype makes them static there)');
156
- assert.match(m[1], /position:\s*sticky/);
157
- assert.match(m[1], /z-index:\s*var\(--z-progress\)/, 'below the top bar in the ladder');
158
- });
159
-
160
- // ---- 5. the live table --------------------------------------------------------
161
-
162
- test('the live rate card is a section ahead of the closing links, not an appendix after them', () => {
163
- assert.match(drachmaeJs, /CLOSING_H2 = \/\^see it\/i/);
164
- assert.match(drachmaeJs, /article\.insertBefore\(section, closing\)/);
165
- assert.match(drachmaeJs, /else article\.appendChild\(section\)/, 'a retitled footer falls back to appending');
166
- });
167
-
168
- // ---- 6. the parser, actually run ----------------------------------------------
169
-
170
- test('the shared parser reaches h4 — the six earning sources rendered as paragraphs with a literal ####', () => {
171
- const MD = loadReader();
172
- const { html, headings } = MD.parseMarkdown('#### 4. Achievements\n\nUnlocking one pays its bonus.\n', (h) => h);
173
- assert.equal(headings.length, 1);
174
- assert.equal(headings[0].level, 4);
175
- assert.match(html, /<h4 id="4-achievements">4\. Achievements<\/h4>/);
176
- assert.doesNotMatch(html, /####/);
177
- });
178
-
179
- test('a paragraph breaks before an h4 the way it breaks before an h3', () => {
180
- const MD = loadReader();
181
- const { html } = MD.parseMarkdown('a line\n#### A head\n', (h) => h);
182
- assert.match(html, /<p>a line<\/p>\s*<h4/);
183
- });
184
-
185
- test('a heading that carries a link slugs by its text, so the copied address stays readable', () => {
186
- const MD = loadReader();
187
- const withLink = '2. Per-task credits, adjusted by *kind* ([ADR 0023](../adr/0023-kind-multipliers-and-idea-bonus.md))';
188
- assert.equal(MD.slugify(withLink), MD.slugify('2. Per-task credits, adjusted by *kind* (ADR 0023)'));
189
- assert.doesNotMatch(MD.slugify(withLink), /adr0023|md$/, 'the target path is not part of the address');
190
- });
191
-
192
- test('emphasis inside a bold run renders — the API list-price line showed its asterisks', () => {
193
- const MD = loadReader();
194
- const { html } = MD.parseMarkdown('- **It is the API *list* price, deliberately** and not the rest\n', (h) => h);
195
- assert.match(html, /<strong>It is the API <em>list<\/em> price, deliberately<\/strong> and not the rest/);
196
- const two = MD.parseMarkdown('**a** and **b**\n', (h) => h).html;
197
- assert.match(two, /<strong>a<\/strong> and <strong>b<\/strong>/, 'and two bold runs on one line stay two');
198
- });
199
-
200
- // ---- 7. the room's h4 -----------------------------------------------------------
201
-
202
- test('the room gives the fourth level a rule, the measure and the anchor offset', () => {
203
- const h4 = rule(roomCss, '.primer-article h4');
204
- assert.ok(h4, 'room.css styles h4 now that the parser emits it');
205
- assert.match(h4, /scroll-margin-top/, 'a deep link to an h4 clears the top bar');
206
- const measured = decomment(roomCss).match(/\.primer-article\s*>\s*p,[\s\S]*?\{([^}]*)\}/);
207
- assert.ok(measured, 'the measured-children list exists');
208
- assert.match(measured[0], /\.primer-article\s*>\s*h4/, 'and the h4 is measured with the prose');
209
- });
210
-
211
- // ---- 8. the states file and the one h1 ------------------------------------------
212
-
213
- test('the states file measures the reference\'s own states, including the deep link', () => {
214
- for (const name of ['read', 'toc-open', 'jump', 'out']) assert.ok(drachmaeStates.states[name], `state ${name}`);
215
- assert.match(drachmaeStates.states.jump.url, /#how-karma-is-earned-and-lost$/, 'the jump lands on an h3, so the trail is what the shot proves');
216
- assert.ok(drachmaeStates.reducedMotion, 'the kill switch is exercised');
217
- });
218
-
219
- test('the reference renders exactly one h1, the page head\'s', () => {
220
- const html = dehtmlcomment(drachmaeHtml);
221
- assert.equal([...html.matchAll(/<h1\b/g)].length, 1);
222
- assert.match(html, /<h1 class="page-head__h">/);
223
- });
224
-
225
- // ---- 9. the treasury section (task 1003446) --------------------------------
226
- //
227
- // The sharpest version of goal 1000072's thesis. /drachmae is labelled Economy
228
- // in the nav and was a markdown reader with ZERO live numbers, while the
229
- // signed-out status page already drew this very endpoint's cumulative line and
230
- // the Work board fetched the same payload to read ONE scalar off it. The
231
- // stranger got the chart; the builder got the number.
232
- //
233
- // The section's ARITHMETIC is not asserted here — it is the kit's
234
- // (`cumulateByDay`, `timeStepSeriesHtml`, `barSeriesHtml`) and
235
- // tests/hall_kit_charts.mjs runs the real kit against it. What these hold is
236
- // the page's half of the contract, the half a kit test cannot see.
237
-
238
- const costSummaryFixture = JSON.parse(
239
- fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'cost-summary.json'), 'utf8'),
240
- );
241
-
242
- test('the treasury reads the endpoint that already carried the series — no new route', () => {
243
- // The whole point of the task: the payload had both series all along, and the
244
- // route's own comment calls daily[] "the treasury chart's cumulative line".
245
- assert.match(drachmaeJs, /TREASURY_URL = '\/api\/bongos\/public\/cost-summary'/);
246
- assert.ok(costSummaryFixture.daily.length, 'the payload really carries daily[]');
247
- assert.ok(costSummaryFixture.daily_credits.length, 'and daily_credits[]');
248
- assert.ok(Number(costSummaryFixture.total_usd) > 0, 'and the totals the prose states');
249
- assert.ok(Number(costSummaryFixture.total_value_credits) > 0);
250
- });
251
-
252
- test('RUN: the cumulative line ends exactly on the total the prose states', async () => {
253
- // The invariant that matters, and the one a source-text assertion cannot
254
- // reach: the chart and the sentence above it are derived from the SAME
255
- // payload, so they can never disagree. Run against the real kit and the real
256
- // checked-in summary, which is where a units or gap-fill mistake would show
257
- // up as a line ending somewhere other than the headline number.
258
- const kitPath = path.join(HALL, 'hall-kit.js');
259
- const kit = await import(`file://${kitPath.replace(/\\/g, '/')}`).then((m) => m.default);
260
- const spend = kit.cumulateByDay(costSummaryFixture.daily, { valueOf: (r) => r.amount_usd });
261
- const earned = kit.cumulateByDay(costSummaryFixture.daily_credits, { valueOf: (r) => r.credits });
262
- assert.ok(spend.length > 1 && earned.length > 1, 'both series have depth');
263
- assert.ok(Math.abs(spend[spend.length - 1].value - Number(costSummaryFixture.total_usd)) < 1,
264
- 'the spend line ends on total_usd');
265
- assert.equal(earned[earned.length - 1].value, Number(costSummaryFixture.total_value_credits),
266
- 'and the awarded line ends on total_value_credits');
267
- // Whatever the plot draws from that must be finite — a NaN in a path `d`
268
- // renders as an invisible chart with no error anywhere.
269
- const series = kit.timeStepSeriesHtml(spend, {
270
- x0: spend[0].x, x1: spend[spend.length - 1].x, yMax: spend[spend.length - 1].value,
271
- });
272
- assert.doesNotMatch(series.body, /NaN|Infinity/);
273
- assert.doesNotMatch(
274
- kit.barSeriesHtml(costSummaryFixture.by_category.map((c) => ({ label: c.category, value: c.total_usd }))),
275
- /NaN|Infinity/,
276
- );
277
- });
278
-
279
- test('the page loads the chart primitive BEFORE the script that reads it', () => {
280
- // window.OTBKit read at insert time with the kit loaded after it is an empty
281
- // section in production and a green unit suite.
282
- const loaded = [...drachmaeHtml.matchAll(/<script[^>]+src="([^"?]+)/g)].map((m) => m[1].split('/').pop());
283
- const kitAt = loaded.indexOf('hall-kit.js');
284
- const pageAt = loaded.indexOf('drachmae.js');
285
- assert.ok(kitAt > -1, 'drachmae.html must load hall-kit.js');
286
- assert.ok(pageAt > -1, 'and drachmae.js');
287
- assert.ok(kitAt < pageAt, 'kit first');
288
- });
289
-
290
- test('every mark on the page is drawn by the kit, none by hand', () => {
291
- const js = dejscomment(drachmaeJs);
292
- for (const fn of ['cumulateByDay', 'timeStepSeriesHtml', 'plotSvgHtml', 'plotValueLabelHtml', 'barSeriesHtml']) {
293
- assert.match(js, new RegExp(`(kit|window\\.OTBKit)\\.${fn}\\(`), `the treasury draws through ${fn}`);
294
- }
295
- // The §6b audit rule holds this globally; asserted here too because THIS is
296
- // the page that had no chart at all and so the easiest place to hand-roll one.
297
- assert.doesNotMatch(js, /<(?:path|polyline|rect)\b[^>]*\b(?:d|points)="\$\{/,
298
- 'no hand-built SVG geometry on the reference page');
299
- });
300
-
301
- test('the section degrades to NOTHING rather than an empty panel', () => {
302
- const js = dejscomment(drachmaeJs);
303
- // The bounty section's contract, kept: a failed read leaves the prose alone
304
- // and adds no outline entry that leads nowhere.
305
- const fn = js.slice(js.indexOf('async function insertTreasurySection'));
306
- const body = fn.slice(0, fn.indexOf('\n }\n'));
307
- assert.match(body, /console\.warn\([^)]*cost summary unavailable/);
308
- assert.equal((body.match(/return false;/g) || []).length, 3,
309
- 'three honest exits: no kit, an unreadable summary, and a treasury with nothing in it');
310
- assert.match(body, /if \(!spend\.length && !earned\.length\) return false;/,
311
- 'both series empty is one honest nothing, not two empty plots');
312
- });
313
-
314
- test('the plots ship no focus stop, because nothing here wires a readout', () => {
315
- // A role="img" with tabindex="0" is a focus stop a keyboard reader lands on
316
- // with nothing to do, and a crosshair nothing moves only looks like a
317
- // feature. The kit half is asserted in tests/hall_kit_charts.mjs.
318
- assert.match(dejscomment(drachmaeJs), /interactive: false/);
319
- });
320
-
321
- test('the currency is the branding pack\'s, never a hardcoded word', () => {
322
- const js = dejscomment(drachmaeJs);
323
- assert.match(js, /__BRANDING__ && window\.__BRANDING__\.currency/);
324
- assert.match(js, /\|\| 'credits'/, 'with a neutral fallback, not a world-specific one');
325
- assert.doesNotMatch(js.replace(/\|\| 'credits'/g, ''), /drachmae'/i,
326
- 'the instance\'s own currency name is not spelled into the core page');
327
- });
328
-
329
- test('the treasury lands ahead of the rate card, so the doc still ends on "dig deeper"', () => {
330
- const js = dejscomment(drachmaeJs);
331
- // The bounty section goes in first and the treasury anchors on its id, so the
332
- // order is prose → what happened → the published rates → the closing links.
333
- const boot = js.slice(js.indexOf('async function boot()'));
334
- assert.ok(boot.indexOf('insertBountySection(article)') < boot.indexOf('insertTreasurySection(article)'),
335
- 'the rate card is inserted first, so the treasury can anchor ahead of it');
336
- assert.match(js, /article\.querySelector\(`#\$\{BOUNTY_SECTION_ID\}`\)/);
337
- assert.match(js, /CLOSING_H2\.test/, 'and it falls back to the closing links, then to appending');
338
- });
339
-
340
- test('the figure chrome is shared, because a second page now draws figures', () => {
341
- // .pchart* moved from profile.css to style.css with this task. drachmae.html
342
- // loads style.css and NOT profile.css, so a copy left behind would render
343
- // this page's figures unstyled.
344
- const style = decomment(read('style.css'));
345
- const profile = decomment(read('profile.css'));
346
- for (const sel of ['.pcharts', '.pchart ', '.pchart__title', '.pchart__twin', '.pchart__foot']) {
347
- assert.ok(style.includes(sel), `${sel} is in the shared sheet`);
348
- }
349
- assert.ok(!/^\.pcharts\s*\{/m.test(profile), 'and not left duplicated in profile.css');
350
- assert.match(drachmaeHtml, /style\.css/);
351
- assert.doesNotMatch(drachmaeHtml, /profile\.css/, 'the reference page must not need the profile sheet');
352
- });