@bongos/core 1.21.19 → 1.21.21
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 +84 -74
- package/docs/copy-inventory.md +29 -25
- package/docs/copy-registry.json +87 -30
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +9 -3
- package/docs/page-readings.json +399 -388
- package/modules/autonomy/governor-docket.js +118 -4
- package/modules/economy/cost.js +36 -0
- package/modules/economy/governor-docket.js +81 -0
- package/modules/economy/routes/cost.js +2 -0
- package/modules/government/docket-floors.js +222 -0
- package/modules/government/docket-vocab.js +20 -0
- package/modules/government/docket.js +186 -5
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.css +16 -2
- package/modules/hall-ui/public/city.html +29 -9
- package/modules/hall-ui/public/city.js +25 -2
- package/modules/hall-ui/public/city.states.json +7 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +3 -0
- package/modules/hall-ui/public/street-inside.js +99 -43
- package/modules/hall-ui/public/street.css +17 -4
- package/modules/hall-ui/public/street.html +26 -8
- package/modules/hall-ui/public/street.js +16 -2
- package/modules/hall-ui/public/street.states.json +17 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/governor-docket.js +42 -1
- package/modules/lifecycle/governor-docket.js +197 -2
- package/modules/security/routes/reports.js +16 -0
- package/modules/security/security.js +5 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +186 -2
- package/tests/hall_audit.mjs +7 -12
- package/tests/hall_city.mjs +44 -4
- package/tests/hall_street.mjs +37 -5
- package/tests/hall_street_inside.mjs +97 -3
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
package/tests/hall_audit.mjs
CHANGED
|
@@ -375,18 +375,13 @@ test('the module doc still records where the 2179 density pass landed', () => {
|
|
|
375
375
|
// else is on the page but the wordmark, a sun/moon switch, start governing and
|
|
376
376
|
// one line. It still loads style.css (the hall's tokens, so no private palette:
|
|
377
377
|
// city.css spends var(--bg), var(--ink), var(--accent) …), and reads and writes
|
|
378
|
-
// the hall's own 'hall-theme' preference.
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
// hall's tokens, the hall's own 'hall-theme' preference, no private palette.
|
|
386
|
-
const STANDALONE_EXEMPT = {
|
|
387
|
-
'city.html': 'the governor city home: nothing on the page but the prototype home (task 1004524)',
|
|
388
|
-
'street.html': 'the governor street: nothing on the page but the prototype street view (task 1004525)',
|
|
389
|
-
};
|
|
378
|
+
// the hall's own 'hall-theme' preference. THE STREET (task 1004525) followed
|
|
379
|
+
// it for the same reason. Both left this list in task 1004544 (owner decision
|
|
380
|
+
// 2026-10-02): they sit in the hall chrome now, behind the shell's full-screen
|
|
381
|
+
// switch, exactly as the sky did in task 1004327, so the list is empty again.
|
|
382
|
+
// Neither has a sidebar link (the owner's call): the city is reached by URL and
|
|
383
|
+
// the street from the city's start governing.
|
|
384
|
+
const STANDALONE_EXEMPT = {};
|
|
390
385
|
|
|
391
386
|
test('every hall page loads style.css and shell.js', () => {
|
|
392
387
|
const missing = [];
|
package/tests/hall_city.mjs
CHANGED
|
@@ -241,10 +241,27 @@ test('the page holds only the wordmark, the switch, the city, one button and one
|
|
|
241
241
|
assert.equal((html.match(/<a\b/g) || []).length, 1, 'one door');
|
|
242
242
|
assert.equal((html.match(/<button\b/g) || []).length, 1, 'one switch');
|
|
243
243
|
assert.match(html, /class="city-mark">\{\{productName\}\}</);
|
|
244
|
-
assert.doesNotMatch(html, /app-sidebar|shell\.js/, 'no hall chrome on the city');
|
|
245
244
|
assert.equal(City.STREET_HREF, '/builders/street');
|
|
246
245
|
});
|
|
247
246
|
|
|
247
|
+
// task 1004544 (owner decision 2026-10-02): the city sits in the hall chrome
|
|
248
|
+
// behind the shell's full-screen switch, the sky's mechanism (task 1004327),
|
|
249
|
+
// and still has no sidebar link.
|
|
250
|
+
test('the city sits in the hall chrome, behind the shell\'s full-screen switch', () => {
|
|
251
|
+
const html = read('city.html').replace(/<!--[\s\S]*?-->/g, '');
|
|
252
|
+
assert.match(html, /<body data-page="city"[^>]*\sdata-fullscreen-switch>/, 'the bare attribute: the page lays itself out for full screen');
|
|
253
|
+
for (const id of ['app-sidebar', 'app-scrim', 'app-topbar']) assert.match(html, new RegExp(`id="${id}"`), id);
|
|
254
|
+
assert.match(html, /<main id="hall-main" class="city-main">\s*<div class="city-top">/, 'the stage is the one <main>, the strip inside it');
|
|
255
|
+
const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
|
|
256
|
+
assert.deepEqual(scripts, ['shell', 'palette', 'city-draw', 'city'], 'the shell first, as on every hall page');
|
|
257
|
+
const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
258
|
+
assert.match(css, /\.city-main \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
|
|
259
|
+
assert.match(css, /html\[data-fullscreen="on"\] \.city-main \{ top: 0; left: 0; \}/);
|
|
260
|
+
assert.match(css, /html\[data-fullscreen="on"\] \.city-top \{ right: 184px; \}/, 'the strip steps clear of the corner exit');
|
|
261
|
+
const shell = read('shell.js');
|
|
262
|
+
assert.doesNotMatch(shell, /href:\s*'(\/builders)?\/(city|street)'/, 'no sidebar link (owner: leave it off the sidebar)');
|
|
263
|
+
});
|
|
264
|
+
|
|
248
265
|
test('motion is CSS only and dies under reduced motion; no page :root; no innerHTML', () => {
|
|
249
266
|
const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
250
267
|
assert.doesNotMatch(css, /:root/);
|
|
@@ -293,7 +310,7 @@ function fakeDom() {
|
|
|
293
310
|
return { document, byId, html };
|
|
294
311
|
}
|
|
295
312
|
|
|
296
|
-
async function bootPage({ feeds = {}, stored = null, osDark = true } = {}) {
|
|
313
|
+
async function bootPage({ feeds = {}, stored = null, osDark = true, shell = undefined, stage = null } = {}) {
|
|
297
314
|
const { document, byId, html } = fakeDom();
|
|
298
315
|
const store = new Map(stored ? [['hall-theme', stored]] : []);
|
|
299
316
|
const calls = [];
|
|
@@ -308,14 +325,18 @@ async function bootPage({ feeds = {}, stored = null, osDark = true } = {}) {
|
|
|
308
325
|
const body = feeds[url];
|
|
309
326
|
return Promise.resolve(body === undefined ? { ok: false, json: async () => ({}) } : { ok: true, json: async () => body });
|
|
310
327
|
};
|
|
311
|
-
const
|
|
328
|
+
const observers = [];
|
|
329
|
+
class MutationObserver { constructor(fn) { this.fn = fn; observers.push(this); } observe() {} }
|
|
330
|
+
window.OTBShell = shell;
|
|
331
|
+
if (stage) { const m = document.createElement('main'); m.setAttribute('id', 'hall-main'); Object.assign(m, stage); }
|
|
332
|
+
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, console, MutationObserver });
|
|
312
333
|
ctx.globalThis = ctx;
|
|
313
334
|
window.GovCity = undefined;
|
|
314
335
|
vm.runInContext(read('city-draw.js'), ctx);
|
|
315
336
|
window.GovCity = ctx.GovCity || ctx.window.GovCity;
|
|
316
337
|
vm.runInContext(read('city.js'), ctx);
|
|
317
338
|
await window.GovCityPage.ready;
|
|
318
|
-
return { window, byId, html, store, calls };
|
|
339
|
+
return { window, byId, html, store, calls, observers };
|
|
319
340
|
}
|
|
320
341
|
|
|
321
342
|
const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1 })]) => ({
|
|
@@ -363,6 +384,25 @@ test('the switch reads and writes the hall\'s own theme preference', async () =>
|
|
|
363
384
|
assert.equal(q.html.attrs['data-theme'], 'dark', 'no stored choice follows the OS');
|
|
364
385
|
});
|
|
365
386
|
|
|
387
|
+
test('in the hall, the city and the top bar\'s theme switch never disagree (task 1004544)', async () => {
|
|
388
|
+
let shellPaints = 0;
|
|
389
|
+
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
|
|
390
|
+
assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
|
|
391
|
+
p.byId.get('cityMode').fire('click');
|
|
392
|
+
assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
|
|
393
|
+
const before = JSON.stringify(p.byId.get('cityFlat').children[0].attrs);
|
|
394
|
+
p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
|
|
395
|
+
p.observers[0].fn();
|
|
396
|
+
assert.equal(p.byId.get('cityMode').attrs['aria-label'], 'Switch to dark mode', 'this switch follows the shell\'s');
|
|
397
|
+
assert.notEqual(JSON.stringify(p.byId.get('cityFlat').children[0].attrs), before, 'and the city redraws in that mode');
|
|
398
|
+
assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('the artboard fits the stage the hall chrome leaves, not the window (task 1004544)', async () => {
|
|
402
|
+
const p = await bootPage({ feeds: FEEDS([]), stage: { clientWidth: 688, clientHeight: 900 } });
|
|
403
|
+
assert.equal(p.byId.get('cityStage').style['--city-scale'], '0.688');
|
|
404
|
+
});
|
|
405
|
+
|
|
366
406
|
test('docket text lands as text: a title with markup is never parsed', async () => {
|
|
367
407
|
const p = await bootPage({ feeds: FEEDS([matter({ title: '<img src=x onerror=alert(1)>', short: '<b>x</b>' })]) });
|
|
368
408
|
assert.equal(p.byId.get('cityPins').children.length, 1);
|
package/tests/hall_street.mjs
CHANGED
|
@@ -375,12 +375,27 @@ test('the page holds the prototype\'s header, the street, the readings and the c
|
|
|
375
375
|
assert.equal((html.match(/<h1\b/g) || []).length, 1);
|
|
376
376
|
assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
|
|
377
377
|
for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetMode', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
|
|
378
|
-
assert.doesNotMatch(html, /app-sidebar|shell\.js/, 'no hall chrome, as the city');
|
|
379
378
|
const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
|
|
380
|
-
assert.deepEqual(scripts, ['city-draw', 'street-draw', 'street-card', 'street-inside', 'street-amend', 'street'], 'the
|
|
379
|
+
assert.deepEqual(scripts, ['shell', 'palette', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-amend', 'street'], 'the hall shell (task 1004544), the shared kit, then the card (task 1004528), the interiors (task 1004526) and the drafted amendment (task 1004530)');
|
|
381
380
|
assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
|
|
382
381
|
});
|
|
383
382
|
|
|
383
|
+
// task 1004544 (owner decision 2026-10-02): the street sits in the hall chrome
|
|
384
|
+
// behind the shell's full-screen switch, as the city and the sky (task 1004327),
|
|
385
|
+
// and still has no sidebar link.
|
|
386
|
+
test('the street sits in the hall chrome, behind the shell\'s full-screen switch', () => {
|
|
387
|
+
const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
|
|
388
|
+
assert.match(html, /<body data-page="street"[^>]*\sdata-fullscreen-switch>/, 'the bare attribute: the page lays itself out for full screen');
|
|
389
|
+
for (const id of ['app-sidebar', 'app-scrim', 'app-topbar']) assert.match(html, new RegExp(`id="${id}"`), id);
|
|
390
|
+
assert.match(html, /<main id="hall-main" class="street-stage">\s*<div class="street-top">/, 'the stage is the one <main>, the strip inside it');
|
|
391
|
+
assert.match(html, /<div class="street-window" id="streetWindow">/, 'the street band is a <div> inside the stage');
|
|
392
|
+
const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
393
|
+
assert.match(css, /\.street-stage \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
|
|
394
|
+
assert.match(css, /html\[data-fullscreen="on"\] \.street-stage \{ top: 0; left: 0; \}/);
|
|
395
|
+
assert.match(css, /html\[data-fullscreen="on"\] \.street-top \{ padding-right: 184px; \}/, 'the strip steps clear of the corner exit');
|
|
396
|
+
assert.doesNotMatch(read('shell.js'), /href:\s*'(\/builders)?\/(city|street)'/, 'no sidebar link (owner: leave it off the sidebar)');
|
|
397
|
+
});
|
|
398
|
+
|
|
384
399
|
test('motion is CSS only and dies under reduced motion; no page :root; no innerHTML', () => {
|
|
385
400
|
const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
386
401
|
assert.doesNotMatch(css, /:root/);
|
|
@@ -428,7 +443,7 @@ function fakeDom() {
|
|
|
428
443
|
return { document, byId, html, focused };
|
|
429
444
|
}
|
|
430
445
|
|
|
431
|
-
async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '' } = {}) {
|
|
446
|
+
async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '', shell = undefined } = {}) {
|
|
432
447
|
const { document, byId, html, focused } = fakeDom();
|
|
433
448
|
const store = new Map(stored ? [['hall-theme', stored]] : []);
|
|
434
449
|
const calls = [];
|
|
@@ -444,7 +459,10 @@ async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '' }
|
|
|
444
459
|
const body = feeds[url];
|
|
445
460
|
return Promise.resolve(body === undefined ? { ok: false, json: async () => ({}) } : { ok: true, json: async () => body });
|
|
446
461
|
};
|
|
447
|
-
const
|
|
462
|
+
const observers = [];
|
|
463
|
+
class MutationObserver { constructor(fn) { this.fn = fn; observers.push(this); } observe() {} }
|
|
464
|
+
window.OTBShell = shell;
|
|
465
|
+
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, MutationObserver });
|
|
448
466
|
ctx.globalThis = ctx;
|
|
449
467
|
// each file hangs its api on `window`, as the browser loads them
|
|
450
468
|
vm.runInContext(read('city-draw.js'), ctx);
|
|
@@ -453,7 +471,7 @@ async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '' }
|
|
|
453
471
|
vm.runInContext(read('street.js'), ctx);
|
|
454
472
|
await window.GovStreetPage.ready;
|
|
455
473
|
const key = (t, e) => (listeners[t] || []).forEach((fn) => fn(e));
|
|
456
|
-
return { window, byId, html, store, calls, key, focused };
|
|
474
|
+
return { window, byId, html, store, calls, key, focused, observers };
|
|
457
475
|
}
|
|
458
476
|
|
|
459
477
|
const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1 })]) => ({
|
|
@@ -509,6 +527,20 @@ test('the switch reads and writes the hall\'s own theme preference and redraws t
|
|
|
509
527
|
assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'the street redraws in the new mode');
|
|
510
528
|
});
|
|
511
529
|
|
|
530
|
+
test('in the hall, the street and the top bar\'s theme switch never disagree (task 1004544)', async () => {
|
|
531
|
+
let shellPaints = 0;
|
|
532
|
+
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
|
|
533
|
+
assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
|
|
534
|
+
p.byId.get('streetMode').fire('click');
|
|
535
|
+
assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
|
|
536
|
+
const before = JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs);
|
|
537
|
+
p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
|
|
538
|
+
p.observers[0].fn();
|
|
539
|
+
assert.equal(p.byId.get('streetModeWord').textContent, 'dark', 'this switch follows the shell\'s');
|
|
540
|
+
assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'and the street redraws in that mode');
|
|
541
|
+
assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
|
|
542
|
+
});
|
|
543
|
+
|
|
512
544
|
test('arriving through a door marks that building, and a failed docket says so', async () => {
|
|
513
545
|
const p = await bootPage({ feeds: FEEDS([]), hash: '#building=treasury' });
|
|
514
546
|
const here = p.byId.get('streetDoors').children.filter((a) => a.attrs['aria-current'] === 'location');
|
|
@@ -68,6 +68,8 @@ function docket(matters = [], over = {}) {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
const goal = (id, status = 'open', title = `Goal ${id}`) => ({ id: String(id), title, status });
|
|
71
|
+
// a docket floor row in the route's shape (docket.js buildDocket `floors`)
|
|
72
|
+
const floorRow = (key, over = {}) => ({ key, building: key.split('.')[0], status: 'live', count: '3', line: 'three things', lines: [['One thing.', 'today']], hot: false, reason: null, ...over });
|
|
71
73
|
|
|
72
74
|
// ── the drawing is the prototype's ───────────────────────────────────────────
|
|
73
75
|
|
|
@@ -176,10 +178,12 @@ test('each building keeps the prototype\'s capacity, words and floor order', ()
|
|
|
176
178
|
|
|
177
179
|
// ── what a floor says comes from the docket, and only from it ───────────────
|
|
178
180
|
|
|
179
|
-
test('a floor
|
|
180
|
-
const
|
|
181
|
+
test('a floor whose engine is coming stands as not recorded yet: faint, no number, and says so', () => {
|
|
182
|
+
const coming = docket([], { floors: I.PLAN.charter.floors.map((f) => floorRow(f[2].floor, { status: 'coming', count: '', lines: [], reason: 'Not yet.' })) });
|
|
183
|
+
const b = I.civicInfo('charter', { docket: coming, goals: [], now: NOW });
|
|
181
184
|
assert.ok(b.floors.every((f) => f.status === 'coming' && f.count === ''), 'nothing on the charter is read yet');
|
|
182
|
-
|
|
185
|
+
assert.deepEqual(b.floors[1].lines, [['Not yet.', 'coming']], 'it says why, in the docket\'s words');
|
|
186
|
+
const bv = I.buildingModel({ key: 'charter', docket: coming, mode: 'dark', now: NOW });
|
|
183
187
|
const quiet = bv.texts.filter((t) => t.v === 'not recorded yet');
|
|
184
188
|
assert.equal(quiet.length, 8, 'every used floor says it on the wall');
|
|
185
189
|
assert.ok(bv.texts.filter((t) => t.s === 14.5).every((t) => t.c === '#8e8e93'), 'its name lettered faint, like a proposed floor');
|
|
@@ -265,6 +269,96 @@ test('who holds it: the docket\'s holders when it names them, else an empty seat
|
|
|
265
269
|
assert.deepEqual([held.seats, held.who, held.how], [['M'], 'Mira, treasurer', 'more than half must say yes']);
|
|
266
270
|
});
|
|
267
271
|
|
|
272
|
+
// ── real data on every floor (task 1004527) ─────────────────────────────────
|
|
273
|
+
|
|
274
|
+
test('every floor of every building is bound to a docket feed: none is left unwired', () => {
|
|
275
|
+
for (const key of S.CIVIC_KEYS) {
|
|
276
|
+
for (const [name, , bind] of I.PLAN[key].floors) {
|
|
277
|
+
assert.ok(bind && (bind.floor || bind.source || bind.reading || bind.goals), `${key} · ${name} reads something`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('a live floor shows exactly what the docket says: its count, line and lines; hot is being decided', () => {
|
|
283
|
+
const d = docket([], { floors: [
|
|
284
|
+
floorRow('treasury.rewards', { count: '$2,400', line: '$2,400 this month · 6 builders', lines: [['Paid to builders for shipped work.', 'each ship']] }),
|
|
285
|
+
floorRow('automation.allowed_goals', { count: '2', line: '2 goals it may build', lines: [['goal 7', 'first in line'], ['goal 99', 'since sep 2']] }),
|
|
286
|
+
floorRow('charter.founding', { count: '', line: '1 of 2 founding steps passed', lines: [['The charter.', 'passed']], hot: true }),
|
|
287
|
+
] });
|
|
288
|
+
const t = I.civicInfo('treasury', { docket: d, now: NOW }).floors.find((f) => f.name === 'Builder rewards');
|
|
289
|
+
assert.deepEqual([t.count, t.line, t.status, t.lines], ['$2,400', '$2,400 this month · 6 builders', 'ok', [['Paid to builders for shipped work.', 'each ship']]]);
|
|
290
|
+
const a = I.civicInfo('automation', { docket: d, goals: [goal(7, 'open', 'Onboarding')], now: NOW }).floors.find((f) => f.name === 'Allowed goals');
|
|
291
|
+
assert.deepEqual(a.lines, [['Onboarding', 'first in line'], ['goal 99', 'since sep 2']], 'a goal it knows is lettered by title; one it does not keeps its id');
|
|
292
|
+
const c = I.buildingModel({ key: 'charter', docket: d, mode: 'dark', now: NOW });
|
|
293
|
+
assert.equal(c.selFloor, 0, 'a founding step being decided is the floor chosen first');
|
|
294
|
+
assert.equal(c.openKicker, 'floor 1 · being decided');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('Rules in force: each rule and what it settled, from its floor; the reading when the floor is absent', () => {
|
|
298
|
+
const d = docket([], { floors: [floorRow('rules.in_force', { count: '1', line: '1 rule · settled 18 this month', lines: [['Every filing lands in its goal without a vote.', 'settled 18']] })] });
|
|
299
|
+
const f = I.civicInfo('rules', { docket: d, now: NOW }).floors[0];
|
|
300
|
+
assert.deepEqual([f.count, f.line, f.lines[0]], ['1', '1 rule · settled 18 this month', ['Every filing lands in its goal without a vote.', 'settled 18']]);
|
|
301
|
+
const old = docket();
|
|
302
|
+
Object.assign(old.readings.find((r) => r.key === 'rules'), { status: 'live', figure: '1 rule', line: 'the landing rule settled 4 filings this month' });
|
|
303
|
+
assert.deepEqual(I.civicInfo('rules', { docket: old, now: NOW }).floors[0].count, '1', 'an older docket still reads the reading');
|
|
304
|
+
const h = I.holderOf('people', { decision: { who: 'metic', rule: 'consent' }, holders: [], holder_count: 3 }, []);
|
|
305
|
+
assert.equal(h.who, 'metic', 'a count with no names never letters undefined');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('a floor the docket cannot answer never shows a number; Goals falls back to the open goals', () => {
|
|
309
|
+
const d = docket([], { floors: [floorRow('people.ranks', { status: 'not_yours', count: '9' }), floorRow('people.council', { status: 'off', count: '5', reason: 'Off here.' }), floorRow('people.newcomers', { status: 'unavailable', count: '2' })] });
|
|
310
|
+
const by = Object.fromEntries(I.civicInfo('people', { docket: d, now: NOW }).floors.map((f) => [f.name, f]));
|
|
311
|
+
for (const [n, st] of [['Ranks', 'not_yours'], ['The council', 'off'], ['Newcomers', 'unavailable'], ['Testers', 'unavailable']]) {
|
|
312
|
+
assert.deepEqual([by[n].status, by[n].count], [st, ''], `${n} says ${st} and no number`);
|
|
313
|
+
}
|
|
314
|
+
const g = I.civicInfo('planning', { docket: docket(), goals: [goal(1), goal(2)], now: NOW }).floors[0];
|
|
315
|
+
assert.deepEqual([g.count, g.line], ['2', '2 goals open'], 'an older docket without floors still counts the open goals');
|
|
316
|
+
const live = I.civicInfo('planning', { docket: docket([], { floors: [floorRow('planning.goals', { count: '1', line: '1 goal open', lines: [['Hall', '5 of 5 kept']] })] }), goals: [goal(1), goal(2)], now: NOW }).floors[0];
|
|
317
|
+
assert.deepEqual([live.count, live.lines], ['1', [['Hall', '5 of 5 kept']]], 'with the floor, its promises kept of total');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('no made-up number: every count on every wall is one the docket sent', () => {
|
|
321
|
+
const ms = [matter({ id: 'access_requests:1', source: 'access_requests', building: 'people' })];
|
|
322
|
+
const floors = [floorRow('people.ranks', { count: '4' }), floorRow('treasury.hosting', { count: '$120' }), floorRow('release.live_month', { count: '7' })];
|
|
323
|
+
const d = docket(ms, { floors });
|
|
324
|
+
Object.assign(d.readings.find((r) => r.key === 'money'), { status: 'live', figure: '$1,100 left', line: 'left' });
|
|
325
|
+
const sent = new Set(['', ...floors.map((f) => f.count), '1', '0', '$1,100']);
|
|
326
|
+
for (const key of S.CIVIC_KEYS) {
|
|
327
|
+
for (const f of I.civicInfo(key, { docket: d, goals: [], now: NOW }).floors) assert.ok(sent.has(f.count), `${key} · ${f.name} shows ${f.count}`);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('who holds it reads the decision table: the people it seats, its rule, you, and a vacant seat', () => {
|
|
332
|
+
const info = { key: 'release', decision: { who: 'whoever may put a release live', rule: 'decide', rule_words: 'decides alone', as_today: true }, holders: [{ id: '95', name: 'Ana' }, { id: '7', login: 'theo' }], holder_count: 2 };
|
|
333
|
+
const h = I.holderOf('release', info, [], '95');
|
|
334
|
+
assert.deepEqual([h.seats, h.who, h.rule, h.how], [['you', 'T'], 'Ana and theo · whoever may put a release live', 'decide', 'one person decides']);
|
|
335
|
+
const many = I.holderOf('people', { decision: { who: 'metic and above', rule: 'consent' }, holders: [{ name: 'A' }, { name: 'B' }, { name: 'C' }], holder_count: 7 }, []);
|
|
336
|
+
assert.equal(many.who, 'A, B, C and 4 more · metic and above');
|
|
337
|
+
const vacant = I.holderOf('review', { decision: { who: 'no one yet', rule: 'vacant' }, holders: [] }, []);
|
|
338
|
+
assert.deepEqual([vacant.seats, vacant.who, vacant.how], [['—'], 'no one holds it yet', 'its matters wait until a seat is filled']);
|
|
339
|
+
const d = docket();
|
|
340
|
+
d.buildings.find((b) => b.key === 'review').decision = { who: 'no one yet', rule: 'vacant' };
|
|
341
|
+
assert.equal(I.civicInfo('review', { docket: d, now: NOW }).vacant, true, 'a vacant row is an unheld Review');
|
|
342
|
+
const held = I.civicInfo('review', { docket: docket(), now: NOW });
|
|
343
|
+
assert.deepEqual(held.floors.map((f) => f.name), ['Testers', 'Being tested', 'Signed off'], 'held, its queue is the prototype\'s Being tested');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('the record drawers read the real history by month, file a sitting once, and cap a full drawer', () => {
|
|
347
|
+
const history = [
|
|
348
|
+
{ id: 'government:amendment:12', source: 'government', building: 'charter', title: '§6 Review: testers hold the seat', at: '2026-10-01T09:00:00.000Z', outcome: 'passed' },
|
|
349
|
+
{ id: 'government:founding:3', source: 'government', building: 'charter', title: 'Founding: the charter', at: '2026-07-04T09:00:00.000Z', outcome: 'passed' },
|
|
350
|
+
{ id: 'releases:9', source: 'releases', building: 'release', title: 'Invite flow', at: '2026-09-26T09:00:00.000Z', outcome: 'live' },
|
|
351
|
+
];
|
|
352
|
+
const ms = [matter({ id: 'board:12', source: 'board', building: 'charter', state: 'settled', title: '§6 Review: testers hold the seat', ref: { type: 'board_item', id: '12' }, opened_at: '2026-09-28T00:00:00.000Z' })];
|
|
353
|
+
const m = I.buildingModel({ key: 'charter', docket: docket(ms, { history }), mode: 'dark', now: NOW });
|
|
354
|
+
assert.deepEqual(m.drawers.map((x) => [x.label, x.count]), [['oct', 1], ['jul', 1]], 'the settled sitting is filed once, by when it closed');
|
|
355
|
+
assert.deepEqual(m.drawerItems.map((x) => [x.text, x.meta]), [['§6 Review: testers hold the seat', 'passed']]);
|
|
356
|
+
assert.equal(m.onFile, '1 matter on the docket · 2 on the record');
|
|
357
|
+
const lots = Array.from({ length: 15 }, (_, i) => ({ id: `releases:${i}`, building: 'release', title: `R${i}`, at: '2026-09-02T00:00:00.000Z', outcome: 'live' }));
|
|
358
|
+
const r = I.recordOf([], NOW, lots);
|
|
359
|
+
assert.deepEqual([r[0].count, r[0].items.length, r[0].items[12]], [15, 13, ['and 3 more', '']]);
|
|
360
|
+
});
|
|
361
|
+
|
|
268
362
|
test('the annex lot stands only when the building is full; an unheld Review is the dashed ghost', () => {
|
|
269
363
|
assert.equal(I.buildingModel({ key: 'charter', docket: docket(), mode: 'dark', now: NOW }).hasAnnex, false, '8 of 9');
|
|
270
364
|
const save = I.PLAN.rules.cap;
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-10-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-02-theme/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = 'a8dc2884930ca19a';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|