@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,336 @@
|
|
|
1
|
+
// tests/hall_gov_counts.mjs — ONE SOURCE OF COUNTS for the living city (task
|
|
2
|
+
// 1004685, the integration pass of the living-city prototype).
|
|
3
|
+
//
|
|
4
|
+
// Before this pass the same fixtures printed five different numbers: the city's
|
|
5
|
+
// plaque "24 on you", the street's header "needs you 37 · moving 0", its readings
|
|
6
|
+
// "5 agents working" (but the city said "5 agents at work"), the deck "38 to decide · 23 on your seats" and the shell's
|
|
7
|
+
// tally "needs you 9 · moving 6". Each surface counted for itself, over its own
|
|
8
|
+
// reads. Now every number is gov-living.js countsOf over the reads gov-feeds.js
|
|
9
|
+
// makes once per page, and each has one label (COUNT_WORDS after a number, "24
|
|
10
|
+
// to do"; COUNT_LABELS as a heading, "to do 24": the owner's word for the main
|
|
11
|
+
// count, task 1004701).
|
|
12
|
+
//
|
|
13
|
+
// What it pins, over the REAL harness fixtures:
|
|
14
|
+
// 1. countsOf's numbers, and that moving can never read under agents at work;
|
|
15
|
+
// 2. COUNT_FEEDS alone (what the shell's tally reads) give the same counts as
|
|
16
|
+
// every feed (what the city and the street read), with and without answers;
|
|
17
|
+
// 3. each surface, rendered from the same fixture, prints those numbers: the
|
|
18
|
+
// city's plaque, door and line; the street's header and its flow reading;
|
|
19
|
+
// the deck's headline and its scope chips; the shell's tally (the REAL
|
|
20
|
+
// craft-mode.js + gov-feeds.js in a vm, over a fetch serving the fixtures);
|
|
21
|
+
// 4. an answer moves every surface by the same one;
|
|
22
|
+
// 5. the labels are COUNT_WORDS, verbatim, and the retired ones are gone.
|
|
23
|
+
//
|
|
24
|
+
// Run: node --test --test-reporter=tap tests/hall_gov_counts.mjs
|
|
25
|
+
|
|
26
|
+
import { test } from 'node:test';
|
|
27
|
+
import assert from 'node:assert/strict';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import vm from 'node:vm';
|
|
31
|
+
import { createRequire } from 'node:module';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
35
|
+
const PUB = path.join(ROOT, 'modules/hall-ui/public');
|
|
36
|
+
const FX = path.join(ROOT, 'scripts/hall-preview/fixtures');
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
const L = require(path.join(PUB, 'gov-living.js'));
|
|
39
|
+
const routing = require(path.join(ROOT, 'modules', 'government', 'routing.js'));
|
|
40
|
+
const serverCounts = require(path.join(ROOT, 'modules', 'government', 'counts.js'));
|
|
41
|
+
const F = require(path.join(PUB, 'gov-feeds.js'));
|
|
42
|
+
const City = require(path.join(PUB, 'city-draw.js'));
|
|
43
|
+
const S = require(path.join(PUB, 'street-draw.js'));
|
|
44
|
+
require(path.join(PUB, 'street-inside.js'));
|
|
45
|
+
const depthFile = path.join(PUB, 'street-depth.js');
|
|
46
|
+
require(depthFile);
|
|
47
|
+
require(path.join(PUB, 'street-card.js'));
|
|
48
|
+
require(path.join(PUB, 'street-desk.js'));
|
|
49
|
+
const K = require(path.join(PUB, 'street-deck.js'));
|
|
50
|
+
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
51
|
+
const fx = (n) => JSON.parse(fs.readFileSync(path.join(FX, n + '.json'), 'utf8'));
|
|
52
|
+
const NOW = Date.parse('2026-10-09T15:00:00.000Z');
|
|
53
|
+
|
|
54
|
+
// the route each feed key is served from, and its fixture (the harness's own names)
|
|
55
|
+
const FIXTURE = {
|
|
56
|
+
docket: 'government__docket', counts: 'government__counts', constitution: 'government__constitution', laws: 'government__laws', leads: 'government__leads', goals: 'goals', rollups: 'goals__rollups',
|
|
57
|
+
claims: 'tasks__claimable', tasks: 'tasks__status-active', cost: 'public__cost-summary', roster: 'builders__roster',
|
|
58
|
+
me: 'me', blockers: 'blockers', fence: 'autonomy__fence', moving: 'moving',
|
|
59
|
+
// the pots, the reserve, the spend cap (task 1004712)
|
|
60
|
+
treasury: 'government__treasury',
|
|
61
|
+
presets: 'government__presets',
|
|
62
|
+
};
|
|
63
|
+
// The hub has seated no Lead yet (GET /government/leads, task 1004709), so every
|
|
64
|
+
// ask there falls to the owner seat — every Archon, the viewer among them — and
|
|
65
|
+
// nothing would wait on others. These counts are about one source, not about who
|
|
66
|
+
// leads, so the read here seats five Leads, as if appointed by People votes: asks
|
|
67
|
+
// on the viewer's seats and asks on other people's, both present.
|
|
68
|
+
const SEATED = [
|
|
69
|
+
{ building: 'planning', lead: '1000025', since: '2026-09-12', charter: { scope: 'Planning floors: goals, priorities, the order of work.' } },
|
|
70
|
+
{ building: 'release', lead: '1000027', since: '2026-09-05', charter: { scope: 'Release floors and the path to going live.' } },
|
|
71
|
+
{ building: 'review', lead: '1000095', since: '2026-09-20', charter: { scope: 'Who checks finished work, and how.' } },
|
|
72
|
+
{ building: 'treasury', lead: '1000094', since: '2026-09-28', charter: { scope: 'Pots and the reserve. A new spending arm needs a board vote.' } },
|
|
73
|
+
{ building: 'people', lead: '1000070', since: '2026-10-02', charter: { scope: 'Who joins, and the ranks they hold.' } },
|
|
74
|
+
];
|
|
75
|
+
function seatedLeads() {
|
|
76
|
+
const read = fx('government__leads');
|
|
77
|
+
const roster = fx('builders__roster').builders;
|
|
78
|
+
for (const l of SEATED) {
|
|
79
|
+
const b = read.buildings.find((x) => x.building === l.building);
|
|
80
|
+
const p = roster.find((r) => String(r.id) === String(l.lead)) || { id: l.lead };
|
|
81
|
+
b.lead = { id: 'L' + l.building, building: l.building, builder: { id: String(p.id), login: p.github_login || null, name: p.display_name || null }, charter: l.charter.scope, since: l.since, appointed_by: { item_id: '1' } };
|
|
82
|
+
b.answers = { seat: 'lead:' + l.building, builder_ids: [String(p.id)], fell_to_owner: false };
|
|
83
|
+
}
|
|
84
|
+
return read;
|
|
85
|
+
}
|
|
86
|
+
// The dry pot's borrow (task 1004712) waits on the Treasury's Lead. Seated above
|
|
87
|
+
// (not the viewer), it is routed to them by the server's own rule:
|
|
88
|
+
// the ask on others these counts need.
|
|
89
|
+
function seatedDocket() {
|
|
90
|
+
const d = fx('government__docket');
|
|
91
|
+
const read = seatedLeads();
|
|
92
|
+
const seats = { leads: read.buildings.filter((b) => b.lead).map((b) => ({ building: b.building, builder_id: b.lead.builder.id })), owner: read.owner, holders: {} };
|
|
93
|
+
d.matters.filter((m) => m.source === 'treasury_spending').forEach((m) => {
|
|
94
|
+
const { route, ...bare } = m;
|
|
95
|
+
m.route = routing.routeMatter(bare, d.viewer.id, seats, Date.parse(d.generated_at));
|
|
96
|
+
});
|
|
97
|
+
return d;
|
|
98
|
+
}
|
|
99
|
+
// GET /government/counts over that same seated docket, by the server's own rule
|
|
100
|
+
// (modules/government/counts.js, task 1004716)
|
|
101
|
+
function seatedCounts() {
|
|
102
|
+
const d = seatedDocket();
|
|
103
|
+
return Object.assign({ viewer: d.viewer, generated_at: d.generated_at }, serverCounts.countsOf(d, { moving: fx('moving').moving, agents_at_work: fx('tasks__claimable').active_claims.length }));
|
|
104
|
+
}
|
|
105
|
+
function fxFor(name) { return name === FIXTURE.leads ? seatedLeads() : name === FIXTURE.docket ? seatedDocket() : name === FIXTURE.counts ? seatedCounts() : fx(name); }
|
|
106
|
+
function raw(keys = Object.keys(F.FEEDS)) {
|
|
107
|
+
const out = {};
|
|
108
|
+
keys.forEach((k) => { out[k] = fxFor(FIXTURE[k]); });
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
const full = () => L.livingModel(F.inputOf(raw(), NOW));
|
|
112
|
+
const strip = (c) => { const o = { ...c }; delete o._sources; return o; };
|
|
113
|
+
|
|
114
|
+
// ── 1. the numbers ───────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
test('countsOf: on you, on others, to decide, agents at work and moving, from one model', () => {
|
|
117
|
+
const m = full();
|
|
118
|
+
const c = L.countsOf(m);
|
|
119
|
+
assert.deepEqual(strip(c), strip(m.city.counts), 'the model carries the same counts it would be asked for');
|
|
120
|
+
assert.ok(c.on_you > 0 && c.on_others > 0, 'the fixture has asks on you and on others');
|
|
121
|
+
assert.equal(c.open, c.on_you + c.on_others);
|
|
122
|
+
assert.equal(c.on_you, m.city.asks.filter(L.isOnYou).length);
|
|
123
|
+
assert.equal(c.on_others, m.city.asks.filter(L.isOnOthers).length);
|
|
124
|
+
assert.equal(c.agents_at_work, fx('tasks__claimable').active_claims.length, 'one agent per active claim');
|
|
125
|
+
assert.equal(c.moving, fx('moving').moving, 'moving is GET /moving, the owner\'s definition');
|
|
126
|
+
assert.ok(c.moving >= c.agents_at_work, 'moving (active + confirmed tasks) can never read under agents at work');
|
|
127
|
+
assert.deepEqual(Object.keys(c._sources).sort(), ['agents_at_work', 'moving', 'on_others', 'on_you', 'open']);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('a figure that could not be read is null, never zero', () => {
|
|
131
|
+
const none = L.countsOf(L.livingModel({ now: NOW }));
|
|
132
|
+
assert.equal(none.on_you, null, 'no docket: nothing honest to say about what is on you');
|
|
133
|
+
assert.equal(none.agents_at_work, null, 'no claims read: unknown, not "no agent"');
|
|
134
|
+
assert.equal(none.moving, null);
|
|
135
|
+
const quiet = L.countsOf(L.livingModel({ docket: { matters: [] }, claims: { active_claims: [] }, moving: { moving: 0 }, now: NOW }));
|
|
136
|
+
assert.deepEqual([quiet.on_you, quiet.agents_at_work, quiet.moving], [0, 0, 0], 'read and empty is a real zero');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('with no Lead read a docket matter waits where the docket says: on you', () => {
|
|
140
|
+
const r = raw();
|
|
141
|
+
delete r.leads;
|
|
142
|
+
const m = L.livingModel(F.inputOf(r, NOW));
|
|
143
|
+
const docketNeeds = r.docket.matters.filter((x) => x.state === 'needs' && x.route && x.route.yours).length;
|
|
144
|
+
assert.equal(L.countsOf(m).on_you, docketNeeds, 'a real site with no Leads prints the docket\'s own count');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── 2. the shell reads less, and counts the same ────────────────────────────
|
|
148
|
+
|
|
149
|
+
test('on a real site COUNT_FEEDS alone (one server read) give the same counts as every feed', () => {
|
|
150
|
+
// the counts are GET /government/counts, and nothing else
|
|
151
|
+
const m = L.livingModel(F.inputOf(raw(), NOW));
|
|
152
|
+
const small = L.livingModel(F.inputOf(raw(F.COUNT_FEEDS), NOW));
|
|
153
|
+
assert.deepEqual(strip(L.countsOf(small)), strip(L.countsOf(m)));
|
|
154
|
+
assert.deepEqual(L.countsOf(small)._sources, { on_you: 'real', on_others: 'real', open: 'real', agents_at_work: 'real', moving: 'real' });
|
|
155
|
+
const ids = m.city.asks.filter(L.isOnYou).slice(0, 2).map((a) => a.id).concat(m.city.asks.filter(L.isOnOthers).slice(0, 1).map((a) => a.id));
|
|
156
|
+
assert.deepEqual(strip(L.countsOf(small, { answered: ids })), strip(L.countsOf(m, { answered: ids })));
|
|
157
|
+
assert.equal(L.countsOf(small, { answered: ids }).on_you, L.countsOf(small).on_you - 2);
|
|
158
|
+
// the tally reads the server's count, not the lists it used to count over
|
|
159
|
+
for (const k of ['counts']) assert.ok(F.COUNT_FEEDS.includes(k), k);
|
|
160
|
+
for (const k of ['docket', 'blockers', 'leads', 'claims', 'moving']) assert.ok(!F.COUNT_FEEDS.includes(k), `the tally no longer reads ${k}`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('in the harness COUNT_FEEDS, with the docket the page already read, give the same counts as every feed, with and without answers', () => {
|
|
164
|
+
// the city and the street (the tally's only pages) always read the docket
|
|
165
|
+
const m = full();
|
|
166
|
+
const small = L.livingModel(F.inputOf(raw(F.COUNT_FEEDS.concat(['docket'])), NOW));
|
|
167
|
+
assert.deepEqual(strip(L.countsOf(small)), strip(L.countsOf(m)));
|
|
168
|
+
const ids = m.city.asks.filter(L.isOnYou).slice(0, 3).map((a) => a.id).concat(m.city.asks.filter(L.isOnOthers).slice(0, 1).map((a) => a.id));
|
|
169
|
+
const s = { answered: ids };
|
|
170
|
+
assert.deepEqual(strip(L.countsOf(small, s)), strip(L.countsOf(m, s)));
|
|
171
|
+
assert.equal(L.countsOf(m, s).on_you, L.countsOf(m).on_you - 3);
|
|
172
|
+
assert.equal(L.countsOf(m, s).on_others, L.countsOf(m).on_others - 1);
|
|
173
|
+
// and nothing the shell reads is a task list
|
|
174
|
+
assert.ok(!F.COUNT_FEEDS.includes('tasks') && !F.COUNT_FEEDS.includes('goals'), 'the tally reads no task or goal list');
|
|
175
|
+
assert.match(F.FEEDS.claims, /limit=1\b/, 'the claims ride the claimable feed at one row');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── 3. every surface prints the same numbers ────────────────────────────────
|
|
179
|
+
|
|
180
|
+
function cityOf(answered = []) {
|
|
181
|
+
const r = raw();
|
|
182
|
+
const m = full();
|
|
183
|
+
return City.cityModel({
|
|
184
|
+
docket: r.docket, goals: r.goals.goals, rollups: r.rollups.rollups, mode: 'dark', now: NOW,
|
|
185
|
+
living: m, constitution: r.constitution, answered, counts: L.countsOf(m, { answered }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function streetOf(answered = []) {
|
|
189
|
+
const r = raw();
|
|
190
|
+
const m = full();
|
|
191
|
+
return S.streetModel({
|
|
192
|
+
docket: r.docket, goals: r.goals.goals, rollups: r.rollups.rollups, mode: 'dark', now: NOW, here: null,
|
|
193
|
+
depth: true, living: m, roster: r.roster, counts: L.countsOf(m, { answered }),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function deckOf(done = {}) {
|
|
197
|
+
const r = raw();
|
|
198
|
+
const m = full();
|
|
199
|
+
return K.deckModel({
|
|
200
|
+
living: m, docket: r.docket, goals: r.goals.goals.filter((g) => g.status === 'open'), now: NOW, viewerId: String(r.docket.viewer.id),
|
|
201
|
+
scope: null, session: { done, delegated: {}, skipped: [], extra: [], batch: null, noBatch: {} },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
test('the city prints them: the plaque, the door and the line', () => {
|
|
206
|
+
const c = L.countsOf(full());
|
|
207
|
+
const cm = cityOf();
|
|
208
|
+
const you = cm.plaque.find((t) => t.key === 'you');
|
|
209
|
+
assert.equal(you.figure, String(c.on_you));
|
|
210
|
+
assert.equal(you.words, L.countWords('on_you', c.on_you));
|
|
211
|
+
assert.equal(cm.cta.label, 'decide ' + c.on_you + ' in a row');
|
|
212
|
+
assert.equal(cm.line, `${c.on_you} to do · ${c.on_others} ${L.countWords('on_others', c.on_others)} · ${c.agents_at_work} agents working`);
|
|
213
|
+
assert.deepEqual([cm.counts.on_you, cm.counts.on_others, cm.counts.agents_at_work, cm.counts.moving], [c.on_you, c.on_others, c.agents_at_work, c.moving]);
|
|
214
|
+
// the buildings' own windows and cards add up to the plaque
|
|
215
|
+
assert.equal(cm.civic.reduce((n, b) => n + b.on_you, 0), c.on_you, 'the nine buildings hold every ask on you');
|
|
216
|
+
assert.equal(cm.civic.reduce((n, b) => n + b.on_others, 0), c.on_others);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('the street prints them: the header, the deck door and the flow reading', () => {
|
|
220
|
+
const c = L.countsOf(full());
|
|
221
|
+
const sm = streetOf();
|
|
222
|
+
assert.equal(sm.hud.needs, String(c.on_you), 'on you');
|
|
223
|
+
assert.equal(sm.hud.moving, String(c.moving), 'moving, never the docket\'s own moving-state count');
|
|
224
|
+
assert.equal(sm.hud.hot, c.on_you > 0);
|
|
225
|
+
assert.match(sm.hud.deck.aria, new RegExp('^' + c.on_you + ' to do\\.'));
|
|
226
|
+
assert.equal(sm.depth.tiles.flow.n, String(c.agents_at_work), 'the flow reading is agents at work');
|
|
227
|
+
assert.equal(sm.depth.tiles.flow.words, L.countWords('agents_at_work', c.agents_at_work));
|
|
228
|
+
// while the living model is still reading, the header says so rather than flash the docket's number
|
|
229
|
+
const pending = S.streetModel({ docket: fx('government__docket'), goals: [], rollups: [], mode: 'dark', now: NOW, depth: true, living: null, counts: null });
|
|
230
|
+
assert.deepEqual([pending.hud.needs, pending.hud.moving], ['—', '—']);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('the deck prints them: the headline, the line under it and the scope chips', () => {
|
|
234
|
+
const c = L.countsOf(full());
|
|
235
|
+
const dm = deckOf();
|
|
236
|
+
assert.equal(dm.lead.n, c.on_you);
|
|
237
|
+
assert.equal(dm.lead.words, L.countWords('on_you', c.on_you));
|
|
238
|
+
assert.equal(dm.title, `${c.on_you} to do`);
|
|
239
|
+
assert.equal(dm.sub, `${c.on_others} ${L.countWords('on_others', c.on_others)} after these`);
|
|
240
|
+
assert.equal(dm.remaining, c.open, 'the deck holds every ask that needs a person');
|
|
241
|
+
assert.equal(dm.scopes.reduce((n, s) => n + s.n, 0), c.open, 'the "all" chip is the sum of the buildings');
|
|
242
|
+
assert.deepEqual(strip(dm.counts), strip(c));
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('the shell\'s tally prints them, through the real craft-mode.js and gov-feeds.js', async () => {
|
|
246
|
+
const c = L.countsOf(full());
|
|
247
|
+
const t = await shellTally();
|
|
248
|
+
assert.equal(t.text, `to do ${c.on_you} · moving ${c.moving}`);
|
|
249
|
+
for (const k of F.COUNT_FEEDS) assert.ok(t.asked.some((u) => u === F.API + F.FEEDS[k]), `the tally reads ${k}`);
|
|
250
|
+
assert.equal(t.asked.length, F.COUNT_FEEDS.length + 1, 'one read per feed: the COUNT_FEEDS, and the docket the page read');
|
|
251
|
+
assert.ok(!t.asked.some((u) => /\/goals|status=active/.test(u)), 'never a task or goal list');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── 4. an answer moves every surface by the same one ─────────────────────────
|
|
255
|
+
|
|
256
|
+
test('answering one ask on you moves the city, the street, the deck and the tally by one', async () => {
|
|
257
|
+
const m = full();
|
|
258
|
+
const c = L.countsOf(m);
|
|
259
|
+
const first = m.city.asks.find(L.isOnYou).id;
|
|
260
|
+
const cm = cityOf([first]);
|
|
261
|
+
const sm = streetOf([first]);
|
|
262
|
+
const dm = deckOf({ [first]: true });
|
|
263
|
+
const t = await shellTally([first]);
|
|
264
|
+
assert.equal(cm.counts.on_you, c.on_you - 1);
|
|
265
|
+
assert.equal(cm.cta.label, 'decide ' + (c.on_you - 1) + ' in a row');
|
|
266
|
+
assert.equal(sm.hud.needs, String(c.on_you - 1));
|
|
267
|
+
assert.equal(dm.lead.n, c.on_you - 1);
|
|
268
|
+
assert.equal(t.text, `to do ${c.on_you - 1} · moving ${c.moving}`);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ── 5. the labels ────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
test('every surface spells the counts in COUNT_WORDS, and the retired labels are gone', () => {
|
|
274
|
+
assert.deepEqual(Object.keys(L.COUNT_WORDS), ['on_you', 'on_others', 'open', 'agents_at_work', 'moving']);
|
|
275
|
+
const street = read('street.html');
|
|
276
|
+
assert.match(street, /<span class="street-label">to do<\/span>/, 'the header names the count as the tally does');
|
|
277
|
+
assert.equal(L.countLabel('on_you'), 'to do', 'the main count is "to do" (owner, 2026-10-09)');
|
|
278
|
+
assert.deepEqual([L.countWords('on_you', 1), L.countWords('on_you', 24)], ['to do', 'to do']);
|
|
279
|
+
assert.equal(cityOf().plaque.find((t) => t.key === 'you').words, 'to do');
|
|
280
|
+
const code = ['city-draw.js', 'city.js', 'street-draw.js', 'street-deck.js', 'street-office.js', 'craft-mode.js']
|
|
281
|
+
.concat(['street-depth.js'])
|
|
282
|
+
.map((f) => read(f).split('\n').filter((l) => !/^\s*(\/\/|\*)/.test(l)).join('\n')).join('\n');
|
|
283
|
+
for (const gone of ["' at work'", "' with others'", "' on your seats'", "' wait on you'", "words: 'on you'"]) {
|
|
284
|
+
assert.ok(!code.includes(gone), `${gone} is no longer printed anywhere`);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ── the shell, in a vm: craft-mode.js + gov-living.js + gov-feeds.js ─────────
|
|
289
|
+
|
|
290
|
+
function node(tag, id, cls) {
|
|
291
|
+
const attrs = {};
|
|
292
|
+
return {
|
|
293
|
+
tagName: tag, id: id || '', className: cls || '', dataset: {}, children: [], parentNode: null, textContent: '', innerHTML: '',
|
|
294
|
+
setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; }, removeAttribute(k) { delete attrs[k]; },
|
|
295
|
+
get nextSibling() { const sib = this.parentNode && this.parentNode.children; return sib ? sib[sib.indexOf(this) + 1] || null : null; },
|
|
296
|
+
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
|
|
297
|
+
insertBefore(c, ref) { c.parentNode = this; const at = ref ? this.children.indexOf(ref) : -1; if (at === -1) this.children.push(c); else this.children.splice(at, 0, c); return c; },
|
|
298
|
+
removeChild(c) { this.children.splice(this.children.indexOf(c), 1); c.parentNode = null; return c; },
|
|
299
|
+
querySelector(sel) { return all(this).find((n) => sel === '.' + n.className) || null; },
|
|
300
|
+
querySelectorAll() { return []; }, addEventListener() {}, classList: { toggle() {} },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function all(root) { return root.children.flatMap((c) => [c, ...all(c)]); }
|
|
304
|
+
async function shellTally(answered = []) {
|
|
305
|
+
const sidebar = node('NAV', 'app-sidebar');
|
|
306
|
+
sidebar.appendChild(node('DIV', '', 'app-sidebar__brand'));
|
|
307
|
+
const topbar = node('DIV', 'app-topbar');
|
|
308
|
+
topbar.appendChild(node('H2', 'topbar-title'));
|
|
309
|
+
const document = {
|
|
310
|
+
documentElement: node('HTML'), body: (() => { const b = node('BODY'); b.setAttribute('data-page', 'street'); return b; })(), readyState: 'complete',
|
|
311
|
+
getElementById: (id) => [sidebar, topbar, ...all(sidebar), ...all(topbar)].find((n) => n.id === id) || null,
|
|
312
|
+
querySelectorAll: () => [], createElement: (t) => node(t.toUpperCase()), addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
|
|
313
|
+
};
|
|
314
|
+
const asked = [];
|
|
315
|
+
const byUrl = {};
|
|
316
|
+
Object.keys(F.FEEDS).forEach((k) => { byUrl[F.API + F.FEEDS[k]] = FIXTURE[k]; });
|
|
317
|
+
const fetch = (url) => {
|
|
318
|
+
asked.push(url);
|
|
319
|
+
const name = byUrl[url];
|
|
320
|
+
return Promise.resolve(name ? { ok: true, status: 200, json: () => Promise.resolve(fxFor(name)) } : { ok: false, status: 404, json: () => Promise.resolve({}) });
|
|
321
|
+
};
|
|
322
|
+
const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
|
|
323
|
+
const window = { localStorage: mk({}), sessionStorage: mk({ 'hall-craft': 'governing' }), matchMedia: () => ({ matches: false }), fetch };
|
|
324
|
+
const sandbox = { window, document, fetch, Date, Promise, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
|
|
325
|
+
vm.createContext(sandbox);
|
|
326
|
+
vm.runInContext(read('gov-living.js'), sandbox, { filename: 'gov-living.js' });
|
|
327
|
+
vm.runInContext(read('gov-feeds.js'), sandbox, { filename: 'gov-feeds.js' });
|
|
328
|
+
window.GovFeeds.useSession(() => ({ answered }));
|
|
329
|
+
// the street page has read its docket before the shell asks (street.js load)
|
|
330
|
+
await window.GovFeeds.read(['docket']);
|
|
331
|
+
vm.runInContext(read('craft-mode.js'), sandbox, { filename: 'craft-mode.js' });
|
|
332
|
+
window.OTBCraftMode.mount();
|
|
333
|
+
for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r));
|
|
334
|
+
const box = all(topbar).find((n) => n.id === 'craft-tallies');
|
|
335
|
+
return { text: box ? box.innerHTML.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim() : '', asked };
|
|
336
|
+
}
|