@bongos/core 1.21.112 → 1.21.114
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 +616 -186
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -0
- package/GOVERNMENT.md +128 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +28 -0
- package/clients/bongos-client/index.cjs +28 -0
- package/clients/bongos-client/index.d.ts +45 -2
- package/clients/bongos-client/index.mjs +28 -0
- package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
- package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
- package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
- package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
- package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
- package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
- package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
- package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
- package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +989 -87
- package/docs/api-reference.md +18 -4
- package/docs/copy-inventory.md +343 -262
- package/docs/copy-registry.json +1223 -426
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +5017 -4458
- package/modules/builder-settings/law-locks.js +202 -0
- package/modules/builder-settings/routes/settings.js +66 -6
- package/modules/builder-settings/settings.js +7 -0
- package/modules/discord/ship-broadcast.js +22 -13
- package/modules/government/asks-db.js +54 -0
- package/modules/government/asks.js +269 -0
- package/modules/government/board-idea-reads.js +68 -0
- package/modules/government/board.js +21 -57
- package/modules/government/build-rights.js +148 -0
- package/modules/government/buildouts.js +160 -0
- package/modules/government/counts.js +81 -0
- package/modules/government/db.js +242 -0
- package/modules/government/decision-board.js +234 -8
- package/modules/government/decision-rules.js +2 -0
- package/modules/government/docket.js +114 -6
- package/modules/government/law-board.js +131 -0
- package/modules/government/laws.js +241 -0
- package/modules/government/leads.js +170 -0
- package/modules/government/migrations/government_027_laws.sql +143 -0
- package/modules/government/migrations/government_028_asks.sql +57 -0
- package/modules/government/migrations/government_029_leads.sql +135 -0
- package/modules/government/migrations/government_030_buildouts.sql +111 -0
- package/modules/government/migrations/government_031_treasury.sql +107 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/treasury-borrow.js +62 -0
- package/modules/government/positions.js +107 -0
- package/modules/government/presets.js +149 -0
- package/modules/government/routes/government.js +425 -2
- package/modules/government/routing.js +154 -0
- package/modules/government/treasury-db.js +166 -0
- package/modules/government/treasury.js +241 -0
- 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/board-room.js +2 -0
- package/modules/hall-ui/public/city-draw.js +604 -11
- package/modules/hall-ui/public/city.css +157 -1
- package/modules/hall-ui/public/city.html +37 -7
- package/modules/hall-ui/public/city.js +441 -21
- package/modules/hall-ui/public/city.states.json +110 -5
- 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 +112 -29
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +12 -0
- package/modules/hall-ui/public/diagram-viewer.js +4 -0
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -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/gov-feeds.js +141 -0
- package/modules/hall-ui/public/gov-living.js +1002 -0
- package/modules/hall-ui/public/gov-positions.js +97 -0
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +3 -0
- 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 +3 -3
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.css +334 -0
- package/modules/hall-ui/public/my-agents.html +72 -0
- package/modules/hall-ui/public/my-agents.js +603 -0
- package/modules/hall-ui/public/my-agents.states.json +37 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile-connections.js +40 -28
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +8 -0
- 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 +116 -16
- package/modules/hall-ui/public/street-card.js +18 -1
- package/modules/hall-ui/public/street-deck.css +299 -0
- package/modules/hall-ui/public/street-deck.js +1145 -0
- package/modules/hall-ui/public/street-depth.js +712 -0
- package/modules/hall-ui/public/street-desk.js +5 -2
- package/modules/hall-ui/public/street-draw.js +109 -16
- package/modules/hall-ui/public/street-inside.js +137 -22
- package/modules/hall-ui/public/street-office.js +705 -0
- package/modules/hall-ui/public/street.css +161 -11
- package/modules/hall-ui/public/street.html +41 -11
- package/modules/hall-ui/public/street.js +78 -66
- package/modules/hall-ui/public/street.states.json +129 -21
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +5 -1
- 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/watch.js +14 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/living-city.md +57 -0
- package/modules/hall-ui/records/my-agents.md +21 -0
- package/modules/hall-ui/records/oversight-family.md +1 -0
- package/modules/hall-ui/records/profile-connections.md +10 -0
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/ideas/routes/blockers.js +92 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +17 -1
- package/modules/lifecycle/routes/moving.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +23 -0
- package/modules/onboarding/approval-broadcast.js +8 -0
- package/modules/onboarding/routes/access-requests.js +16 -6
- package/modules/platform-identity/connections.js +13 -2
- package/modules/platform-identity/routes/connections.js +7 -1
- package/modules/public-landing/public/account-private.states.json +2 -2
- package/modules/public-landing/public/account.html +65 -5
- package/modules/public-landing/public/account.states.json +3 -3
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
- package/modules/public-landing/public/assets/world.css +4 -4
- package/modules/public-landing/public/connections-empty.states.json +14 -0
- package/modules/public-landing/public/connections-inactive.states.json +14 -0
- package/modules/public-landing/public/connections.html +450 -0
- package/modules/public-landing/public/connections.probes.json +414 -0
- package/modules/public-landing/public/connections.states.json +17 -0
- package/modules/public-landing/public/contact.html +2 -2
- package/modules/public-landing/public/guild.html +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/privacy.html +2 -2
- package/modules/public-landing/public/projects.html +2 -2
- package/modules/public-landing/public/projects.probes.json +6 -2
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +128 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-run.js +20 -5
- package/scripts/gds/autobongos-tweak-lane.js +262 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/claim.js +2 -0
- package/scripts/gds/government-walk.js +638 -4
- package/scripts/gds/pulse.js +9 -6
- package/scripts/gds/ship-escalation.js +22 -3
- package/scripts/gds/ship-finish.js +5 -4
- package/scripts/gds/ship-merge-gate.js +18 -6
- package/scripts/gds/tweak-halt.js +1 -0
- package/scripts/hall-preview/README.md +41 -1
- package/scripts/hall-preview/pref-writes.js +140 -0
- package/scripts/hall-preview/refresh-fixtures.js +101 -7
- package/scripts/hall-preview/scrub-identity.js +366 -7
- package/scripts/hall-preview/server.js +27 -1
- package/scripts/render-diagrams.sh +4 -2
- package/src/bongos/auth-idp.js +6 -1
- package/src/bongos/project-settings.js +41 -0
- package/src/bongos/route-rank-check.js +27 -0
- package/src/bongos/routes/me.js +68 -7
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +7 -1
- package/src/platform-server.js +10 -0
- package/tests/application_lifecycle.mjs +47 -5
- package/tests/autobongos_tweak_lane.mjs +208 -0
- package/tests/conductor_main_e2e.mjs +21 -0
- package/tests/connections_api.mjs +18 -0
- package/tests/craft_mode_nav.mjs +5 -3
- package/tests/craft_mode_tallies.mjs +52 -16
- package/tests/discord_approvals.mjs +11 -0
- package/tests/discord_channels.mjs +20 -0
- package/tests/fixtures/gov-seated.mjs +64 -0
- package/tests/fixtures/government-ask-cards.json +1284 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/government_agent_asks.mjs +322 -0
- package/tests/government_asks.mjs +408 -0
- package/tests/government_build_rights.mjs +253 -0
- package/tests/government_buildouts.mjs +382 -0
- package/tests/government_counts.mjs +143 -0
- package/tests/government_decision_rules.mjs +5 -1
- package/tests/government_laws.mjs +488 -0
- package/tests/government_laws_session.mjs +321 -0
- package/tests/government_leads.mjs +375 -0
- package/tests/government_positions.mjs +127 -0
- package/tests/government_presets.mjs +215 -0
- package/tests/government_routes.mjs +8 -1
- package/tests/government_routing.mjs +118 -0
- package/tests/government_seed.mjs +5 -0
- package/tests/government_treasury.mjs +394 -0
- package/tests/governor_docket.mjs +26 -2
- package/tests/hall_buildouts.mjs +208 -0
- package/tests/hall_city.mjs +14 -6
- package/tests/hall_city_living.mjs +617 -0
- package/tests/hall_gov_counts.mjs +336 -0
- package/tests/hall_gov_living.mjs +636 -0
- package/tests/hall_gov_real_site.mjs +655 -0
- package/tests/hall_matter_card.mjs +6 -1
- package/tests/hall_my_agents.mjs +481 -0
- package/tests/hall_nav.mjs +107 -4
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_preview_governor_fixtures.mjs +30 -3
- package/tests/hall_preview_scrub.mjs +59 -1
- package/tests/hall_preview_security.mjs +169 -0
- package/tests/hall_reading_world.mjs +44 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_street.mjs +18 -2
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_deck.mjs +609 -0
- package/tests/hall_street_depth.mjs +559 -0
- package/tests/hall_street_desk.mjs +1 -1
- package/tests/hall_street_inside.mjs +29 -5
- package/tests/hall_tweak_editor.mjs +4 -1
- package/tests/hub_account_page.mjs +94 -3
- package/tests/hub_connections_page.mjs +507 -0
- package/tests/hub_invite_badge.mjs +1 -1
- package/tests/idp_device_exchange.mjs +18 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +23 -4
- package/tests/nav_permission_atoms.mjs +4 -1
- package/tests/profile_connections_ui.mjs +88 -25
- package/tests/projects_hub.mjs +15 -0
- package/tests/render_check.mjs +2 -2
- package/tests/ship_broadcast_routing.mjs +57 -16
- package/tests/ship_broadcast_server.mjs +40 -7
- package/tests/ship_merge_gate.mjs +19 -0
- package/tests/ui_design_kit.mjs +2 -2
- package/tests/watch_applications_queue.mjs +23 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
// tests/hall_street_depth.mjs — THE LIVING STREET (task 1004685, the
|
|
2
|
+
// living-city prototype): the street exterior in three planes over the real
|
|
3
|
+
// hall-preview fixtures and nothing else (task 1004717 deleted the prototype
|
|
4
|
+
// overlay), with a seated government in the routes' own shapes
|
|
5
|
+
// (tests/fixtures/gov-seated.mjs).
|
|
6
|
+
//
|
|
7
|
+
// What it pins:
|
|
8
|
+
// 1. without `depth` the street is exactly the street it was (the prototype
|
|
9
|
+
// tests in tests/hall_street.mjs keep holding), and with it the civic row
|
|
10
|
+
// has its deeper side face;
|
|
11
|
+
// 2. BACK: one skyline cluster per OPEN goal, heights from the real rollups,
|
|
12
|
+
// in two rows, never over the Charter;
|
|
13
|
+
// 3. MIDDLE: height is structure — a seated Lead and each committee room
|
|
14
|
+
// add a capsule floor and make the building taller; the plate
|
|
15
|
+
// names the Lead, "you", or "owner" for an unseated building;
|
|
16
|
+
// 4. the agents: exactly one pod per active claim, on the skylane from its
|
|
17
|
+
// goal's plot (or the plot for the other goals) or on the road, a fixed
|
|
18
|
+
// gap apart, every pod on one clock (data is never a speed);
|
|
19
|
+
// 5. construction: the authorised build-out is a site at its % complete, a
|
|
20
|
+
// proposal a ghost annex with one tally dot per seat; an open sitting is
|
|
21
|
+
// a shaft; what needs you lights SALMON windows on the floor that holds
|
|
22
|
+
// it, with a ring that fills with age; nothing else is salmon;
|
|
23
|
+
// 6. the readings as tiles: one number and two words, hidden at zero;
|
|
24
|
+
// 7. all of it real: an overlay handed in changes nothing, nothing is marked
|
|
25
|
+
// prototype and there is no prototype key;
|
|
26
|
+
// 8. the CSS: Keith Clark's parallax, motion at 20 s or slower on transform
|
|
27
|
+
// and offset-distance only, the kill switch, tokens only, the dark twins;
|
|
28
|
+
// 9. the page in a vm: the layers drawn, the deck link, the tiles.
|
|
29
|
+
//
|
|
30
|
+
// Run: node --test --test-reporter=tap tests/hall_street_depth.mjs
|
|
31
|
+
|
|
32
|
+
import { test } from 'node:test';
|
|
33
|
+
// five Leads seated, a room and two build-outs, the docket routed to them (test data)
|
|
34
|
+
import { SEATED, leadsRead, docketRouted } from './fixtures/gov-seated.mjs';
|
|
35
|
+
import assert from 'node:assert/strict';
|
|
36
|
+
import fs from 'node:fs';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
import vm from 'node:vm';
|
|
39
|
+
import { createRequire } from 'node:module';
|
|
40
|
+
import { fileURLToPath } from 'node:url';
|
|
41
|
+
|
|
42
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
|
+
const PUB = path.join(ROOT, 'modules/hall-ui/public');
|
|
44
|
+
const FX = path.join(ROOT, 'scripts/hall-preview/fixtures');
|
|
45
|
+
const require = createRequire(import.meta.url);
|
|
46
|
+
const City = require(path.join(PUB, 'city-draw.js'));
|
|
47
|
+
const S = require(path.join(PUB, 'street-draw.js'));
|
|
48
|
+
const L = require(path.join(PUB, 'gov-living.js'));
|
|
49
|
+
const I = require(path.join(PUB, 'street-inside.js'));
|
|
50
|
+
const Dp = require(path.join(PUB, 'street-depth.js'));
|
|
51
|
+
require(path.join(PUB, 'street-desk.js')); // the line rule's fit, on the global the street reads
|
|
52
|
+
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
53
|
+
const fx = (name) => JSON.parse(fs.readFileSync(path.join(FX, name + '.json'), 'utf8'));
|
|
54
|
+
const NOW = Date.parse('2026-10-09T14:00:00.000Z');
|
|
55
|
+
const clone = (o) => JSON.parse(JSON.stringify(o));
|
|
56
|
+
|
|
57
|
+
function feeds(over = {}) {
|
|
58
|
+
return {
|
|
59
|
+
docket: docketRouted(), goals: fx('goals'), rollups: fx('goals__rollups'), claims: fx('tasks__claimable'),
|
|
60
|
+
tasks: fx('tasks__status-active'), cost: fx('public__cost-summary'), roster: fx('builders__roster'), me: fx('me'),
|
|
61
|
+
leads: leadsRead(), ...over,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function living(f) { return L.livingModel({ ...f, now: NOW }); }
|
|
65
|
+
function street(over = {}, mode = 'dark') {
|
|
66
|
+
const f = feeds(over);
|
|
67
|
+
return S.streetModel({ docket: f.docket, goals: f.goals.goals, rollups: f.rollups.rollups, mode, now: NOW, depth: true, living: f.living === null ? null : living(f), roster: f.roster });
|
|
68
|
+
}
|
|
69
|
+
const openGoals = () => fx('goals').goals.filter((g) => g.status === 'open');
|
|
70
|
+
|
|
71
|
+
// ── 1. the flat street is untouched; the deep one has its deeper sides ──────
|
|
72
|
+
|
|
73
|
+
test('without depth the street is the street it was: no depth layers, the same buildings and pins', () => {
|
|
74
|
+
const f = feeds();
|
|
75
|
+
const base = { docket: f.docket, goals: f.goals.goals, rollups: f.rollups.rollups, mode: 'dark', now: NOW };
|
|
76
|
+
const flat = S.streetModel(base);
|
|
77
|
+
assert.equal('depth' in flat, false);
|
|
78
|
+
assert.deepEqual(S.streetModel({ ...base, depth: false, living: living(f) }), flat, 'living alone changes nothing');
|
|
79
|
+
assert.equal('deck' in flat.hud, false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('the deep civic row draws its side faces DEPTH.side deep; the prototype theme keeps 7', () => {
|
|
83
|
+
const side = (polys) => {
|
|
84
|
+
const m = /^M([\d.]+) ([\d.]+)L([\d.]+) ([\d.]+)L/.exec(polys[0].d);
|
|
85
|
+
return Number(m[3]) - Number(m[1]);
|
|
86
|
+
};
|
|
87
|
+
for (const kind of S.CIVIC_KEYS) {
|
|
88
|
+
const a = []; const b = [];
|
|
89
|
+
S.civic(a, S.theme('dark'), kind, 100, S.G, 84, { evening: false, reviewHeld: true, fixes: false });
|
|
90
|
+
S.civic(b, { ...S.theme('dark'), depth: Dp.DEPTH.side }, kind, 100, S.G, 84, { evening: false, reviewHeld: true, fixes: false });
|
|
91
|
+
assert.equal(side(a), 7, kind);
|
|
92
|
+
assert.equal(side(b), Dp.DEPTH.side, kind);
|
|
93
|
+
}
|
|
94
|
+
const g = [];
|
|
95
|
+
S.gable(g, 0, 100, 30, 10, S.theme('dark').roof);
|
|
96
|
+
assert.match(g[0].d, /L22 83L37 93/, 'a gable with no depth keeps the prototype\'s 7');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── 2. BACK: the skyline ─────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
test('the skyline is one cluster per OPEN goal, in two rows, its heights from the real rollups', () => {
|
|
102
|
+
const m = street();
|
|
103
|
+
const d = m.depth;
|
|
104
|
+
const open = openGoals();
|
|
105
|
+
assert.equal(d.clusters.length, open.length);
|
|
106
|
+
assert.deepEqual(d.clusters.map((c) => c.goal).sort(), open.map((g) => String(g.id)).sort());
|
|
107
|
+
assert.deepEqual([...new Set(d.clusters.map((c) => c.row))].sort(), ['far', 'near']);
|
|
108
|
+
const ro = {};
|
|
109
|
+
fx('goals__rollups').rollups.forEach((r) => { ro[String(r.goal_id)] = r; });
|
|
110
|
+
for (const c of d.clusters) {
|
|
111
|
+
assert.equal(c.total, ro[c.goal] ? ro[c.goal].total : 0, `goal ${c.goal}: its real size`);
|
|
112
|
+
assert.ok(c.h >= 24 && c.h <= 196, `goal ${c.goal}: ${c.h} inside the skyline's range`);
|
|
113
|
+
}
|
|
114
|
+
// the largest, most-built goal stands tallest; and a skyline never tops the Charter
|
|
115
|
+
const big = d.clusters.reduce((a, b) => (b.total > a.total ? b : a));
|
|
116
|
+
assert.ok(d.clusters.every((c) => c.h <= big.h + 60), 'size drives height');
|
|
117
|
+
assert.ok(Math.max(...d.clusters.map((c) => c.h)) < 226, 'the civic row stays the subject');
|
|
118
|
+
// a goal that is not open builds nothing, and no goals leave an empty sky
|
|
119
|
+
const closed = street({ goals: { goals: open.map((g, i) => (i === 0 ? { ...g, status: 'achieved' } : g)) } });
|
|
120
|
+
assert.equal(closed.depth.clusters.length, open.length - 1);
|
|
121
|
+
assert.deepEqual(street({ goals: { goals: [] } }).depth.near, []);
|
|
122
|
+
// more of a goal built raises it
|
|
123
|
+
const r = clone(fx('goals__rollups'));
|
|
124
|
+
const gid = String(open[0].id);
|
|
125
|
+
const row = r.rollups.find((x) => String(x.goal_id) === gid);
|
|
126
|
+
const before = street({ rollups: r }).depth.clusters.find((c) => c.goal === gid).h;
|
|
127
|
+
row.done = row.total;
|
|
128
|
+
assert.ok(street({ rollups: r }).depth.clusters.find((c) => c.goal === gid).h > before, 'built work raises the cluster');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── 3. MIDDLE: height is structure ──────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
test('a seated Lead and each committee room plug in one capsule floor, and the building rises', () => {
|
|
134
|
+
const m = street();
|
|
135
|
+
const caps = Object.fromEntries(m.depth.capsules.map((c) => [c.building, c]));
|
|
136
|
+
for (const l of SEATED) {
|
|
137
|
+
assert.equal(caps[l.building].floors, l.building === 'review' ? 2 : 1, l.building + ': the Lead, and Review\'s Testers room');
|
|
138
|
+
assert.equal(caps[l.building].roles[0], 'lead', `${l.building}: the Lead's office comes first`);
|
|
139
|
+
assert.equal('proto' in caps[l.building], false);
|
|
140
|
+
}
|
|
141
|
+
for (const k of ['charter', 'rules', 'disputes', 'automation']) assert.equal(caps[k], undefined, `${k}: unseated, no capsule`);
|
|
142
|
+
// the Planning Lead leaves office: Planning stands lower, and its pin comes down with it
|
|
143
|
+
const l2 = leadsRead();
|
|
144
|
+
l2.buildings.find((b) => b.building === 'planning').lead = null;
|
|
145
|
+
const m2 = street({ leads: l2 });
|
|
146
|
+
const door = (mm, k) => mm.doors.find((d) => d.key === k);
|
|
147
|
+
const rise = door(m2, 'planning').y - door(m, 'planning').y;
|
|
148
|
+
assert.ok(rise >= Dp.DEPTH.cap - 2 && rise <= Dp.DEPTH.cap + 5, `the Lead's floor adds about a floor of height (${rise})`);
|
|
149
|
+
const pin = (mm, k) => mm.pins.find((p) => p.building === k);
|
|
150
|
+
assert.equal(pin(m2, 'planning').y - pin(m, 'planning').y, rise, 'the pin stands on the new roof');
|
|
151
|
+
// a Lead and a committee room at Charter make Charter taller too
|
|
152
|
+
const l3 = leadsRead();
|
|
153
|
+
const ch = l3.buildings.find((b) => b.building === 'charter');
|
|
154
|
+
ch.lead = { id: 'Lc', building: 'charter', builder: { id: '3', login: 'builder-2013', name: 'Builder 2013' }, charter: 'x', since: '2026-10-09', appointed_by: { item_id: '2' } };
|
|
155
|
+
ch.structure = { standing: [{ id: 5, capsule: 'committee', name: 'C', line: '', state: 'built', progress: 1 }], proposed: [] };
|
|
156
|
+
const m3 = street({ leads: l3 });
|
|
157
|
+
assert.equal(m3.depth.capsules.find((c) => c.building === 'charter').floors, 2);
|
|
158
|
+
assert.ok(door(m3, 'charter').h > door(m, 'charter').h);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('the name plate at street level names the Lead, "you" for the viewer, and "owner" where nobody holds it', () => {
|
|
162
|
+
const m = street();
|
|
163
|
+
const plates = Object.fromEntries(m.depth.plates.map((p) => [p.building, p]));
|
|
164
|
+
assert.equal(m.depth.plates.length, 9, 'every civic building');
|
|
165
|
+
for (const k of ['charter', 'rules', 'disputes', 'automation']) { assert.equal(plates[k].text, 'owner', k); assert.equal(plates[k].seat, 'owner'); }
|
|
166
|
+
const me = String(fx('me').builder.id);
|
|
167
|
+
const roster = fx('builders__roster').builders;
|
|
168
|
+
for (const l of SEATED) {
|
|
169
|
+
const want = l.lead === me ? 'you' : roster.find((b) => String(b.id) === l.lead).display_name;
|
|
170
|
+
assert.equal(plates[l.building].text, want, l.building);
|
|
171
|
+
assert.equal(plates[l.building].seat, 'lead');
|
|
172
|
+
}
|
|
173
|
+
assert.ok(m.depth.plates.every((p) => !('proto' in p)), 'every plate is GET /government/leads\' own');
|
|
174
|
+
assert.ok(m.depth.plates.every((p) => p.y > S.G && p.y < S.G + 30), 'at street level, on the road\'s kerb');
|
|
175
|
+
assert.match(m.doors.find((d) => d.key === 'planning').aria, /, led by you$/);
|
|
176
|
+
assert.match(m.doors.find((d) => d.key === 'charter').aria, /, held by the owner$/);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ── 4. the agents ────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
test('one pod per active claim: on the skylane from its goal\'s plot, or on the road; never more, never fewer', () => {
|
|
182
|
+
const m = street();
|
|
183
|
+
const d = m.depth;
|
|
184
|
+
const claims = fx('tasks__claimable').active_claims;
|
|
185
|
+
const roadN = d.road.reduce((a, r) => a + r.n, 0);
|
|
186
|
+
assert.equal(d.pods.length + roadN, claims.length, 'the count is the agents');
|
|
187
|
+
assert.equal(d.agents, claims.length);
|
|
188
|
+
// the fixture: two claims whose goals stand on the street, two whose goals are past the eighth, one with no goal
|
|
189
|
+
const lanes = Object.fromEntries(d.lanes.map((l) => [l.key, l.n]));
|
|
190
|
+
assert.deepEqual(lanes, { 'lane:more>charter': 1, 'lane:1000095>review': 1, 'lane:1000090>release': 1, 'lane:more>release': 1 });
|
|
191
|
+
assert.deepEqual(d.road.map((r) => [r.building, r.n]), [['review', 1]], 'the claim with no goal works on the road at its building');
|
|
192
|
+
// more claims on one lane ride a fixed gap apart, on one clock
|
|
193
|
+
const c2 = clone(fx('tasks__claimable'));
|
|
194
|
+
const tpl = c2.active_claims.find((c) => c.task_id === '1004557');
|
|
195
|
+
for (let i = 0; i < 3; i++) c2.active_claims.push({ ...tpl, claim_id: 'x' + i });
|
|
196
|
+
const d2 = street({ claims: c2 }).depth;
|
|
197
|
+
const lane = d2.pods.filter((p) => p.lane === 'lane:1000090>release');
|
|
198
|
+
assert.equal(lane.length, 4);
|
|
199
|
+
const at = lane.map((p) => p.at);
|
|
200
|
+
for (let i = 1; i < at.length; i++) assert.equal(Math.round((at[i] - at[i - 1]) * 100) / 100, Dp.DEPTH.podGap, 'a fixed gap');
|
|
201
|
+
for (const p of d2.pods) assert.equal(p.delay, -Math.round(p.at / 100 * Dp.DEPTH.period * 100) / 100, 'every pod on the same lap, offset by its place');
|
|
202
|
+
// no claims: empty lanes, a real quiet state
|
|
203
|
+
const quiet = street({ claims: { active_claims: [] } }).depth;
|
|
204
|
+
assert.deepEqual([quiet.pods.length, quiet.road.length, quiet.lanes.length], [0, 0, 0]);
|
|
205
|
+
assert.match(quiet.label, /No agent is at work/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('a claim with no building rides the whole road; a claim on a goal that is not open is not drawn as that goal', () => {
|
|
209
|
+
const c = clone(fx('tasks__claimable'));
|
|
210
|
+
c.active_claims = [{ claim_id: 'z', task_id: 'nope', builder_id: '3', touches: [] }];
|
|
211
|
+
const d = street({ claims: c }).depth;
|
|
212
|
+
assert.deepEqual(d.road.map((r) => [r.building, r.n]), [[null, 1]]);
|
|
213
|
+
assert.match(d.road[0].path, /^M\d+(\.\d+)? 578H/, 'along the road\'s near lane');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── 5. construction, sittings, and what needs you ───────────────────────────
|
|
217
|
+
|
|
218
|
+
test('the authorised build-out is a live site at its % complete; the pending proposal a ghost annex with a tally of seats', () => {
|
|
219
|
+
const d = street().depth;
|
|
220
|
+
assert.deepEqual(d.sites.map((s) => [s.building, s.progress]), [['release', 0.55]]);
|
|
221
|
+
assert.ok(d.sites[0].lift > 0 && d.sites[0].platform.rise > 0);
|
|
222
|
+
assert.deepEqual(d.proposals.map((p) => p.building), ['treasury']);
|
|
223
|
+
const seats = fx('government__docket').buildings.find((b) => b.key === 'charter').holders.length;
|
|
224
|
+
assert.equal(d.proposals[0].dots.length, seats, 'one dot per council seat');
|
|
225
|
+
assert.ok(d.proposals[0].dots.every((x) => x.vote === 'open'), 'nobody has voted yet');
|
|
226
|
+
// more progress, more finished floors, a taller scaffold
|
|
227
|
+
const done = leadsRead();
|
|
228
|
+
done.buildings.find((b) => b.building === 'release').structure.standing[0].progress = 1;
|
|
229
|
+
const full = Dp.structureOf({ ...S.theme('dark'), depth: Dp.DEPTH.side }, 'release', 100, living(feeds({ leads: done })).buildings.find((b) => b.key === 'release'), { tallyOf: () => ({ for: 0, against: 0, seats: 5 }) });
|
|
230
|
+
const part = Dp.structureOf({ ...S.theme('dark'), depth: Dp.DEPTH.side }, 'release', 100, living(feeds()).buildings.find((b) => b.key === 'release'), { tallyOf: () => ({ for: 0, against: 0, seats: 5 }) });
|
|
231
|
+
assert.ok(full.polys.length > part.polys.length, 'finished floors stack');
|
|
232
|
+
assert.ok(full.site.platform.rise > part.site.platform.rise, 'the scaffold climbs with the work');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('a real board sitting still open is a scaffold proposal and a shaft from its roof, its tally the vote\'s own', () => {
|
|
236
|
+
const doc = clone(fx('government__docket'));
|
|
237
|
+
doc.matters.push({ id: 'board:99', source: 'board', building: 'rules', state: 'needs', title: 'A new rule', short: 'a new rule', rule: 'majority', opened_at: '2026-10-08T00:00:00Z', deadline: null, tally: { for: 2, against: 1, open: 2 }, ref: { type: 'board_item', id: '99' }, acts: [] });
|
|
238
|
+
const d = street({ docket: doc }).depth;
|
|
239
|
+
const shaft = d.shafts.find((s) => s.building === 'rules');
|
|
240
|
+
assert.ok(shaft, 'an open sitting is a shaft');
|
|
241
|
+
const p = d.proposals.find((x) => x.building === 'rules');
|
|
242
|
+
assert.ok(p);
|
|
243
|
+
assert.deepEqual(p.dots.map((x) => x.vote), ['for', 'for', 'against', 'open', 'open']);
|
|
244
|
+
// settled, it is neither
|
|
245
|
+
doc.matters[doc.matters.length - 1].state = 'settled';
|
|
246
|
+
const e = street({ docket: doc }).depth;
|
|
247
|
+
assert.equal(e.shafts.some((s) => s.building === 'rules'), false);
|
|
248
|
+
assert.equal(e.proposals.some((x) => x.building === 'rules'), false);
|
|
249
|
+
// the annex's own sitting is the treasury's shaft
|
|
250
|
+
assert.deepEqual(street().depth.shafts.map((s) => s.building), ['treasury']);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('what needs you lights salmon windows on the floor that holds it, with a ring that fills with age', () => {
|
|
254
|
+
const d = street().depth;
|
|
255
|
+
const doc = docketRouted();
|
|
256
|
+
// salmon only for what needs YOU: a matter the server routes to someone else
|
|
257
|
+
// (another building's Lead) waits on them, so its floor stays dark
|
|
258
|
+
const yours = new Set(living(feeds()).city.asks.filter(L.isOnYou).map((a) => a.id));
|
|
259
|
+
const needs = doc.matters.filter((m) => m.state === 'needs' && yours.has(m.id));
|
|
260
|
+
assert.ok(needs.length > 0 && needs.length < doc.matters.filter((m) => m.state === 'needs').length, 'some of the docket\'s needs are someone else\'s now');
|
|
261
|
+
// every lit row is a real floor of the building's plan holding real matters that need you
|
|
262
|
+
let counted = 0;
|
|
263
|
+
for (const w of d.windows) {
|
|
264
|
+
const b = w.building;
|
|
265
|
+
const here = needs.filter((m) => m.building === b && I.PLAN[b].floors.find((f) => f[0] === w.floor)[2].source === m.source);
|
|
266
|
+
assert.equal(w.count, here.length, w.key);
|
|
267
|
+
assert.ok(w.lit >= 1 && w.lit <= w.count, w.key);
|
|
268
|
+
assert.equal((w.d.match(/M/g) || []).length, w.lit, `${w.key}: one window per matter, up to the face`);
|
|
269
|
+
const oldest = here.map((m) => Date.parse(m.opened_at)).filter(Number.isFinite);
|
|
270
|
+
const age = oldest.length ? Math.floor((NOW - Math.min(...oldest)) / 86400000) : null;
|
|
271
|
+
assert.equal(w.age, age, w.key);
|
|
272
|
+
assert.equal(w.frac, age == null ? 0 : Math.min(1, Math.round(age / Dp.DEPTH.ageFull * 1000) / 1000), w.key);
|
|
273
|
+
counted += w.count;
|
|
274
|
+
}
|
|
275
|
+
assert.equal(counted, needs.length, 'every matter that needs you has its floor');
|
|
276
|
+
assert.ok(d.windows.some((w) => w.building === 'planning'), 'the viewer leads Planning');
|
|
277
|
+
assert.ok(!d.windows.some((w) => ['review', 'release', 'treasury'].includes(w.building)), 'what waits on another Lead lights nothing');
|
|
278
|
+
// the hub as it is (no Lead read, everything on the owner seat): every docket need is the viewer's own
|
|
279
|
+
assert.deepEqual(street({ leads: null, docket: fx('government__docket') }).depth.windows.map((w) => w.key), ['needs:automation:0', 'needs:disputes:0', 'needs:planning:2', 'needs:release:0', 'needs:review:1', 'needs:treasury:4']);
|
|
280
|
+
// moving and settled light nothing; nor does a source no floor holds
|
|
281
|
+
const doc2 = clone(doc);
|
|
282
|
+
doc2.matters.forEach((m) => { m.state = 'moving'; });
|
|
283
|
+
assert.deepEqual(street({ docket: doc2 }).depth.windows, []);
|
|
284
|
+
const doc3 = clone(doc);
|
|
285
|
+
doc3.matters = [{ ...doc.matters[0], source: 'nowhere' }];
|
|
286
|
+
assert.deepEqual(street({ docket: doc3 }).depth.windows, [], 'no floor holds it: no window is invented');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ── 6. the readings as tiles ─────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
test('the six readings are tiles: one number and two words, from the living model where it reads them', () => {
|
|
292
|
+
const t = street().depth.tiles;
|
|
293
|
+
const lm = living(feeds());
|
|
294
|
+
assert.deepEqual(Object.keys(t), ['flow', 'promises', 'money', 'people', 'fairness', 'rules']);
|
|
295
|
+
for (const [k, v] of Object.entries(t)) {
|
|
296
|
+
assert.ok(v, k + ' shows');
|
|
297
|
+
assert.match(v.n, /^\$?[\d,]+$/, k + ': one number');
|
|
298
|
+
assert.equal(v.words.split(' ').length, 2, k + ': two words');
|
|
299
|
+
}
|
|
300
|
+
assert.equal(t.flow.n, String(lm.city.traffic.agents));
|
|
301
|
+
assert.equal(t.money.n, S.money(lm.city.spend.month_usd));
|
|
302
|
+
assert.equal(t.people.n, String(fx('builders__roster').builders.filter((b) => b.status === 'active').length));
|
|
303
|
+
assert.equal(t.promises.n, String(fx('government__docket').readings.find((r) => r.key === 'promises').value.kept));
|
|
304
|
+
assert.equal(t.fairness.n, String(fx('government__docket').readings.find((r) => r.key === 'fairness').value));
|
|
305
|
+
assert.equal(t.rules.n, String(lm.city.laws.length));
|
|
306
|
+
assert.ok(Object.values(t).every((v) => !('proto' in v)), 'every tile is a real read');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('a tile is hidden at zero and when nothing real holds it; never "module not enabled"', () => {
|
|
310
|
+
const off = clone(fx('government__docket'));
|
|
311
|
+
off.readings.forEach((r) => { r.status = 'off'; r.figure = null; r.value = null; r.reason = 'The module that holds this is not enabled here.'; });
|
|
312
|
+
off.matters = [];
|
|
313
|
+
const t = Dp.readingTiles(off, null, null);
|
|
314
|
+
assert.deepEqual(Object.values(t), [null, null, null, null, null, null]);
|
|
315
|
+
const z = Dp.readingTiles(off, { city: { traffic: { agents: 0 }, spend: { month_usd: 0 }, laws: [] } }, { builders: [] });
|
|
316
|
+
assert.deepEqual(Object.values(z), [null, null, null, null, null, null], 'zero agents, no spend, nobody, no laws: all hidden');
|
|
317
|
+
// with no living model, a live reading's own figure stands in, if it is a number and at most two words
|
|
318
|
+
const live = fx('government__docket');
|
|
319
|
+
const f = Dp.readingTiles(live, null, null);
|
|
320
|
+
assert.deepEqual([f.flow.n, f.flow.words], ['7', 'stuck']);
|
|
321
|
+
assert.deepEqual([f.rules.n, f.rules.words], ['1', 'rule']);
|
|
322
|
+
assert.equal(Dp.readingTiles({ readings: [{ key: 'flow', status: 'live', figure: '7 of 9 stuck in review' }], matters: [] }, null, null).flow, null);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ── 7. all of it real ────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
test('an overlay handed in changes nothing; the Leads not read, no Lead, capsule or build-out is invented; no prototype key', () => {
|
|
328
|
+
const a = street().depth;
|
|
329
|
+
const b = street({ overlay: { leads: [{ building: 'rules', lead: '1000301', committees: [] }], buildouts: [{ id: 'x', building: 'rules', authorised: true, progress: 0.1 }], laws: [{ id: 'y', text: 'An invented law' }] } }).depth;
|
|
330
|
+
assert.deepEqual(b, a, 'the overlay is not read');
|
|
331
|
+
assert.equal('key' in a, false, 'there is no prototype key');
|
|
332
|
+
const c = street({ leads: null }).depth;
|
|
333
|
+
assert.deepEqual([c.plates.length, c.capsules.length, c.sites.length], [0, 0, 0], 'no Leads read: no plate, no capsule, no build-out going up');
|
|
334
|
+
// without the living model at all the deep street still draws its real skyline and windows
|
|
335
|
+
const none = S.streetModel({ docket: fx('government__docket'), goals: fx('goals').goals, rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true, living: null, roster: null }).depth;
|
|
336
|
+
assert.deepEqual(none.clusters, a.clusters);
|
|
337
|
+
assert.deepEqual(none.windows, street({ leads: null, docket: fx('government__docket') }).depth.windows, 'no living model and the hub as it is light the same, the docket\'s own');
|
|
338
|
+
assert.equal(none.agents, 0);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('street-depth.js: its model half is pure, and the two drawing files require each other in one direction only', () => {
|
|
342
|
+
const src = read('street-depth.js');
|
|
343
|
+
const cut = src.indexOf('// THE PAGE HALF.');
|
|
344
|
+
assert.ok(cut > 0, 'the page half is marked');
|
|
345
|
+
const model = src.slice(0, cut).replace(/^\s*\/\/.*$/gm, '');
|
|
346
|
+
assert.doesNotMatch(model, /\bdocument\b|\bfetch\s*\(|Date\.now|new Date\(\)|localStorage|setTimeout/, 'the model half reads no DOM, no network, no clock');
|
|
347
|
+
// street-depth.js needs street-draw.js (the kit) and street-inside.js (the floor plan);
|
|
348
|
+
// street-draw.js finds it on its global, so neither drawing file requires back
|
|
349
|
+
const draw = read('street-draw.js').split('\n').filter((l) => !/^\s*(\/\/|\*)/.test(l)).join('\n');
|
|
350
|
+
const inside = read('street-inside.js').split('\n').filter((l) => !/^\s*(\/\/|\*)/.test(l)).join('\n');
|
|
351
|
+
assert.doesNotMatch(draw, /require\('\.\/street-(inside|depth)\.js'\)/, 'street-draw.js requires neither');
|
|
352
|
+
assert.doesNotMatch(inside, /require\('\.\/street-depth\.js'\)/);
|
|
353
|
+
assert.match(src, /require\('\.\/street-draw\.js'\)/);
|
|
354
|
+
// a street asked to be deep without the depth kit is drawn flat, never half-drawn
|
|
355
|
+
const before = globalThis.GovStreetDepth;
|
|
356
|
+
delete globalThis.GovStreetDepth;
|
|
357
|
+
try {
|
|
358
|
+
const flat = S.streetModel({ docket: fx('government__docket'), goals: openGoals(), rollups: fx('goals__rollups').rollups, mode: 'dark', now: NOW, depth: true });
|
|
359
|
+
assert.equal(flat.depth, undefined);
|
|
360
|
+
} finally { globalThis.GovStreetDepth = before; }
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('street-draw.js stays pure: no DOM, no fetch, no clock', () => {
|
|
364
|
+
const src = read('street-draw.js').replace(/^\s*\/\/.*$/gm, '');
|
|
365
|
+
assert.doesNotMatch(src, /\bdocument\b|\bfetch\s*\(|Date\.now|new Date\(\)|localStorage|setTimeout/);
|
|
366
|
+
const a = street(), b = street();
|
|
367
|
+
assert.deepEqual(a, b, 'the same feeds give the same street');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ── 8. the CSS ───────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
const CSS = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
373
|
+
const rule = (sel) => { const m = new RegExp(sel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ' \\{([^}]*)\\}').exec(CSS); return m ? m[1] : null; };
|
|
374
|
+
|
|
375
|
+
test('the parallax is Keith Clark\'s, sideways, CSS only: a perspective window, kept 3D, each row deeper and scaled back', () => {
|
|
376
|
+
assert.match(rule('.street-window'), /perspective: 1px; perspective-origin: 50% 50%;/);
|
|
377
|
+
assert.match(rule('.street-sizer'), /overflow: visible; transform-style: preserve-3d;/, 'an overflow clip would flatten the 3D');
|
|
378
|
+
assert.match(rule('.street-board'), /transform-style: preserve-3d;/);
|
|
379
|
+
for (const [row, z] of [['near', 1], ['far', 2]]) {
|
|
380
|
+
const m = new RegExp(`translateZ\\(-(\\d+)px\\) scale\\((\\d+)\\)`).exec(rule('.street-depth-back--' + row));
|
|
381
|
+
assert.equal(Number(m[1]), z, row);
|
|
382
|
+
assert.equal(Number(m[2]), 1 + z, `${row}: scaled back to its own size at perspective 1px`);
|
|
383
|
+
}
|
|
384
|
+
assert.match(rule('.street-depth-back'), /width: 1px; height: 1px;[^}]*transform-origin: var\(--street-depth-ox, 720px\) 446px;/, 'a 1 px box: its depth never widens the scroll; origin the band\'s middle (196 + 500 / 2, task 1004718)');
|
|
385
|
+
const scrolls = [...read('street.js').matchAll(/addEventListener\('scroll',\s*([A-Za-z]+)/g)].map((m) => m[1]);
|
|
386
|
+
assert.deepEqual([...new Set(scrolls)], ['placeCard'], 'the one scroll handler places the card; none moves the street');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('motion only says "this is live": 20 s or slower, transform and offset-distance only, dead under the kill switch', () => {
|
|
390
|
+
const block = CSS.slice(CSS.indexOf('.street-depth-layer'), CSS.indexOf('.street-card {'));
|
|
391
|
+
const anims = [...block.matchAll(/animation: (street-depth-[a-z]+) (\d+)s/g)];
|
|
392
|
+
assert.ok(anims.length >= 7, 'the pods, the train, the ring, the platform, the jib, the line, the slab');
|
|
393
|
+
for (const [, name, s] of anims) assert.ok(Number(s) >= 20, `${name}: ${s}s`);
|
|
394
|
+
assert.equal(Number(/street-depth-travel (\d+)s/.exec(block)[1]), Dp.DEPTH.period, 'the pods\' lap is the model\'s period');
|
|
395
|
+
for (const m of block.matchAll(/@keyframes (street-depth-[a-z]+) \{([^@]*?)\}\s*\}/g)) {
|
|
396
|
+
const props = [...m[2].matchAll(/([a-z-]+):/g)].map((x) => x[1]);
|
|
397
|
+
assert.ok(props.every((p) => p === 'transform' || p === 'offset-distance'), `${m[1]} animates ${props}`);
|
|
398
|
+
}
|
|
399
|
+
assert.doesNotMatch(block, /stroke-dashoffset|blur\(/);
|
|
400
|
+
const kill = CSS.slice(CSS.indexOf('@media (prefers-reduced-motion: reduce)'));
|
|
401
|
+
assert.match(kill, /animation: none !important; transition: none !important;/);
|
|
402
|
+
assert.match(kill, /\.street-window \{ perspective: none; \}/);
|
|
403
|
+
assert.match(kill, /\.street-depth-back \{ transform: none; \}/);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('every colour is a token; salmon only for what needs you; the dark twins stay byte-identical', () => {
|
|
407
|
+
const block = CSS.slice(CSS.indexOf('.street-depth-layer'), CSS.indexOf('.street-card {'));
|
|
408
|
+
assert.doesNotMatch(block, /(fill|stroke|background|color|border[a-z-]*): #/, 'no literal paint');
|
|
409
|
+
// the only literal is the mask's black, a mask (DESIGN.md: black with alpha as a scrim or mask)
|
|
410
|
+
for (const m of block.matchAll(/#[0-9a-f]{3,6}\b/gi)) assert.equal(m[0], '#000', 'a literal is only the mask\'s black');
|
|
411
|
+
for (const m of block.matchAll(/([^{}]+)\{([^}]*var\(--accent\)[^}]*)\}/g)) {
|
|
412
|
+
assert.match(m[1], /needs|age__arc|deck/, `salmon spent on ${m[1].trim()}`);
|
|
413
|
+
}
|
|
414
|
+
const twin = (re) => CSS.match(re)[1].replace(/\s+/g, ' ').trim();
|
|
415
|
+
assert.equal(twin(/html\[data-theme="dark"\] body\[data-page="street"\] \{([^}]*)\}/), twin(/html:not\(\[data-theme\]\) body\[data-page="street"\] \{([^}]*)\}/));
|
|
416
|
+
assert.match(CSS, /html\[data-theme="dark"\] body\[data-page="street"\] \{[^}]*--street-depth-face:/, 'the depth tokens have dark twins');
|
|
417
|
+
assert.doesNotMatch(CSS, /:root/);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('the markup: the back rows and the sky behind the street, the front over it, the deck behind "to do"', () => {
|
|
421
|
+
const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
|
|
422
|
+
const order = ['streetEvening', 'streetDepthFar', 'streetDepthNear', 'streetDepthSky', 'streetSvg', 'streetDepthFront', 'streetCrates', 'streetPins'].map((id) => html.indexOf(`id="${id}"`));
|
|
423
|
+
assert.equal(html.indexOf('streetDepthKey'), -1, 'no prototype key');
|
|
424
|
+
assert.ok(order.every((i) => i > 0));
|
|
425
|
+
assert.deepEqual([...order].sort((a, b) => a - b), order, 'in paint order');
|
|
426
|
+
assert.match(html, /<a class="street-hud__item street-depth-deck" id="hudDeck" href="\/builders\/street#deck">\s*<span class="street-hud__dot" id="hudDot"[^>]*><\/span>\s*<span class="street-label">to do<\/span>\s*<span class="street-hud__n" id="hudNeeds">/);
|
|
427
|
+
assert.ok(html.indexOf('/builders/gov-living.js') > 0 && html.indexOf('/builders/gov-living.js') < html.indexOf('/builders/street.js'), 'the living model loads before the page');
|
|
428
|
+
assert.equal(Dp.DECK_HREF, '/builders/street#deck');
|
|
429
|
+
assert.equal(S.parseHash('#deck'), null, 'the deck is not a building or a district: the street stays');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ── 9. the page in a vm ──────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
function fakeDom() {
|
|
435
|
+
const byId = new Map();
|
|
436
|
+
const mk = (tag) => {
|
|
437
|
+
const el = {
|
|
438
|
+
tagName: tag, attrs: {}, children: [], className: '', listeners: {}, _text: '', hidden: false, parentNode: null,
|
|
439
|
+
style: { setProperty(k, v) { this[k] = v; } },
|
|
440
|
+
clientWidth: 1440, clientHeight: 708, offsetLeft: 0, scrollLeft: 0,
|
|
441
|
+
get firstChild() { return this.children[0] || null; },
|
|
442
|
+
setAttribute(k, v) { this.attrs[k] = String(v); if (k === 'id') byId.set(String(v), this); if (k === 'class') this.className = String(v); },
|
|
443
|
+
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; },
|
|
444
|
+
appendChild(c) { this.children.push(c); c.parentNode = this; return c; },
|
|
445
|
+
removeChild(c) { this.children = this.children.filter((x) => x !== c); return c; },
|
|
446
|
+
addEventListener(t, fn) { (this.listeners[t] = this.listeners[t] || []).push(fn); },
|
|
447
|
+
focus() {},
|
|
448
|
+
set textContent(v) { this._text = String(v); this.children = []; },
|
|
449
|
+
get textContent() { return this._text; },
|
|
450
|
+
get innerHTML() { throw new Error('no innerHTML'); },
|
|
451
|
+
set innerHTML(v) { throw new Error('no innerHTML'); },
|
|
452
|
+
};
|
|
453
|
+
return el;
|
|
454
|
+
};
|
|
455
|
+
const ids = ['streetMode', 'streetModeIcon', 'streetModeWord', 'streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', 'streetEvening', 'streetEveningLine', 'hudNeeds', 'hudMoving', 'hudShare', 'hudDot', 'hudDeck', 'streetHome', 'streetCard', 'streetCardWhere', 'streetCardWhen', 'streetCardTitle', 'streetCardBody', 'streetCardClose', 'streetToast', 'streetDepthFar', 'streetDepthNear', 'streetDepthSky', 'streetDepthFront'];
|
|
456
|
+
for (const id of ids) mk('x').setAttribute('id', id);
|
|
457
|
+
const document = { documentElement: mk('html'), getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_ns, t) => mk(t) };
|
|
458
|
+
return { document, byId };
|
|
459
|
+
}
|
|
460
|
+
const walk = (el, out = []) => { out.push(el); el.children.forEach((c) => walk(c, out)); return out; };
|
|
461
|
+
|
|
462
|
+
async function bootPage({ withLiving = true } = {}) {
|
|
463
|
+
const { document, byId } = fakeDom();
|
|
464
|
+
const calls = [];
|
|
465
|
+
const F = {
|
|
466
|
+
'/api/bongos/government/docket': docketRouted(), '/api/bongos/goals': fx('goals'), '/api/bongos/goals/rollups': fx('goals__rollups'),
|
|
467
|
+
'/api/bongos/government/leads': leadsRead(),
|
|
468
|
+
'/api/bongos/tasks/claimable?limit=1': fx('tasks__claimable'), '/api/bongos/tasks?status=active': fx('tasks__status-active'),
|
|
469
|
+
'/api/bongos/public/cost-summary': fx('public__cost-summary'), '/api/bongos/builders/roster': fx('builders__roster'), '/api/bongos/me': fx('me'),
|
|
470
|
+
// the living model's reads, as gov-feeds.js names them
|
|
471
|
+
'/api/bongos/tasks?status=active&limit=200': fx('tasks__status-active'), '/api/bongos/blockers?limit=100': fx('blockers'),
|
|
472
|
+
'/api/bongos/moving': fx('moving'), '/api/bongos/autonomy/fence': fx('autonomy__fence'), '/api/bongos/government/constitution': fx('government__constitution'),
|
|
473
|
+
};
|
|
474
|
+
const fetch = (url) => { calls.push(url); const b = F[url]; return Promise.resolve(b === undefined ? { ok: false, json: async () => ({}) } : { ok: true, json: async () => b }); };
|
|
475
|
+
const window = { innerWidth: 1440, innerHeight: 900, location: { hash: '' }, localStorage: { getItem: () => 'dark', setItem() {} }, matchMedia: () => ({ matches: true, addEventListener() {} }), addEventListener() {} };
|
|
476
|
+
class MutationObserver { observe() {} }
|
|
477
|
+
window.fetch = fetch;
|
|
478
|
+
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, MutationObserver, Set });
|
|
479
|
+
ctx.globalThis = ctx;
|
|
480
|
+
for (const f of (withLiving ? ['gov-living.js', 'gov-feeds.js'] : []).concat(['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-depth.js', 'street-desk.js', 'street.js'])) vm.runInContext(read(f), ctx);
|
|
481
|
+
await window.GovStreetPage.ready;
|
|
482
|
+
for (let i = 0; i < 5; i++) await new Promise((r) => setImmediate(r));
|
|
483
|
+
return { byId, calls, window };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
test('the page draws the living street: the skyline, a pod per claim, the plates, construction, the windows', async () => {
|
|
487
|
+
const p = await bootPage();
|
|
488
|
+
const all = (id) => walk(p.byId.get(id));
|
|
489
|
+
const cls = (id, c) => all(id).filter((e) => (e.className || '').split(' ').includes(c));
|
|
490
|
+
for (const u of ['/api/bongos/tasks/claimable?limit=1', '/api/bongos/government/leads']) assert.ok(p.calls.includes(u), u);
|
|
491
|
+
assert.ok(!p.calls.some((u) => /governor-overlay/.test(u)), 'no overlay is read');
|
|
492
|
+
assert.equal(p.calls.filter((u) => u === '/api/bongos/me').length, 1, 'the living feeds are read once');
|
|
493
|
+
assert.equal(cls('streetDepthNear', 'street-depth-sil__face').length, 1);
|
|
494
|
+
assert.equal(cls('streetDepthFar', 'street-depth-sil__face').length, 1);
|
|
495
|
+
const claims = fx('tasks__claimable').active_claims.length;
|
|
496
|
+
assert.equal(cls('streetDepthSky', 'street-depth-pod').length + cls('streetDepthFront', 'street-depth-pod').length, claims, 'one pod per agent');
|
|
497
|
+
const pod = cls('streetDepthSky', 'street-depth-pod')[0];
|
|
498
|
+
assert.match(pod.style.offsetPath, /^path\('M/);
|
|
499
|
+
assert.match(pod.style.animationDelay, /^-[\d.]+s$/);
|
|
500
|
+
assert.equal(cls('streetDepthFront', 'street-depth-plate').length, 9);
|
|
501
|
+
assert.equal(all('streetDepthFront').concat(all('streetDepthSky')).filter((e) => 'data-proto' in e.attrs).length, 0, 'nothing is marked prototype');
|
|
502
|
+
assert.equal(cls('streetDepthFront', 'street-depth-jib').length, 1, 'the live site\'s crane');
|
|
503
|
+
assert.equal(cls('streetDepthFront', 'street-depth-seat').length, 5, 'the tally\'s seats');
|
|
504
|
+
assert.equal(cls('streetDepthFront', 'street-depth-needs').length, street().depth.windows.length, 'the floors that hold what needs you light');
|
|
505
|
+
assert.equal(cls('streetDepthSky', 'street-depth-shaft').length, 1);
|
|
506
|
+
// the moving nodes stay well under a hundred
|
|
507
|
+
const moving = ['street-depth-pod', 'street-depth-train', 'street-depth-jib', 'street-depth-line', 'street-depth-slab', 'street-depth-platform', 'street-depth-shaft__ring'];
|
|
508
|
+
const n = ['streetDepthSky', 'streetDepthFront'].flatMap((id) => all(id)).filter((e) => moving.some((c) => (e.className || '').split(' ').includes(c))).length;
|
|
509
|
+
assert.ok(n < 100, `${n} moving nodes`);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('"to do" opens the deck; the readings are tiles, one number and two words', async () => {
|
|
513
|
+
const p = await bootPage();
|
|
514
|
+
const deck = p.byId.get('hudDeck');
|
|
515
|
+
assert.equal(deck.attrs.href, '/builders/street#deck');
|
|
516
|
+
// the header's count is the one every surface prints (gov-living.js countsOf)
|
|
517
|
+
const onYou = L.countsOf(living(feeds())).on_you;
|
|
518
|
+
assert.equal(deck.attrs['aria-label'], onYou + ' to do. Decide them in a row.');
|
|
519
|
+
assert.equal(p.byId.get('hudNeeds').textContent, String(onYou), 'the city\'s count, not the docket\'s own');
|
|
520
|
+
assert.equal(p.byId.get('hudMoving').textContent, String(fx('moving').moving), 'moving is GET /moving');
|
|
521
|
+
const signs = p.byId.get('streetSigns').children;
|
|
522
|
+
assert.equal(signs.length, 6);
|
|
523
|
+
for (const a of signs) {
|
|
524
|
+
assert.equal(a.hidden, false);
|
|
525
|
+
const fig = a.children[1];
|
|
526
|
+
assert.equal(fig.className, 'street-sign__figure street-depth-tile__fig');
|
|
527
|
+
assert.match(fig.children[0].textContent, /^\$?[\d,]+$/);
|
|
528
|
+
assert.equal(fig.children[1].textContent.split(' ').length, 2);
|
|
529
|
+
}
|
|
530
|
+
assert.ok(!('data-proto' in signs[5].children[1].children[0].attrs), 'the laws are a real read');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('with no living model (gov-living.js not loaded) the page reads no more feeds and still draws the deep street', async () => {
|
|
534
|
+
const p = await bootPage({ withLiving: false });
|
|
535
|
+
assert.ok(!p.calls.some((u) => /claimable|governor-overlay|cost-summary|roster/.test(u)));
|
|
536
|
+
assert.equal(walk(p.byId.get('streetDepthNear')).filter((e) => e.className === 'street-depth-sil__face').length, 1, 'the real skyline');
|
|
537
|
+
assert.equal(walk(p.byId.get('streetDepthFront')).filter((e) => e.className === 'street-depth-plate').length, 0, 'no Leads');
|
|
538
|
+
assert.equal(p.byId.get('streetSigns').children.filter((a) => !a.hidden).length, 6, 'the docket\'s own readings stand in');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// task 1004718: the street band is cropped to what it draws. The empty sky
|
|
542
|
+
// over the skylanes and the bare ground under the brackets are cut, so a wide
|
|
543
|
+
// stage fills with the street rather than black sky; everything drawn in the
|
|
544
|
+
// sky (the first skylane, a sitting's shaft, the evening's words) stays inside.
|
|
545
|
+
test('the band is cropped to what it draws: no empty sky over the skylanes', () => {
|
|
546
|
+
const js = read('street.js'), css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
547
|
+
const m = /var BAND_TOP = (\d+), BAND_H = (\d+);/.exec(js);
|
|
548
|
+
assert.ok(m, 'the band is named once');
|
|
549
|
+
const top = Number(m[1]), h = Number(m[2]);
|
|
550
|
+
assert.ok(top >= 128, 'the empty sky over the first skylane is cut');
|
|
551
|
+
const ruleOf = (sel) => (css.split('\n').find((l) => l.startsWith(sel + ' {')) || '');
|
|
552
|
+
assert.ok(ruleOf('.street-board').includes('translateY(-' + top + 'px)'), 'the board is lifted by the band\'s top');
|
|
553
|
+
assert.ok(ruleOf('.street-depth-back').includes('720px) ' + (top + h / 2) + 'px;'), 'the parallax turns on the band\'s middle');
|
|
554
|
+
assert.ok(Dp.DEPTH.skyTop - top >= 40, 'the first skylane crests inside the band');
|
|
555
|
+
assert.ok(Dp.DEPTH.shaftTop >= top, 'a sitting\'s shaft starts inside the band');
|
|
556
|
+
const eve = Number(/\.street-evening \{[^}]*top: (\d+)px/.exec(css)[1]);
|
|
557
|
+
assert.ok(eve >= top, 'the evening\'s words stand inside the band');
|
|
558
|
+
assert.ok(top + h >= S.G + 120, 'the brackets and their names stay inside the band');
|
|
559
|
+
});
|
|
@@ -303,7 +303,7 @@ async function bootPage(hash, ms, { refuse = false, over = {}, xhr = null, answe
|
|
|
303
303
|
};
|
|
304
304
|
const ctx = vm.createContext({ window, document, fetch, Promise, Math, String, Number, Array, Object, JSON, Date, console, encodeURIComponent, decodeURIComponent, isNaN, isFinite, Map, Set });
|
|
305
305
|
ctx.globalThis = ctx;
|
|
306
|
-
for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
|
|
306
|
+
for (const f of ['city-draw.js', 'street-draw.js', 'street-card.js', 'street-inside.js', 'street-depth.js', 'street-desk.js', 'street.js']) vm.runInContext(read(f), ctx);
|
|
307
307
|
await window.GovStreetPage.ready;
|
|
308
308
|
await new Promise((r) => setTimeout(r, 0));
|
|
309
309
|
const deskEl = () => (byId.get('insideOpenA').hidden ? byId.get('insideOpenB') : byId.get('insideOpenA'));
|