@bongos/core 1.21.113 → 1.21.115
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 +608 -188
- 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 +709 -626
- package/docs/copy-registry.json +1673 -858
- 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 +4875 -4748
- 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-hall-live.states.json +0 -5
- package/modules/public-landing/public/projects.html +169 -6
- package/modules/public-landing/public/projects.probes.json +10 -6
- package/modules/public-landing/public/projects.states.json +13 -13
- 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-verify.js +21 -2
- 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/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/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/projects_hub_pre_uat.mjs +4 -2
- 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
- package/tests/wizard_physics.mjs +1 -1
- package/tests/wizard_physics_more.mjs +133 -0
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
// modules/hall-ui/public/gov-living.js — THE LIVING CITY's model: the government
|
|
2
|
+
// that steers a project's AI, as data (task 1004685, the living-city prototype).
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS. city-draw.js draws the city from the docket and the goals; this
|
|
5
|
+
// file is the layer under the prototype's next drawings, which make the city
|
|
6
|
+
// "live and breathe". It reads every real governor feed (the harness snapshots
|
|
7
|
+
// them in scripts/hall-preview/fixtures) and returns one model the visual phases
|
|
8
|
+
// draw from. ALL OF IT IS REAL (task 1004717): the prototype overlay that once
|
|
9
|
+
// stood in for Leads, laws, pots, presets, build-outs and the human rewrite of
|
|
10
|
+
// each ask is deleted, so a real project's city is complete on its own reads. A
|
|
11
|
+
// concept no table holds (a Lead's deputies, a committee's members) is not drawn,
|
|
12
|
+
// and a matter with no stored ask keeps its own words.
|
|
13
|
+
//
|
|
14
|
+
// livingModel({ docket, constitution, goals, rollups, blockers, fence, claims,
|
|
15
|
+
// tasks, moving, sessions, learnings, cost, roster, me, laws, leads, presets,
|
|
16
|
+
// treasury, counts, now })
|
|
17
|
+
// → { viewer, owner, strictness, buildings[9], districts[], city, myAgents, _sources }
|
|
18
|
+
//
|
|
19
|
+
// countsOf(model, session) → { on_you, on_others, open, agents_at_work, moving }
|
|
20
|
+
// over the server's own counts (GET /government/counts, task 1004716) when read
|
|
21
|
+
// the ONE definition of every number the city, the street, the deck and the
|
|
22
|
+
// shell's Governing tally print (gov-feeds.js hands it to each of them);
|
|
23
|
+
// tests/hall_gov_counts.mjs renders each surface and holds them equal.
|
|
24
|
+
//
|
|
25
|
+
// Every input takes the route's own body or the array inside it, and every one
|
|
26
|
+
// may be missing: a missing feed leaves its part of the model empty, never
|
|
27
|
+
// invented. `now` is ms since the epoch, passed in (ages, today's spend).
|
|
28
|
+
//
|
|
29
|
+
// WHERE EACH FIELD COMES FROM. Each object carries `_sources`, mapping a field
|
|
30
|
+
// (or a dotted sub-field, `counts.building`) to:
|
|
31
|
+
// 'real' copied from a live read (an unread feed leaves it empty, never invented);
|
|
32
|
+
// 'derived' computed here from live reads only, by a rule written in this file;
|
|
33
|
+
// 'mixed' holds objects that carry their own `_sources` (read those).
|
|
34
|
+
// There is no other label. tests/hall_gov_living.mjs walks the whole model to
|
|
35
|
+
// hold it to that, and proves an `overlay` passed in changes nothing.
|
|
36
|
+
//
|
|
37
|
+
// ACTIVITY (0..1). An agent at work is an ACTIVE CLAIM (GET /tasks/claimable
|
|
38
|
+
// active_claims). A claim belongs to the building its work answers to:
|
|
39
|
+
// 1. its task's goal (GET /tasks?status=active gives task → goal), through
|
|
40
|
+
// goalBuilding(): the maintenance goal → Release; "Working area N" → the
|
|
41
|
+
// AREA_BUILDING table; an Autobongos goal → Automation; else the goal's
|
|
42
|
+
// category (CATEGORY_BUILDING);
|
|
43
|
+
// 2. failing that, the modules its claim touches (MODULE_BUILDING), by majority;
|
|
44
|
+
// 3. failing both, it is traffic with no building (`city.traffic.unplaced`).
|
|
45
|
+
// A building's activity is claims there / ACTIVITY_FULL (3 agents is a busy
|
|
46
|
+
// building), capped at 1. A district's is the larger of its claims and its
|
|
47
|
+
// rollup's `active` count over ACTIVITY_FULL, and at least RECENT_FLOOR when its
|
|
48
|
+
// rollup moved within the last day. `lit` is how lit the windows are: a base glow,
|
|
49
|
+
// plus activity, plus a lamp when something there needs a person.
|
|
50
|
+
//
|
|
51
|
+
// THE ASK FORMAT. askFormat(card) is the enforced shape of a question to a human
|
|
52
|
+
// (one question of at most 12 words, 2-3 tap options each with a one-line result,
|
|
53
|
+
// a recommendation and why, who it waits on, what happens if nobody answers);
|
|
54
|
+
// slopScore(text) measures the run-on text it replaces. A card that fails the
|
|
55
|
+
// format is REFUSED: the model keeps it, marks `format.ok: false`, and sets
|
|
56
|
+
// `shown: null` so a board shows the refusal, never the slop.
|
|
57
|
+
// THE SERVER HOLDS THE SAME RULES (task 1004703): modules/government/asks.js
|
|
58
|
+
// checkAsk refuses a slop ask before it is stored, and a matter that carries a
|
|
59
|
+
// stored ask hands it here as `ask`; that card is real (`ask_real`). A matter
|
|
60
|
+
// with no stored ask has no card: it shows in its own words (`shown: null`).
|
|
61
|
+
// tests/government_asks.mjs holds both copies to one verdict.
|
|
62
|
+
//
|
|
63
|
+
// THE TREASURY (task 1004712): GET /government/treasury says which goals have a
|
|
64
|
+
// pot (a budget the Treasury's holder gave it), what each spent this month
|
|
65
|
+
// (cost_log through the goal's tasks), what the reserve lent it, the reserve, and
|
|
66
|
+
// the spend cap (off by default: no limit). A dry pot's borrow is a real docket
|
|
67
|
+
// matter (treasury_spending:<id>) whose stored ask waits on the Treasury.
|
|
68
|
+
//
|
|
69
|
+
// THE LEADS (task 1004709): GET /government/leads says who leads each building
|
|
70
|
+
// (appointed by a passed People matter) and who holds the owner seat an unseated
|
|
71
|
+
// building falls to; every building's Lead, holder and charter come from it.
|
|
72
|
+
// The same read carries `build_rights` (task 1004710): what each building's
|
|
73
|
+
// holder may build out without a vote, decided on the server from the form of
|
|
74
|
+
// government; this file holds no rule of its own.
|
|
75
|
+
// A LEAD'S BUILD-OUTS (task 1004711): the same read carries each building's
|
|
76
|
+
// `structure` — committee rooms, floors, annexes and districts on the record
|
|
77
|
+
// (going up for a day, then built) and those waiting on a vote. A building's
|
|
78
|
+
// construction is drawn from it (a live site at its real progress, a ghost whose
|
|
79
|
+
// tally is its sitting's on the docket) and its committee rooms are its real ones.
|
|
80
|
+
// WHO AN ASK WAITS ON IS REAL (task 1004705, the direction desk): the server
|
|
81
|
+
// routes every docket matter to a seat (modules/government/routing.js — its
|
|
82
|
+
// stored ask's seat, else its building's Lead or its board; an unseated building
|
|
83
|
+
// falls to the owner) and the docket carries the answer as `route` with
|
|
84
|
+
// `routed: true`. Then `needs_you` is the route's `yours`, `waits_on` is the
|
|
85
|
+
// route's seat, and an ask nobody can answer (past its deadline, or on an empty
|
|
86
|
+
// seat) is settled with its `nobody` line: it does what its "if nobody answers"
|
|
87
|
+
// says and is on nobody's "to do". A docket that is not routed falls back to
|
|
88
|
+
// the seat the real Leads resolve, else the docket's own per-viewer word.
|
|
89
|
+
//
|
|
90
|
+
// PURE. No DOM, no fetch, no clock (the test reads this file to hold it to that).
|
|
91
|
+
|
|
92
|
+
(function (root) {
|
|
93
|
+
'use strict';
|
|
94
|
+
|
|
95
|
+
const R = 'real';
|
|
96
|
+
const D = 'derived';
|
|
97
|
+
// a field holding objects that carry their own _sources (read those instead)
|
|
98
|
+
const M = 'mixed';
|
|
99
|
+
|
|
100
|
+
// ---------- the nine buildings (the docket's vocabulary order) ----------
|
|
101
|
+
const BUILDING_KEYS = Object.freeze(['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'review', 'release', 'treasury']);
|
|
102
|
+
const BUILDING_NAMES = Object.freeze({
|
|
103
|
+
charter: 'Charter', rules: 'Rules', disputes: 'Disputes', people: 'People', planning: 'Planning',
|
|
104
|
+
automation: 'Automation', review: 'Review', release: 'Release', treasury: 'Treasury',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ---------- where work answers to (the activity mapping) ----------
|
|
108
|
+
// A working area (goals.working_area, or "Working area N" in its title) → the
|
|
109
|
+
// building whose Lead it reports to.
|
|
110
|
+
const AREA_BUILDING = Object.freeze({
|
|
111
|
+
1: 'people', // project creation: founders and newcomers arrive
|
|
112
|
+
2: 'people', // accounts, management & community
|
|
113
|
+
3: 'planning', // human project management
|
|
114
|
+
4: 'release', // core distribution
|
|
115
|
+
5: 'release', // module distribution
|
|
116
|
+
6: 'review', // the role experiences, checked by people
|
|
117
|
+
7: 'charter', // government itself
|
|
118
|
+
8: 'treasury', // platform credit economy
|
|
119
|
+
9: 'planning', // analytics: the flow and promises readings
|
|
120
|
+
10: 'disputes', // security reports
|
|
121
|
+
11: 'rules',
|
|
122
|
+
12: 'treasury', // cost optimisation
|
|
123
|
+
});
|
|
124
|
+
const CATEGORY_BUILDING = Object.freeze({
|
|
125
|
+
government: 'charter', economics: 'treasury', security: 'disputes', modules: 'release',
|
|
126
|
+
'builders-hall': 'planning', 'user-experience': 'review',
|
|
127
|
+
});
|
|
128
|
+
const MODULE_BUILDING = Object.freeze({
|
|
129
|
+
government: 'charter', ideas: 'planning', lifecycle: 'planning', grading: 'review', 'copy-desk': 'review',
|
|
130
|
+
'ui-design': 'review', 'hall-ui': 'review', 'npm-release': 'release', provisioning: 'release',
|
|
131
|
+
economy: 'treasury', autonomy: 'automation', 'platform-identity': 'people', onboarding: 'people',
|
|
132
|
+
'builder-settings': 'people', security: 'disputes',
|
|
133
|
+
});
|
|
134
|
+
const ACTIVITY_FULL = 3;
|
|
135
|
+
const RECENT_FLOOR = 0.2;
|
|
136
|
+
const DAY_MS = 24 * 3600 * 1000;
|
|
137
|
+
const URGENT_MS = DAY_MS;
|
|
138
|
+
|
|
139
|
+
// ---------- small helpers ----------
|
|
140
|
+
const arr = (v, key) => (Array.isArray(v) ? v : v && Array.isArray(v[key]) ? v[key] : []);
|
|
141
|
+
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : null);
|
|
142
|
+
const str = (v) => (v == null ? '' : String(v));
|
|
143
|
+
const clamp01 = (n) => Math.max(0, Math.min(1, Number(n) || 0));
|
|
144
|
+
const round2 = (n) => Math.round((Number(n) || 0) * 100) / 100;
|
|
145
|
+
const round3 = (n) => Math.round((Number(n) || 0) * 1000) / 1000;
|
|
146
|
+
const time = (iso) => { const t = iso ? Date.parse(iso) : NaN; return isFinite(t) ? t : null; };
|
|
147
|
+
|
|
148
|
+
// ---------- the ask format ----------
|
|
149
|
+
const ASK_RULES = Object.freeze({ question_words: 12, options_min: 2, options_max: 3, label_words: 4, line_words: 14, why_words: 20 });
|
|
150
|
+
const MARKUP = /\*\*|`|^#|\[[^\]]*\]\(|https?:\/\/|<\/?[a-z]/im;
|
|
151
|
+
|
|
152
|
+
function words(text) { return str(text).trim().split(/\s+/).filter(Boolean); }
|
|
153
|
+
// Sentences end at . ! ? followed by a space and a capital (or a digit, a quote,
|
|
154
|
+
// a bracket), or by the end; a version (1.20.82) or a decimal does not end one.
|
|
155
|
+
function sentences(text) {
|
|
156
|
+
const t = str(text).replace(/\s+/g, ' ').trim();
|
|
157
|
+
const out = [];
|
|
158
|
+
let start = 0;
|
|
159
|
+
for (let i = 0; i < t.length; i++) {
|
|
160
|
+
const ch = t[i];
|
|
161
|
+
if (ch !== '.' && ch !== '!' && ch !== '?') continue;
|
|
162
|
+
const next = t[i + 1];
|
|
163
|
+
if (next !== undefined && next !== ' ') continue;
|
|
164
|
+
const after = t[i + 2];
|
|
165
|
+
if (next === ' ' && after !== undefined && !/[A-Z0-9"'(*[]/.test(after)) continue;
|
|
166
|
+
out.push(t.slice(start, i + 1).trim());
|
|
167
|
+
start = i + 2;
|
|
168
|
+
}
|
|
169
|
+
if (start < t.length) out.push(t.slice(start).trim());
|
|
170
|
+
return out.filter((s) => /[A-Za-z0-9]/.test(s));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// askFormat(card) → { ok, errors: [{ field, rule, message }], question_words }
|
|
174
|
+
function askFormat(card) {
|
|
175
|
+
const errors = [];
|
|
176
|
+
const err = (field, rule, message) => errors.push({ field, rule, message });
|
|
177
|
+
const c = obj(card);
|
|
178
|
+
if (!c) {
|
|
179
|
+
err('card', 'missing', 'No human card: the ask is still in its raw form.');
|
|
180
|
+
return { ok: false, errors, question_words: 0 };
|
|
181
|
+
}
|
|
182
|
+
const q = str(c.question).trim();
|
|
183
|
+
const qWords = words(q).length;
|
|
184
|
+
if (!q) err('question', 'missing', 'Ask one question.');
|
|
185
|
+
else {
|
|
186
|
+
if (qWords > ASK_RULES.question_words) err('question', 'too_long', `The question is ${qWords} words; ${ASK_RULES.question_words} at most.`);
|
|
187
|
+
if (!/\?$/.test(q)) err('question', 'not_a_question', 'End the question with a question mark.');
|
|
188
|
+
if (sentences(q).length > 1 || /\n/.test(q)) err('question', 'more_than_one', 'Ask exactly one question.');
|
|
189
|
+
if (MARKUP.test(q)) err('question', 'markup', 'No markup or links in the question.');
|
|
190
|
+
}
|
|
191
|
+
const opts = Array.isArray(c.options) ? c.options : [];
|
|
192
|
+
if (opts.length < ASK_RULES.options_min || opts.length > ASK_RULES.options_max) {
|
|
193
|
+
err('options', 'count', `Offer ${ASK_RULES.options_min} or ${ASK_RULES.options_max} tap answers, not ${opts.length}.`);
|
|
194
|
+
}
|
|
195
|
+
const keys = new Set();
|
|
196
|
+
opts.forEach((o, i) => {
|
|
197
|
+
const f = `options[${i}]`;
|
|
198
|
+
if (!obj(o)) { err(f, 'missing', 'An answer needs a key, a label and a result.'); return; }
|
|
199
|
+
const k = str(o.key).trim();
|
|
200
|
+
if (!k) err(f, 'no_key', 'An answer needs a key.');
|
|
201
|
+
else if (keys.has(k)) err(f, 'duplicate', `Two answers share the key "${k}".`);
|
|
202
|
+
keys.add(k);
|
|
203
|
+
const lw = words(o.label).length;
|
|
204
|
+
if (!lw) err(f, 'no_label', 'An answer needs a label to tap.');
|
|
205
|
+
else if (lw > ASK_RULES.label_words) err(f, 'label_too_long', `"${str(o.label)}" is ${lw} words; a tap label is ${ASK_RULES.label_words} at most.`);
|
|
206
|
+
lineRule(f + '.result', o.result, 'Say in one line what this answer does.');
|
|
207
|
+
});
|
|
208
|
+
const rec = obj(c.recommend);
|
|
209
|
+
if (!rec) err('recommend', 'missing', 'Recommend an answer and say why.');
|
|
210
|
+
else {
|
|
211
|
+
if (!keys.has(str(rec.key))) err('recommend', 'unknown_key', 'Recommend one of the answers offered.');
|
|
212
|
+
const ww = words(rec.why).length;
|
|
213
|
+
if (!ww) err('recommend', 'no_why', 'Say why in one line.');
|
|
214
|
+
else if (ww > ASK_RULES.why_words || /\n/.test(str(rec.why))) err('recommend', 'why_too_long', `The why is ${ww} words; ${ASK_RULES.why_words} at most, one line.`);
|
|
215
|
+
}
|
|
216
|
+
if (!str(c.waits_on).trim()) err('waits_on', 'missing', 'Say who this waits on.');
|
|
217
|
+
lineRule('if_nobody_answers', c.if_nobody_answers, 'Say what happens if nobody answers.');
|
|
218
|
+
return { ok: errors.length === 0, errors, question_words: qWords };
|
|
219
|
+
|
|
220
|
+
function lineRule(field, text, missingMessage) {
|
|
221
|
+
const t = str(text).trim();
|
|
222
|
+
const n = words(t).length;
|
|
223
|
+
if (!n) err(field, 'missing', missingMessage);
|
|
224
|
+
else if (/\n/.test(t) || n > ASK_RULES.line_words) err(field, 'not_one_line', `${n} words; one line of ${ASK_RULES.line_words} at most.`);
|
|
225
|
+
else if (MARKUP.test(t)) err(field, 'markup', 'No markup or links.');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// slopScore(text) → how far a text is from something a person can answer at a
|
|
230
|
+
// glance. `slop` is true past any one threshold; `score` (0..1) ranks them.
|
|
231
|
+
const SLOP = Object.freeze({ words: 30, sentences: 2, longest: 20 });
|
|
232
|
+
function slopScore(text) {
|
|
233
|
+
const t = str(text);
|
|
234
|
+
const w = words(t).length;
|
|
235
|
+
const ss = sentences(t);
|
|
236
|
+
const longest = ss.reduce((m, s) => Math.max(m, words(s).length), 0);
|
|
237
|
+
const lines = t.trim() ? t.trim().split(/\n+/).length : 0;
|
|
238
|
+
const markup = (t.match(/\*\*|`+|^#+\s|\[[^\]]*\]\([^)]*\)|https?:\/\/\S+|^\s*[-*]\s/gm) || []).length;
|
|
239
|
+
const why = [];
|
|
240
|
+
if (w > SLOP.words) why.push(`${w} words`);
|
|
241
|
+
if (ss.length > SLOP.sentences) why.push(`${ss.length} sentences`);
|
|
242
|
+
if (longest > SLOP.longest) why.push(`a ${longest}-word sentence`);
|
|
243
|
+
if (markup) why.push(`${markup} bits of markup`);
|
|
244
|
+
const score = round3(Math.max(Math.min(1, w / 120), Math.min(1, Math.max(0, ss.length - 1) / 6), Math.min(1, longest / 50), Math.min(1, markup / 6)));
|
|
245
|
+
return { words: w, sentences: ss.length, longest_sentence: longest, lines, markup, score, slop: why.length > 0, why };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------- people ----------
|
|
249
|
+
function peopleIndex(roster, docket) {
|
|
250
|
+
const byId = {};
|
|
251
|
+
arr(roster, 'builders').forEach((b) => {
|
|
252
|
+
if (b && b.id != null) byId[String(b.id)] = { id: String(b.id), login: str(b.github_login), name: str(b.display_name || b.github_login) };
|
|
253
|
+
});
|
|
254
|
+
arr(docket && docket.buildings).forEach((b) => arr(b && b.holders).forEach((h) => {
|
|
255
|
+
if (h && h.id != null && !byId[String(h.id)]) byId[String(h.id)] = { id: String(h.id), login: str(h.login), name: str(h.name || h.login) };
|
|
256
|
+
}));
|
|
257
|
+
return (id) => (id == null ? null : byId[String(id)] || { id: String(id), login: '', name: '' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------- where work answers to ----------
|
|
261
|
+
function goalBuilding(goal) {
|
|
262
|
+
const g = obj(goal);
|
|
263
|
+
if (!g) return null;
|
|
264
|
+
if (g.is_maintenance) return 'release';
|
|
265
|
+
const area = Number(g.working_area) || Number((/working area\s+(\d+)/i.exec(str(g.title)) || [])[1]) || null;
|
|
266
|
+
if (area && AREA_BUILDING[area]) return AREA_BUILDING[area];
|
|
267
|
+
if (/autobongos|unattended/i.test(str(g.title))) return 'automation';
|
|
268
|
+
const slug = g.category && g.category.slug;
|
|
269
|
+
return (slug && CATEGORY_BUILDING[slug]) || null;
|
|
270
|
+
}
|
|
271
|
+
function touchesBuilding(touches) {
|
|
272
|
+
const tally = {};
|
|
273
|
+
arr(touches).forEach((t) => {
|
|
274
|
+
const m = /^modules\/([^/]+)\//.exec(str(t));
|
|
275
|
+
const b = m && MODULE_BUILDING[m[1]];
|
|
276
|
+
if (b) tally[b] = (tally[b] || 0) + 1;
|
|
277
|
+
});
|
|
278
|
+
const best = Object.keys(tally).sort((a, b) => tally[b] - tally[a] || BUILDING_KEYS.indexOf(a) - BUILDING_KEYS.indexOf(b))[0];
|
|
279
|
+
return best || null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------- seats ----------
|
|
283
|
+
// A seat names who an ask waits on: 'owner', 'lead:<building>' (an unseated
|
|
284
|
+
// building falls to the owner), 'committee:<building>.<key>', 'board:<building>'.
|
|
285
|
+
function seatResolver(ctx) {
|
|
286
|
+
return function resolve(seat, building) {
|
|
287
|
+
const s = str(seat) || (building ? 'lead:' + building : 'owner');
|
|
288
|
+
let m;
|
|
289
|
+
// the owner seat's members: every one GET /government/leads names (none unread)
|
|
290
|
+
const ownerIds = ctx.ownerIds || [];
|
|
291
|
+
if (s === 'owner') return { seat: s, label: 'the owner', person: ctx.owner, members: ownerIds };
|
|
292
|
+
if ((m = /^lead:(\w+)$/.exec(s))) {
|
|
293
|
+
const lead = ctx.leads[m[1]];
|
|
294
|
+
if (lead && lead.person) return { seat: s, label: `the ${BUILDING_NAMES[m[1]] || m[1]} Lead`, person: lead.person, members: [lead.person.id] };
|
|
295
|
+
return { seat: s, label: 'the owner', person: ctx.owner, members: ownerIds, fell_to_owner: true };
|
|
296
|
+
}
|
|
297
|
+
if ((m = /^committee:(\w+)\.(\w+)$/.exec(s))) {
|
|
298
|
+
const lead = ctx.leads[m[1]];
|
|
299
|
+
const com = lead && lead.committees.find((c) => c.key === m[2]);
|
|
300
|
+
if (com) return { seat: s, label: `${com.name} (${BUILDING_NAMES[m[1]] || m[1]})`, person: lead.person, members: com.members.map((p) => p.id) };
|
|
301
|
+
return resolve('lead:' + m[1], m[1]);
|
|
302
|
+
}
|
|
303
|
+
if ((m = /^board:(\w+)$/.exec(s))) {
|
|
304
|
+
const holders = ctx.holders[m[1]] || [];
|
|
305
|
+
return { seat: s, label: 'the board', person: null, members: holders.map((p) => p.id) };
|
|
306
|
+
}
|
|
307
|
+
return { seat: s, label: s, person: null, members: [] };
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Who answers for a building, as GET /government/leads says: its Lead's seat, or
|
|
312
|
+
// the owner's when it is unseated (task 1004709). { seat, ids, fell_to_owner }
|
|
313
|
+
function answersOf(read, key) {
|
|
314
|
+
const b = arr(read.buildings).find((x) => x && x.building === key);
|
|
315
|
+
const a = b && obj(b.answers);
|
|
316
|
+
return a ? { seat: str(a.seat), ids: arr(a.builder_ids).map(str), fell_to_owner: !!a.fell_to_owner } : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// What a building's holder may build out without a vote (task 1004710). The
|
|
320
|
+
// SERVER decides it (modules/government/build-rights.js: the form of government
|
|
321
|
+
// and the building's own decision rule, never a Lead's charter) and GET
|
|
322
|
+
// /government/leads carries it per building; this only reads the answer, one
|
|
323
|
+
// capsule at a time. { form, form_name, holder, capsules: { key: { without_vote, why, by } } } or null.
|
|
324
|
+
const CAPSULE_KEYS = Object.freeze(['committee', 'floor', 'annex', 'district']);
|
|
325
|
+
function rightsShape(r) {
|
|
326
|
+
if (!obj(r) || !obj(r.capsules)) return null;
|
|
327
|
+
const capsules = {};
|
|
328
|
+
CAPSULE_KEYS.forEach((k) => {
|
|
329
|
+
const c = obj(r.capsules[k]) ? r.capsules[k] : null;
|
|
330
|
+
capsules[k] = { without_vote: !!(c && c.without_vote === true), why: str(c && c.why), ground: str(c && c.ground) };
|
|
331
|
+
});
|
|
332
|
+
return { form: str(r.form), form_name: str(r.form_name), holder: str(r.holder), capsules };
|
|
333
|
+
}
|
|
334
|
+
function rightsOf(read, key) {
|
|
335
|
+
const b = arr(read.buildings).find((x) => x && x.building === key);
|
|
336
|
+
return b ? rightsShape(b.build_rights) : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------- one ask card ----------
|
|
340
|
+
const STATE_ORDER = Object.freeze({ needs: 1, moving: 2, settled: 3 });
|
|
341
|
+
function makeAsk(base, human, ctx, sources) {
|
|
342
|
+
const opened = time(base.opened_at);
|
|
343
|
+
const deadline = time(base.deadline);
|
|
344
|
+
const format = askFormat(human);
|
|
345
|
+
// the server's route (task 1004705), when the docket was routed
|
|
346
|
+
const served = ctx.routed ? obj(base.route) : null;
|
|
347
|
+
const route = served
|
|
348
|
+
? { seat: str(served.seat), label: str(served.label), person: null, fell_to_owner: !!served.fell_to_owner, members: [] }
|
|
349
|
+
: ctx.resolveSeat(human && human.waits_on, base.building);
|
|
350
|
+
const nobody = served && obj(served.nobody) ? { why: str(served.nobody.why), outcome: served.nobody.outcome == null ? null : str(served.nobody.outcome) } : null;
|
|
351
|
+
// an ask nobody answers has done what its "if nobody answers" line says
|
|
352
|
+
const state = nobody ? 'settled' : STATE_ORDER[base.state] ? base.state : 'needs';
|
|
353
|
+
// Routed: the route's word. A matter the routed docket does not carry is
|
|
354
|
+
// nobody's here. Not routed: whether the viewer holds the seat the real Leads
|
|
355
|
+
// resolve (GET /government/leads read), else the docket's own per-viewer 'needs'.
|
|
356
|
+
const needsYou = state === 'needs' && !!ctx.viewerId && (served ? !!served.yours && ctx.routedFor === ctx.viewerId
|
|
357
|
+
: ctx.routed ? false
|
|
358
|
+
: (route.members.indexOf(ctx.viewerId) >= 0 || (!ctx.seated && !!base.from_docket)));
|
|
359
|
+
const rawText = str(base.raw_title) + (base.raw_detail ? '\n\n' + str(base.raw_detail) : '');
|
|
360
|
+
return {
|
|
361
|
+
id: str(base.id),
|
|
362
|
+
source: str(base.source),
|
|
363
|
+
building: base.building,
|
|
364
|
+
state,
|
|
365
|
+
urgent: state === 'needs' && deadline != null && ctx.now != null && deadline - ctx.now <= URGENT_MS,
|
|
366
|
+
opened_at: base.opened_at || null,
|
|
367
|
+
deadline: base.deadline || null,
|
|
368
|
+
age_days: opened != null && ctx.now != null ? Math.max(0, Math.floor((ctx.now - opened) / DAY_MS)) : null,
|
|
369
|
+
rule: base.rule || null,
|
|
370
|
+
ref: base.ref || null,
|
|
371
|
+
href: base.href || null,
|
|
372
|
+
acts: base.acts || [],
|
|
373
|
+
tally: base.tally || null,
|
|
374
|
+
raw: { title: str(base.raw_title), detail: str(base.raw_detail), slop: slopScore(rawText) },
|
|
375
|
+
human: human || null,
|
|
376
|
+
format,
|
|
377
|
+
shown: format.ok ? human : null,
|
|
378
|
+
waits_on: { seat: route.seat, label: route.label, person: route.person || null, fell_to_owner: !!route.fell_to_owner, real: !!served },
|
|
379
|
+
needs_you: needsYou,
|
|
380
|
+
nobody,
|
|
381
|
+
// the card's words are the agent's own stored ask (task 1004703)
|
|
382
|
+
ask_real: !!base.ask_real,
|
|
383
|
+
_sources: served ? Object.assign({}, sources, { state: D, waits_on: R, needs_you: D, nobody: R }) : Object.assign({}, sources, { nobody: D }),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Needs-you first (needs that wait on the viewer's seat), then the rest that
|
|
388
|
+
// need a person, then moving, then settled; inside a tier the oldest first (an
|
|
389
|
+
// ask with no date after the dated ones), then by id.
|
|
390
|
+
function askTier(a) { return a.needs_you ? 0 : STATE_ORDER[a.state]; }
|
|
391
|
+
function compareAsks(a, b) {
|
|
392
|
+
const t = askTier(a) - askTier(b);
|
|
393
|
+
if (t) return t;
|
|
394
|
+
const ta = time(a.opened_at);
|
|
395
|
+
const tb = time(b.opened_at);
|
|
396
|
+
if (ta != null && tb != null && ta !== tb) return ta - tb;
|
|
397
|
+
if ((ta == null) !== (tb == null)) return ta == null ? 1 : -1;
|
|
398
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------- the counts every surface prints (one source) ----------
|
|
402
|
+
// The city's plaque, door and line, the street's header and readings, the
|
|
403
|
+
// decision deck and the shell's Governing tally all print these numbers, so
|
|
404
|
+
// each is defined once, here, with one plain label:
|
|
405
|
+
// on_you asks that need a person and wait on one of the viewer's seats
|
|
406
|
+
// on_others asks that need a person and wait on someone else's seat
|
|
407
|
+
// open on_you + on_others: what the deck holds
|
|
408
|
+
// agents_at_work active claims, one sky pod each ("5 agents working": a tile is
|
|
409
|
+
// one number and two words, the owner's line rule)
|
|
410
|
+
// moving tasks being built or waiting to land (GET /moving: active +
|
|
411
|
+
// confirmed, owner 2026-10-08); never below agents_at_work,
|
|
412
|
+
// since every active claim holds an active task
|
|
413
|
+
// `session` { answered: ids answered or passed on in this page view, extra: asks
|
|
414
|
+
// a building added this visit } is the page's own state, applied the same way
|
|
415
|
+
// wherever the numbers are printed. A figure that could not be read is null,
|
|
416
|
+
// never 0: zero says nothing waits on you.
|
|
417
|
+
// THE LABELS. The main count is "to do" (the owner's word, 2026-10-09; task
|
|
418
|
+
// 1004701): after a number "24 to do", and a heading that names the count (the
|
|
419
|
+
// street's header, the tally) is countLabel(): "to do 24". A desk's own floor
|
|
420
|
+
// line keeps "needs you": that is one matter, not the count.
|
|
421
|
+
const COUNT_WORDS = Object.freeze({
|
|
422
|
+
on_you: Object.freeze(['to do', 'to do']),
|
|
423
|
+
on_others: Object.freeze(['waits on others', 'wait on others']),
|
|
424
|
+
open: Object.freeze(['to decide', 'to decide']),
|
|
425
|
+
agents_at_work: Object.freeze(['agent working', 'agents working']),
|
|
426
|
+
moving: Object.freeze(['moving', 'moving']),
|
|
427
|
+
});
|
|
428
|
+
const COUNT_LABELS = Object.freeze({ on_you: 'to do', on_others: 'on others', open: 'to decide', agents_at_work: 'agents working', moving: 'moving' });
|
|
429
|
+
function countWords(key, n) { const w = COUNT_WORDS[key]; return w ? w[n === 1 ? 0 : 1] : ''; }
|
|
430
|
+
function countLabel(key) { return COUNT_LABELS[key] || ''; }
|
|
431
|
+
function isOnYou(a) { return !!a && a.state === 'needs' && !!a.needs_you; }
|
|
432
|
+
function isOnOthers(a) { return !!a && a.state === 'needs' && !a.needs_you; }
|
|
433
|
+
// THE SERVER'S COUNTS (task 1004716): GET /government/counts answers all five
|
|
434
|
+
// numbers in one read, over the docket's own routes (modules/government/
|
|
435
|
+
// counts.js), and names the matters on you and on others. Read, they ARE the
|
|
436
|
+
// real part of every count: this page takes away what it answered this visit
|
|
437
|
+
// and adds only what no server read holds yet (an ask a building added this
|
|
438
|
+
// visit). Not read, the counts are made here from the docket and the feeds, as
|
|
439
|
+
// before; tests/government_counts.mjs holds the two equal.
|
|
440
|
+
function servedCounts(c) {
|
|
441
|
+
if (!obj(c) || !Array.isArray(c.yours) || !Array.isArray(c.others)) return null;
|
|
442
|
+
const n = (v) => (typeof v === 'number' && isFinite(v) && v >= 0 ? v : null);
|
|
443
|
+
return { yours: c.yours.map(str), others: c.others.map(str), agents_at_work: n(c.agents_at_work), moving: n(c.moving) };
|
|
444
|
+
}
|
|
445
|
+
function countsOf(model, session) {
|
|
446
|
+
const city = model && obj(model.city) ? model.city : null;
|
|
447
|
+
const s = obj(session) || {};
|
|
448
|
+
const gone = {};
|
|
449
|
+
arr(s.answered).forEach((id) => { gone[str(id)] = true; });
|
|
450
|
+
const seen = {};
|
|
451
|
+
const extra = {};
|
|
452
|
+
arr(s.extra).forEach((a) => { if (a && a.id != null) extra[str(a.id)] = true; });
|
|
453
|
+
const asks = (city ? arr(city.asks) : []).concat(arr(s.extra)).filter((a) => {
|
|
454
|
+
if (!a || a.id == null || seen[a.id] || gone[str(a.id)]) return false;
|
|
455
|
+
seen[a.id] = true;
|
|
456
|
+
return true;
|
|
457
|
+
});
|
|
458
|
+
const served = city && obj(city.served) ? city.served : null;
|
|
459
|
+
const t = city && obj(city.traffic) ? city.traffic : null;
|
|
460
|
+
if (served) {
|
|
461
|
+
const held = {};
|
|
462
|
+
served.yours.concat(served.others).forEach((id) => { held[id] = true; });
|
|
463
|
+
const own = asks.filter((a) => !held[str(a.id)] && extra[str(a.id)]);
|
|
464
|
+
const onYou = served.yours.filter((id) => !gone[id]).length + own.filter(isOnYou).length;
|
|
465
|
+
const onOthers = served.others.filter((id) => !gone[id]).length + own.filter(isOnOthers).length;
|
|
466
|
+
const src = R;
|
|
467
|
+
return {
|
|
468
|
+
on_you: onYou,
|
|
469
|
+
on_others: onOthers,
|
|
470
|
+
open: onYou + onOthers,
|
|
471
|
+
agents_at_work: served.agents_at_work != null ? served.agents_at_work : t && t.read ? Number(t.agents) || 0 : null,
|
|
472
|
+
moving: served.moving != null ? served.moving : t && t.moving != null ? Number(t.moving) : null,
|
|
473
|
+
_sources: { on_you: src, on_others: src, open: src, agents_at_work: R, moving: R },
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
// the asks stand on the docket: without it there is nothing honest to count
|
|
477
|
+
const read = !!city && !!city.docket_read;
|
|
478
|
+
const onYou = read ? asks.filter(isOnYou).length : null;
|
|
479
|
+
const onOthers = read ? asks.filter(isOnOthers).length : null;
|
|
480
|
+
return {
|
|
481
|
+
on_you: onYou,
|
|
482
|
+
on_others: onOthers,
|
|
483
|
+
open: read ? onYou + onOthers : null,
|
|
484
|
+
agents_at_work: t && t.read ? Number(t.agents) || 0 : null,
|
|
485
|
+
moving: t && t.moving != null ? Number(t.moving) : null,
|
|
486
|
+
_sources: { on_you: D, on_others: D, open: D, agents_at_work: D, moving: R },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---------- my agents ----------
|
|
491
|
+
function agentSettings(me) {
|
|
492
|
+
const m = obj(me) || {};
|
|
493
|
+
const out = [];
|
|
494
|
+
const inter = obj(m.interaction);
|
|
495
|
+
if (inter) {
|
|
496
|
+
arr(inter.fields).forEach((f) => {
|
|
497
|
+
const eff = obj(inter.effective) || {};
|
|
498
|
+
out.push({
|
|
499
|
+
key: str(f.field), label: str(f.label), group: 'How it talks to you', source: 'me/interaction',
|
|
500
|
+
options: arr(f.values).map((v) => v.value), default: f.default_value,
|
|
501
|
+
value: eff[f.field] !== undefined ? eff[f.field] : f.default_value, real: true,
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const wand = obj(m.wandering);
|
|
506
|
+
if (wand) {
|
|
507
|
+
out.push({
|
|
508
|
+
key: 'wandering', label: 'How far agents wander', group: 'How your agents work', source: 'me/wandering',
|
|
509
|
+
options: arr(wand.levels).filter((l) => l.allowed !== false).map((l) => l.key), default: wand.default_key,
|
|
510
|
+
value: wand.effective_key || wand.default_key, real: true,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const alloc = obj(m.model_allocation);
|
|
514
|
+
if (alloc) {
|
|
515
|
+
arr(alloc.slots).forEach((s) => {
|
|
516
|
+
const eff = obj(alloc.effective) || {};
|
|
517
|
+
out.push({
|
|
518
|
+
key: 'model.' + str(s.slot), label: str(s.label), group: 'Which models run', source: 'me/model-allocation',
|
|
519
|
+
options: arr(alloc.allowed_tiers).slice(), default: s.code_default,
|
|
520
|
+
value: eff[s.slot] !== undefined ? eff[s.slot] : s.code_default, real: true,
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
// THE LOCKS (task 1004707): GET /me's `laws.locks` — what each law in force
|
|
525
|
+
// did to THIS viewer's settings, enforced by the server. The slice's effective
|
|
526
|
+
// value already obeys it; `from` is the viewer's own. A lock the server could
|
|
527
|
+
// not enforce (a setting no table holds yet) locks nothing here: the row would
|
|
528
|
+
// claim an enforcement that is not happening.
|
|
529
|
+
const realLockFor = {};
|
|
530
|
+
arr(obj(m.laws) && m.laws.locks).forEach((l) => { if (l && l.enforced && l.setting && !realLockFor[l.setting]) realLockFor[l.setting] = l; });
|
|
531
|
+
return out.map((s) => {
|
|
532
|
+
const real = realLockFor[s.key] || null;
|
|
533
|
+
// A CAP (task 1004714): a law that only caps a setting ({ setting, max })
|
|
534
|
+
// leaves every value up to the cap the viewer's to pick. The server names
|
|
535
|
+
// them (`allowed`, law-locks.js); the row offers those and is not locked. A
|
|
536
|
+
// cap that leaves one value or fewer is a lock in all but name.
|
|
537
|
+
const capAllowed = real && real.max !== undefined && Array.isArray(real.allowed)
|
|
538
|
+
? s.options.filter((v) => real.allowed.some((a) => String(a) === String(v))) : null;
|
|
539
|
+
if (capAllowed && capAllowed.length > 1) {
|
|
540
|
+
return {
|
|
541
|
+
key: s.key, label: s.label, group: s.group, options: capAllowed, default: s.default,
|
|
542
|
+
value: real.from != null ? real.from : s.value, effective: s.value, locked: false, lockedBy: null, advisedBy: null,
|
|
543
|
+
cappedBy: { law_id: 'law:' + str(real.law_id), no: Number(real.law_no) || null, text: str(real.law_text), max: real.max, real: true },
|
|
544
|
+
_sources: { key: R, label: R, options: R, default: R, value: R, effective: R, locked: R, lockedBy: R, advisedBy: D, cappedBy: R },
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
if (real) {
|
|
548
|
+
return {
|
|
549
|
+
key: s.key, label: s.label, group: s.group, options: s.options, default: s.default,
|
|
550
|
+
value: real.from != null ? real.from : s.value, effective: s.value, locked: true,
|
|
551
|
+
lockedBy: { law_id: 'law:' + str(real.law_id), no: Number(real.law_no) || null, text: str(real.law_text), real: true }, advisedBy: null,
|
|
552
|
+
_sources: { key: R, label: R, options: R, default: R, value: R, effective: R, locked: R, lockedBy: R, advisedBy: D },
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
// no law in force touches it: the viewer's own, free to change
|
|
556
|
+
return {
|
|
557
|
+
key: s.key, label: s.label, group: s.group, options: s.options, default: s.default,
|
|
558
|
+
value: s.value, effective: s.value, locked: false, lockedBy: null, advisedBy: null,
|
|
559
|
+
_sources: { key: R, label: R, options: R, default: R, value: R, effective: R, locked: D, lockedBy: D, advisedBy: D },
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---------- the whole model ----------
|
|
565
|
+
function livingModel(input) {
|
|
566
|
+
const o = obj(input) || {};
|
|
567
|
+
const now = Number(o.now) > 0 ? Number(o.now) : null;
|
|
568
|
+
const docket = obj(o.docket) && Array.isArray(o.docket.matters) ? o.docket : null;
|
|
569
|
+
const goalsAll = arr(o.goals, 'goals');
|
|
570
|
+
const rollups = {};
|
|
571
|
+
arr(o.rollups, 'rollups').forEach((r) => { if (r && r.goal_id != null) rollups[String(r.goal_id)] = r; });
|
|
572
|
+
const claims = arr(o.claims, 'active_claims');
|
|
573
|
+
// whether the claims feed was read at all: no read is "unknown", never "no agent"
|
|
574
|
+
const claimsRead = Array.isArray(o.claims) || !!(obj(o.claims) && Array.isArray(o.claims.active_claims));
|
|
575
|
+
const tasks = arr(o.tasks, 'tasks');
|
|
576
|
+
const person = peopleIndex(o.roster, docket);
|
|
577
|
+
const me = obj(o.me) ? (obj(o.me.builder) ? o.me : { builder: o.me }) : null;
|
|
578
|
+
|
|
579
|
+
// the viewer
|
|
580
|
+
const vb = me && me.builder;
|
|
581
|
+
const viewerId = vb && vb.id != null ? String(vb.id) : null;
|
|
582
|
+
|
|
583
|
+
// real holders per building (who may decide there today)
|
|
584
|
+
const holders = {};
|
|
585
|
+
const dBuilding = {};
|
|
586
|
+
arr(docket && docket.buildings).forEach((b) => {
|
|
587
|
+
if (!b || !b.key) return;
|
|
588
|
+
dBuilding[b.key] = b;
|
|
589
|
+
holders[b.key] = arr(b.holders).map((h) => person(h.id));
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// THE LEADS (task 1004709): GET /government/leads — who leads each building and
|
|
593
|
+
// who holds the owner seat. Unread, no building names a Lead and no owner.
|
|
594
|
+
const leadsRead = obj(o.leads) && Array.isArray(o.leads.buildings) ? o.leads : null;
|
|
595
|
+
const who = (p) => ({ id: str(p.id), login: str(p.login), name: str(p.name || p.login) });
|
|
596
|
+
const ownerRow = leadsRead && obj(leadsRead.owner) ? arr(leadsRead.owner.builders)[0] : null;
|
|
597
|
+
const owner = obj(ownerRow) && ownerRow.id != null ? who(ownerRow) : null;
|
|
598
|
+
const leads = {};
|
|
599
|
+
if (leadsRead) {
|
|
600
|
+
leadsRead.buildings.forEach((b) => {
|
|
601
|
+
const l = b && obj(b.lead);
|
|
602
|
+
if (!l || BUILDING_KEYS.indexOf(b.building) < 0 || !obj(l.builder) || l.builder.id == null) return;
|
|
603
|
+
leads[b.building] = { person: who(l.builder), since: l.since || null, charter: { scope: str(l.charter) }, committees: [], real: true };
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
// THE STRUCTURE (task 1004711): the same read carries what each building has
|
|
607
|
+
// grown under its holder (`structure`: build-outs standing, built or going up,
|
|
608
|
+
// and the ones waiting on a vote). A building's committee rooms are its real
|
|
609
|
+
// ones (no table seats members in them yet) and its build-outs are drawn from it.
|
|
610
|
+
const structRead = {};
|
|
611
|
+
if (leadsRead) {
|
|
612
|
+
leadsRead.buildings.forEach((b) => {
|
|
613
|
+
if (!b || BUILDING_KEYS.indexOf(b.building) < 0 || !obj(b.structure)) return;
|
|
614
|
+
structRead[b.building] = { standing: arr(b.structure.standing).filter(obj), proposed: arr(b.structure.proposed).filter(obj) };
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
const realRooms = (key) => structRead[key].standing.filter((x) => x.capsule === 'committee')
|
|
618
|
+
.map((x) => ({ key: 'b' + str(x.id), name: str(x.name), line: str(x.line), members: [], real: true, state: x.state === 'built' ? 'built' : 'building' }));
|
|
619
|
+
const realBuilt = (key) => structRead[key].standing.filter((x) => x.capsule !== 'committee' && x.state === 'built')
|
|
620
|
+
.map((x) => ({ id: 'buildout:' + str(x.id), capsule: str(x.capsule), name: str(x.name), line: str(x.line) }));
|
|
621
|
+
Object.keys(leads).forEach((k) => { if (structRead[k]) leads[k] = Object.assign({}, leads[k], { committees: realRooms(k) }); });
|
|
622
|
+
// THE PRESETS (task 1004715): GET /government/presets. The project's strictness
|
|
623
|
+
// is the preset its laws in force amount to (null: none), and each preset
|
|
624
|
+
// carries the locks a switch to it would leave in force. Unread: unknown (null).
|
|
625
|
+
const presetsRead = obj(o.presets) && Array.isArray(o.presets.presets) ? o.presets : null;
|
|
626
|
+
const realPresets = presetsRead ? presetsRead.presets.filter((p) => obj(p) && p.key).map((p) => ({
|
|
627
|
+
key: str(p.key), name: str(p.name), line: str(p.line), locks: arr(p.locks).filter((l) => obj(l) && l.setting).map((l) => Object.assign({}, l)),
|
|
628
|
+
})) : null;
|
|
629
|
+
const realCurrent = realPresets ? realPresets.find((p) => p.key === presetsRead.current) || null : null;
|
|
630
|
+
const ownerIds = leadsRead && obj(leadsRead.owner) ? arr(leadsRead.owner.builders).filter((p) => obj(p) && p.id != null).map((p) => str(p.id)) : null;
|
|
631
|
+
const resolveSeat = seatResolver({ owner, ownerIds, leads, holders });
|
|
632
|
+
// routed: the server routed every docket matter (task 1004705)
|
|
633
|
+
// (a docket is read per viewer: its `yours` is only ever its own viewer's)
|
|
634
|
+
const routed = !!(docket && docket.routed === true);
|
|
635
|
+
const routedFor = routed && obj(docket.viewer) && docket.viewer.id != null ? String(docket.viewer.id) : null;
|
|
636
|
+
const ctx = { now, viewerId, resolveSeat, routed, routedFor, seated: !!leadsRead };
|
|
637
|
+
|
|
638
|
+
// ---- asks: every docket matter, and every open blocker it does not already carry ----
|
|
639
|
+
const humanOf = (a) => (a ? {
|
|
640
|
+
question: a.question, options: a.options, recommend: a.recommend, waits_on: a.waits_on,
|
|
641
|
+
if_nobody_answers: a.if_nobody_answers, routing_note: a.routing_note || null,
|
|
642
|
+
} : null);
|
|
643
|
+
// THE STORED ASK (task 1004703): a matter whose agent wrote its card in the ask
|
|
644
|
+
// shape carries it as `ask` (GET /government/docket, GET /blockers), checked by
|
|
645
|
+
// the server's own rules (modules/government/asks.js). Its words are real; a
|
|
646
|
+
// matter without one has no card (`human: null`) and shows in its own words.
|
|
647
|
+
// Who its seat names is the server's route when the docket is routed (makeAsk
|
|
648
|
+
// relabels waits_on and needs_you), else the seat the real Leads resolve.
|
|
649
|
+
const ASK_SOURCES = { id: R, source: R, building: R, state: R, urgent: D, opened_at: R, deadline: R, age_days: D, rule: R, ref: R, href: R, acts: R, tally: R, raw: R, human: R, format: D, shown: D, waits_on: D, needs_you: D, ask_real: D };
|
|
650
|
+
const storedOf = (a) => (obj(a) && str(a.question) ? a : null);
|
|
651
|
+
const asks = [];
|
|
652
|
+
const seen = {};
|
|
653
|
+
arr(docket && docket.matters).forEach((m) => {
|
|
654
|
+
if (!m || m.id == null || seen[m.id]) return;
|
|
655
|
+
seen[m.id] = true;
|
|
656
|
+
const building = BUILDING_KEYS.indexOf(m.building) >= 0 ? m.building : 'planning';
|
|
657
|
+
const stored = storedOf(m.ask);
|
|
658
|
+
asks.push(makeAsk({
|
|
659
|
+
id: m.id, source: m.source, building, state: m.state, opened_at: m.opened_at, deadline: m.deadline,
|
|
660
|
+
rule: m.rule, ref: m.ref, href: m.href, acts: m.acts, tally: m.tally, raw_title: m.title, raw_detail: m.detail, from_docket: true, ask_real: !!stored, route: m.route,
|
|
661
|
+
}, humanOf(stored), ctx, ASK_SOURCES));
|
|
662
|
+
});
|
|
663
|
+
arr(o.blockers, 'blockers').forEach((b) => {
|
|
664
|
+
if (!b || b.id == null) return;
|
|
665
|
+
const id = 'blockers:' + b.id;
|
|
666
|
+
if (seen[id] || (b.status && b.status !== 'open')) return;
|
|
667
|
+
seen[id] = true;
|
|
668
|
+
const stored = storedOf(b.ask);
|
|
669
|
+
asks.push(makeAsk({
|
|
670
|
+
id, source: 'blockers', building: 'planning', state: 'needs', opened_at: b.created_at, deadline: null,
|
|
671
|
+
rule: 'decide', ref: { type: 'blocker', id: String(b.id) }, href: '/blockers#blocker-' + b.id, acts: [], raw_title: b.title, raw_detail: b.body_md, ask_real: !!stored,
|
|
672
|
+
}, humanOf(stored), ctx, ASK_SOURCES));
|
|
673
|
+
});
|
|
674
|
+
asks.sort(compareAsks);
|
|
675
|
+
|
|
676
|
+
// ---- traffic: where the agents are ----
|
|
677
|
+
const goalById = {};
|
|
678
|
+
goalsAll.forEach((g) => { if (g && g.id != null) goalById[String(g.id)] = g; });
|
|
679
|
+
const taskById = {};
|
|
680
|
+
tasks.forEach((t) => { if (t && t.id != null) taskById[String(t.id)] = t; });
|
|
681
|
+
const placed = claims.map((c) => {
|
|
682
|
+
const t = taskById[String(c.task_id)];
|
|
683
|
+
const goalId = t && t.goal_id != null ? String(t.goal_id) : null;
|
|
684
|
+
const building = (goalId && goalBuilding(goalById[goalId])) || touchesBuilding(c.touches) || null;
|
|
685
|
+
return { claim_id: str(c.claim_id), task_id: str(c.task_id), goal_id: goalId, building, builder: person(c.builder_id), since: c.claimed_at || null };
|
|
686
|
+
});
|
|
687
|
+
const claimsAt = {};
|
|
688
|
+
const claimsInGoal = {};
|
|
689
|
+
placed.forEach((p) => {
|
|
690
|
+
if (p.building) claimsAt[p.building] = (claimsAt[p.building] || 0) + 1;
|
|
691
|
+
if (p.goal_id) claimsInGoal[p.goal_id] = (claimsInGoal[p.goal_id] || 0) + 1;
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// ---- construction ----
|
|
695
|
+
const construction = {};
|
|
696
|
+
BUILDING_KEYS.forEach((k) => { construction[k] = []; });
|
|
697
|
+
// A LEAD'S BUILD-OUTS (task 1004711), from GET /government/leads `structure`:
|
|
698
|
+
// one going up is a live site (scaffold, crane) at its real progress — a day
|
|
699
|
+
// from when it started; one waiting on a vote is a ghost whose tally is its
|
|
700
|
+
// sitting's on the docket (`ask_id` is that docket id). First in the list, so
|
|
701
|
+
// the drawing shows the build-out ahead of any other open sitting there.
|
|
702
|
+
const buildoutVotes = {};
|
|
703
|
+
BUILDING_KEYS.forEach((k) => {
|
|
704
|
+
const s = structRead[k];
|
|
705
|
+
if (!s) return;
|
|
706
|
+
s.standing.filter((x) => x.state === 'building').forEach((x) => {
|
|
707
|
+
const allowed = obj(x.allowed) ? x.allowed : {};
|
|
708
|
+
construction[k].push({
|
|
709
|
+
kind: 'buildout', capsule: str(x.capsule), id: 'buildout:' + str(x.id), label: str(x.name), line: str(x.line),
|
|
710
|
+
authorised: true, authorised_by: allowed.path === 'vote' ? 'a passed vote' : str(allowed.why) || null, needs: null,
|
|
711
|
+
progress: clamp01(x.progress), sitting_id: allowed.item_id != null ? String(allowed.item_id) : null,
|
|
712
|
+
illustrative: false, driven_by_task: null, task_status: null, ask_id: null,
|
|
713
|
+
_sources: { kind: D, capsule: R, id: R, label: R, line: R, authorised: D, authorised_by: R, needs: D, progress: R, sitting_id: R, illustrative: D, driven_by_task: D, task_status: D, ask_id: D },
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
s.proposed.forEach((x) => {
|
|
717
|
+
const id = str(x.docket_id);
|
|
718
|
+
buildoutVotes[id] = true;
|
|
719
|
+
construction[k].push({
|
|
720
|
+
kind: 'scaffold', capsule: str(x.capsule), id, label: str(x.name), line: str(x.line),
|
|
721
|
+
authorised: false, authorised_by: null, needs: 'a vote',
|
|
722
|
+
progress: null, sitting_id: x.item_id != null ? String(x.item_id) : null,
|
|
723
|
+
illustrative: false, driven_by_task: null, task_status: null, ask_id: id,
|
|
724
|
+
_sources: { kind: D, capsule: R, id: R, label: R, line: R, authorised: D, authorised_by: D, needs: D, progress: D, sitting_id: R, illustrative: D, driven_by_task: D, task_status: D, ask_id: R },
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
arr(docket && docket.matters).forEach((m) => {
|
|
729
|
+
// a sitting still open is a proposal drawn into the city: a real scaffold
|
|
730
|
+
// (a build-out's own sitting is drawn above, as its ghost)
|
|
731
|
+
if (!m || m.source !== 'board' || m.state === 'settled' || BUILDING_KEYS.indexOf(m.building) < 0 || buildoutVotes[str(m.id)]) return;
|
|
732
|
+
construction[m.building].push({
|
|
733
|
+
kind: 'scaffold', id: str(m.id), label: str(m.short || m.title), line: str(m.title), authorised: false,
|
|
734
|
+
progress: null, sitting_id: m.ref && m.ref.id != null ? String(m.ref.id) : null, illustrative: false, task_status: null,
|
|
735
|
+
_sources: { kind: D, id: R, label: R, line: R, authorised: D, progress: D, sitting_id: R, illustrative: D, task_status: D },
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
BUILDING_KEYS.forEach((k) => {
|
|
739
|
+
const d = dBuilding[k];
|
|
740
|
+
if (d && d.decision && d.decision.sitting) {
|
|
741
|
+
construction[k].push({
|
|
742
|
+
kind: 'scaffold', id: 'decision:' + k, label: 'A new way to decide', line: str(d.decision.rule_words), authorised: false,
|
|
743
|
+
progress: null, sitting_id: d.decision.set_by_item_id != null ? String(d.decision.set_by_item_id) : null, illustrative: false, task_status: null,
|
|
744
|
+
_sources: { kind: D, id: D, label: D, line: R, authorised: D, progress: D, sitting_id: R, illustrative: D, task_status: D },
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// ---- the nine buildings ----
|
|
750
|
+
const vocab = {};
|
|
751
|
+
arr(docket && docket.vocabulary && docket.vocabulary.buildings).forEach((v) => { if (v && v.key) vocab[v.key] = v; });
|
|
752
|
+
const readings = arr(docket && docket.readings);
|
|
753
|
+
const floors = arr(docket && docket.floors);
|
|
754
|
+
const buildings = BUILDING_KEYS.map((key) => {
|
|
755
|
+
const d = dBuilding[key] || {};
|
|
756
|
+
const v = vocab[key] || {};
|
|
757
|
+
const lead = leads[key] || null;
|
|
758
|
+
const here = asks.filter((a) => a.building === key);
|
|
759
|
+
const needs = here.filter((a) => a.state === 'needs').length;
|
|
760
|
+
const movingAsks = here.filter((a) => a.state === 'moving').length;
|
|
761
|
+
const agents = claimsAt[key] || 0;
|
|
762
|
+
const activity = round3(clamp01(agents / ACTIVITY_FULL));
|
|
763
|
+
const lit = round3(clamp01(0.15 + 0.55 * activity + (needs > 0 ? 0.3 : 0)));
|
|
764
|
+
const seats = holders[key] ? holders[key].map((p) => ({ id: p.id, login: p.login, name: p.name, role: 'holder' })) : [];
|
|
765
|
+
if (lead && lead.person) seats.unshift({ id: lead.person.id, login: lead.person.login, name: lead.person.name, role: 'lead' });
|
|
766
|
+
const decision = d.decision ? {
|
|
767
|
+
rule: d.decision.rule || null, rule_words: d.decision.rule_words || null, who: d.decision.who || null,
|
|
768
|
+
decided_by: d.decision.decided_by || null, sitting: !!d.decision.sitting,
|
|
769
|
+
} : null;
|
|
770
|
+
return {
|
|
771
|
+
key,
|
|
772
|
+
name: str(d.name || v.name || BUILDING_NAMES[key]),
|
|
773
|
+
group: str(d.group || v.group),
|
|
774
|
+
lead: lead && lead.person ? Object.assign({ since: lead.since }, lead.person) : null,
|
|
775
|
+
held_by: lead && lead.person ? 'lead' : 'owner',
|
|
776
|
+
holder: lead && lead.person ? lead.person : owner,
|
|
777
|
+
seats,
|
|
778
|
+
charter: lead ? lead.charter : null,
|
|
779
|
+
// its committee rooms and the build-outs that stand built (GET
|
|
780
|
+
// /government/leads `structure`, task 1004711; empty unread)
|
|
781
|
+
structure: {
|
|
782
|
+
committees: structRead[key] ? realRooms(key) : [],
|
|
783
|
+
built: structRead[key] ? realBuilt(key) : [],
|
|
784
|
+
},
|
|
785
|
+
decision,
|
|
786
|
+
// who answers for it, as GET /government/leads says (null without that read)
|
|
787
|
+
answers: leadsRead ? answersOf(leadsRead, key) : null,
|
|
788
|
+
// what its holder may build out without a vote: the server's answer (null unread)
|
|
789
|
+
build_rights: leadsRead ? rightsOf(leadsRead, key) : null,
|
|
790
|
+
asks: here,
|
|
791
|
+
counts: { needs, moving: agents + movingAsks, building: construction[key].length, asks: here.length },
|
|
792
|
+
activity,
|
|
793
|
+
agents,
|
|
794
|
+
construction: construction[key],
|
|
795
|
+
lit,
|
|
796
|
+
readings: readings.filter((r) => r && r.building === key),
|
|
797
|
+
floors: floors.filter((f) => f && f.building === key).length,
|
|
798
|
+
_sources: {
|
|
799
|
+
key: D, name: R, group: R, decision: R, asks: M,
|
|
800
|
+
// the Lead, its holder and charter: GET /government/leads (task 1004709)
|
|
801
|
+
lead: R, held_by: D, holder: R, charter: R, seats: D, structure: R, answers: R, build_rights: R,
|
|
802
|
+
'counts.needs': D, 'counts.moving': D, 'counts.building': D, 'counts.asks': D, activity: D, agents: D, construction: M, lit: D, readings: R, floors: D,
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// ---- districts: one per open goal, in the order the goals come ----
|
|
808
|
+
// the treasury (task 1004712); unread, no district shows a pot
|
|
809
|
+
const treasuryRead = obj(o.treasury) && Array.isArray(o.treasury.pots) ? o.treasury : null;
|
|
810
|
+
const pots = {};
|
|
811
|
+
if (treasuryRead) treasuryRead.pots.forEach((p) => { if (p && p.goal_id != null) pots[String(p.goal_id)] = p; });
|
|
812
|
+
// a pot's borrow is the docket matter its server filed, when the docket carries it
|
|
813
|
+
const borrowOf = (p) => {
|
|
814
|
+
const b = obj(p.borrow);
|
|
815
|
+
return b && b.state === 'open' && b.matter_id && seen[b.matter_id] ? str(b.matter_id) : null;
|
|
816
|
+
};
|
|
817
|
+
const districts = goalsAll.filter((g) => g && g.status === 'open' && g.id != null).map((g) => {
|
|
818
|
+
const id = String(g.id);
|
|
819
|
+
const r = rollups[id] || {};
|
|
820
|
+
const inGoal = claimsInGoal[id] || 0;
|
|
821
|
+
const last = time(r.last_activity_at);
|
|
822
|
+
const recent = now != null && last != null && now - last <= DAY_MS;
|
|
823
|
+
let activity = clamp01(Math.max(inGoal, Number(r.active) || 0) / ACTIVITY_FULL);
|
|
824
|
+
if (recent) activity = Math.max(activity, RECENT_FLOOR);
|
|
825
|
+
const p = pots[id];
|
|
826
|
+
const pot = !p ? null : {
|
|
827
|
+
budget_usd: round2(p.budget_usd), spent_usd: round2(p.spent_usd), lent_usd: round2(p.lent_usd), left_usd: round2(p.left_usd),
|
|
828
|
+
dry: !!p.dry, share: p.share != null ? Number(p.share) : null, borrow_ask_id: borrowOf(p), note: null, real: true,
|
|
829
|
+
};
|
|
830
|
+
return {
|
|
831
|
+
goal_id: id,
|
|
832
|
+
title: str(g.title),
|
|
833
|
+
version_id: g.version_id || null,
|
|
834
|
+
working_area: g.working_area != null ? Number(g.working_area) : null,
|
|
835
|
+
building: goalBuilding(g),
|
|
836
|
+
counts: {
|
|
837
|
+
shipped: Number(r.done) || 0, active: Number(r.active) || 0, ready: Number(r.ready) || 0,
|
|
838
|
+
blocked: Number(r.blocked) || 0, backlog: Number(r.backlog) || 0, total: Number(r.total) || 0,
|
|
839
|
+
},
|
|
840
|
+
last_activity_at: r.last_activity_at || null,
|
|
841
|
+
agents: inGoal,
|
|
842
|
+
activity: round3(activity),
|
|
843
|
+
pot,
|
|
844
|
+
_sources: { goal_id: R, title: R, version_id: R, working_area: R, building: D, counts: R, last_activity_at: R, agents: D, activity: D, pot: R },
|
|
845
|
+
};
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// ---- the city as a whole ----
|
|
849
|
+
const cost = obj(o.cost) || {};
|
|
850
|
+
const dayKey = now != null ? new Date(now).toISOString().slice(0, 10) : null;
|
|
851
|
+
const monthKey = dayKey ? dayKey.slice(0, 7) : null;
|
|
852
|
+
const daily = arr(cost.daily);
|
|
853
|
+
const byMonth = arr(cost.by_month);
|
|
854
|
+
const today = dayKey ? daily.find((x) => str(x.day).slice(0, 10) === dayKey) : null;
|
|
855
|
+
const month = monthKey ? byMonth.find((x) => x.month === monthKey) : null;
|
|
856
|
+
const monthIdx = month ? byMonth.indexOf(month) : -1;
|
|
857
|
+
const money = readings.find((r) => r && r.key === 'money');
|
|
858
|
+
const mv = obj(o.moving);
|
|
859
|
+
const sessionsList = arr(o.sessions, 'sessions');
|
|
860
|
+
const recentSessions = now != null ? sessionsList.filter((s) => { const t = time(s.uploaded_at); return t != null && now - t <= DAY_MS; }) : [];
|
|
861
|
+
const learningsList = arr(o.learnings, 'learnings');
|
|
862
|
+
const rulesReading = readings.find((r) => r && r.key === 'rules');
|
|
863
|
+
// THE LAW BOOK (task 1004706): GET /government/laws — every law in force,
|
|
864
|
+
// passed by a sitting of the government in force; a repealed law is not in the
|
|
865
|
+
// book. Its locks are enforced by the server (task 1004707): My agents reads
|
|
866
|
+
// what each lock did to the viewer off GET /me's `laws.locks`, never from this book.
|
|
867
|
+
const laws = arr(o.laws, 'laws').filter((l) => l && l.id != null && l.in_force !== false).map((l) => {
|
|
868
|
+
const pb = obj(l.passed_by) || {};
|
|
869
|
+
return {
|
|
870
|
+
id: 'law:' + str(l.id), text: str(l.text),
|
|
871
|
+
passed_by: pb.government ? { id: null, login: '', name: str(pb.government) } : null,
|
|
872
|
+
passed_at: l.passed_at || null, origin: 'vote', effect_on_ai: str(l.effect_on_ai), locks: arr(l.locks).slice(),
|
|
873
|
+
enforced_by: null, real: true,
|
|
874
|
+
_sources: { id: R, text: R, passed_by: R, passed_at: R, origin: D, effect_on_ai: R, locks: R, enforced_by: D, real: D },
|
|
875
|
+
};
|
|
876
|
+
});
|
|
877
|
+
if (rulesReading && rulesReading.status === 'live') {
|
|
878
|
+
// the one standing rule the project really runs (the docket's rules-at-work reading)
|
|
879
|
+
// its name is the reading's own words ("the landing rule settled 24 filings…")
|
|
880
|
+
const named = /^the ([a-z][a-z -]*? rule)\b/i.exec(str(rulesReading.line));
|
|
881
|
+
laws.unshift({
|
|
882
|
+
id: 'rules:' + str(rulesReading.key), text: named ? 'The ' + named[1] : str(rulesReading.figure), passed_by: null, passed_at: null, origin: 'docket',
|
|
883
|
+
effect_on_ai: str(rulesReading.line), locks: [], enforced_by: null, real: true,
|
|
884
|
+
_sources: { id: D, text: D, passed_by: D, passed_at: D, origin: D, effect_on_ai: R, locks: D, enforced_by: D, real: D },
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
const counts = { total: asks.length, needs_you: 0, needs: 0, moving: 0, settled: 0, refused: 0, by_seat: {} };
|
|
888
|
+
asks.forEach((a) => {
|
|
889
|
+
if (a.needs_you) counts.needs_you += 1;
|
|
890
|
+
counts[a.state] += 1;
|
|
891
|
+
if (!a.format.ok) counts.refused += 1;
|
|
892
|
+
counts.by_seat[a.waits_on.seat] = (counts.by_seat[a.waits_on.seat] || 0) + 1;
|
|
893
|
+
});
|
|
894
|
+
const city = {
|
|
895
|
+
line: docket && docket.line ? str(docket.line) : 'the docket could not be read just now',
|
|
896
|
+
matters: docket && docket.counts ? Object.assign({}, docket.counts) : null,
|
|
897
|
+
docket_read: !!docket,
|
|
898
|
+
// every docket matter was routed to a real seat on the server (task 1004705)
|
|
899
|
+
routed,
|
|
900
|
+
// the server's counts, when GET /government/counts was read (task 1004716)
|
|
901
|
+
served: servedCounts(o.counts),
|
|
902
|
+
laws,
|
|
903
|
+
asks,
|
|
904
|
+
ask_counts: counts,
|
|
905
|
+
traffic: {
|
|
906
|
+
agents: placed.length,
|
|
907
|
+
read: claimsRead,
|
|
908
|
+
moving: mv && mv.moving != null ? Number(mv.moving) : null,
|
|
909
|
+
by_building: claimsAt,
|
|
910
|
+
unplaced: placed.filter((p) => !p.building).length,
|
|
911
|
+
claims: placed,
|
|
912
|
+
},
|
|
913
|
+
spend: {
|
|
914
|
+
today_usd: today ? round2(today.amount_usd) : (dayKey && daily.length ? 0 : null),
|
|
915
|
+
month_usd: month ? round2(month.total_usd) : (monthKey && byMonth.length ? 0 : null),
|
|
916
|
+
last_month_usd: monthIdx > 0 ? round2(byMonth[monthIdx - 1].total_usd) : null,
|
|
917
|
+
total_usd: cost.total_usd != null ? round2(cost.total_usd) : null,
|
|
918
|
+
unattended: money && obj(money.value) ? { left_usd: money.value.left_usd, spent_usd: money.value.spent_usd, ceiling_usd: money.value.ceiling_usd, line: str(money.line) } : null,
|
|
919
|
+
reserve_usd: treasuryRead && obj(treasuryRead.reserve) && treasuryRead.reserve.usd != null ? round2(treasuryRead.reserve.usd) : null,
|
|
920
|
+
reserve_line: treasuryRead && obj(treasuryRead.reserve) ? str(treasuryRead.reserve.line) : null,
|
|
921
|
+
pots_basis: treasuryRead ? str(treasuryRead.basis) : null,
|
|
922
|
+
// no default spend limit (the owner's ruling): the treasury's cap setting, off
|
|
923
|
+
// unless the project set one; unknown without the read
|
|
924
|
+
limit: treasuryRead && obj(treasuryRead.cap) ? (treasuryRead.cap.on ? round2(treasuryRead.cap.usd) : 'none') : null,
|
|
925
|
+
cap: treasuryRead && obj(treasuryRead.cap) ? { on: !!treasuryRead.cap.on, usd: treasuryRead.cap.usd != null ? round2(treasuryRead.cap.usd) : null, line: str(treasuryRead.cap.line) } : null,
|
|
926
|
+
// who answers for the Treasury today, and whether it is the viewer
|
|
927
|
+
treasury_yours: treasuryRead && obj(treasuryRead.answers) ? !!treasuryRead.answers.yours : null,
|
|
928
|
+
},
|
|
929
|
+
sessions: { last_day: recentSessions.length, builders_last_day: Object.keys(recentSessions.reduce((m, s) => { m[str(s.builder_id)] = 1; return m; }, {})).length, total: (obj(o.sessions) && o.sessions.total != null) ? Number(o.sessions.total) : sessionsList.length },
|
|
930
|
+
learnings: { count: learningsList.length, latest_at: learningsList.reduce((m, l) => (str(l.created_at) > m ? str(l.created_at) : m), '') || null },
|
|
931
|
+
fence: obj(o.fence) ? { enabled: !!o.fence.enabled, paused_reason: o.fence.paused_reason || null, updated_at: o.fence.updated_at || null } : null,
|
|
932
|
+
_sources: {
|
|
933
|
+
line: R, matters: R, docket_read: D, routed: D, served: R, laws: M, asks: M, ask_counts: D,
|
|
934
|
+
'traffic.agents': D, 'traffic.read': D, 'traffic.moving': R, 'traffic.by_building': D, 'traffic.unplaced': D, 'traffic.claims': D, counts: M,
|
|
935
|
+
'spend.today_usd': R, 'spend.month_usd': R, 'spend.last_month_usd': R, 'spend.total_usd': R, 'spend.unattended': R,
|
|
936
|
+
'spend.reserve_usd': R, 'spend.reserve_line': R, 'spend.pots_basis': R, 'spend.limit': R, 'spend.cap': R, 'spend.treasury_yours': R,
|
|
937
|
+
sessions: D, learnings: D, fence: R,
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
// the numbers every surface prints, as nobody has answered anything yet
|
|
941
|
+
city.counts = countsOf({ city });
|
|
942
|
+
|
|
943
|
+
// ---- the viewer and my agents ----
|
|
944
|
+
const holds = BUILDING_KEYS.filter((k) => viewerId && (holders[k] || []).some((p) => p.id === viewerId));
|
|
945
|
+
const seats = [];
|
|
946
|
+
holds.forEach((k) => seats.push({ building: k, role: 'holder' }));
|
|
947
|
+
Object.keys(leads).forEach((k) => {
|
|
948
|
+
const l = leads[k];
|
|
949
|
+
if (l.person && l.person.id === viewerId) seats.push({ building: k, role: 'lead' });
|
|
950
|
+
});
|
|
951
|
+
if (ownerIds && viewerId && ownerIds.indexOf(viewerId) >= 0) seats.push({ building: null, role: 'owner' });
|
|
952
|
+
// task 1004713: GET /me's `positions` block is the server's answer to "which seats
|
|
953
|
+
// do you hold" (modules/government/positions.js). When /me carries it, the seats,
|
|
954
|
+
// the seat flag and the governance tab are that block's; without it, the seats
|
|
955
|
+
// the docket's holders and GET /government/leads give above.
|
|
956
|
+
const sent = me && obj(me.positions) && Array.isArray(me.positions.held) ? me.positions : null;
|
|
957
|
+
const realSeats = sent ? arr(sent.held).filter((p) => p && p.kind).map((p) => ({ building: p.building || null, role: str(p.kind), title: str(p.title) })) : null;
|
|
958
|
+
const seatsNow = realSeats || seats;
|
|
959
|
+
const seatSrc = realSeats ? R : D;
|
|
960
|
+
const viewer = vb ? {
|
|
961
|
+
id: viewerId, login: str(vb.github_login), name: str(vb.display_name || vb.github_login), rank: vb.rank || null,
|
|
962
|
+
holds, seats: seatsNow, has_seat: seatsNow.length > 0,
|
|
963
|
+
tabs: seatsNow.length > 0 ? ['city', 'agents', 'governance'] : ['city', 'agents'],
|
|
964
|
+
_sources: { id: R, login: R, name: R, rank: R, holds: D, seats: seatSrc, has_seat: seatSrc, tabs: seatSrc },
|
|
965
|
+
} : null;
|
|
966
|
+
const settings = agentSettings(me);
|
|
967
|
+
const myAgents = {
|
|
968
|
+
strictness: realCurrent ? realCurrent.key : null,
|
|
969
|
+
settings,
|
|
970
|
+
locked: settings.filter((s) => s.locked).length,
|
|
971
|
+
_sources: { strictness: R, settings: M, locked: D },
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
return {
|
|
975
|
+
generated_at: now != null ? new Date(now).toISOString() : null,
|
|
976
|
+
viewer,
|
|
977
|
+
owner,
|
|
978
|
+
// the preset the laws in force amount to (GET /government/presets); null unread
|
|
979
|
+
strictness: realPresets
|
|
980
|
+
? { key: realCurrent ? realCurrent.key : null, name: realCurrent ? realCurrent.name : 'No preset yet', line: realCurrent ? realCurrent.line : 'Laws are passed one at a time.', presets: realPresets, real: true }
|
|
981
|
+
: null,
|
|
982
|
+
buildings,
|
|
983
|
+
districts,
|
|
984
|
+
city,
|
|
985
|
+
myAgents,
|
|
986
|
+
// who leads each building is a real read (GET /government/leads, task 1004709)
|
|
987
|
+
seats_real: !!leadsRead,
|
|
988
|
+
// what needs whom is routed on the server (task 1004705)
|
|
989
|
+
routed,
|
|
990
|
+
_sources: { generated_at: D, viewer: M, owner: R, strictness: R, buildings: M, districts: M, city: M, myAgents: M, seats_real: D, routed: D },
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const api = {
|
|
995
|
+
livingModel, askFormat, slopScore, goalBuilding, touchesBuilding, compareAsks,
|
|
996
|
+
countsOf, countWords, countLabel, isOnYou, isOnOthers, COUNT_WORDS, COUNT_LABELS,
|
|
997
|
+
BUILDING_KEYS, BUILDING_NAMES, AREA_BUILDING, CATEGORY_BUILDING, MODULE_BUILDING, ASK_RULES, SLOP,
|
|
998
|
+
ACTIVITY_FULL, RECENT_FLOOR, SOURCES: Object.freeze({ REAL: R, DERIVED: D, MIXED: M }),
|
|
999
|
+
};
|
|
1000
|
+
root.GovLiving = api;
|
|
1001
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
1002
|
+
}(typeof window !== 'undefined' ? window : globalThis));
|