@bongos/core 1.21.113 → 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 +603 -183
- 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 +2 -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 +122 -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/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,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/asks.js — THE ASK: the one shape every request from an AI
|
|
4
|
+
// agent to a person takes (task 1004703, goal 1000111 "government is how a
|
|
5
|
+
// project steers its AI").
|
|
6
|
+
//
|
|
7
|
+
// WHY. The owner's biggest worry: agents "slop" people with run-on text when they
|
|
8
|
+
// need a decision, and a wall of words suppresses the decision. So a blocker, a
|
|
9
|
+
// "please confirm", a review or a sign-off carries an ASK, stored in this shape:
|
|
10
|
+
//
|
|
11
|
+
// { question: 'Turn unattended building back on?', // ≤ 12 words, ends '?'
|
|
12
|
+
// options: [ // 2 or 3 tap answers, each a short label + a one-line result
|
|
13
|
+
// { key: 'on', label: 'Turn it on', result: 'Agents build overnight again.', act?: 'go ahead' },
|
|
14
|
+
// { key: 'off', label: 'Keep it off', result: 'Nothing builds while you sleep.' } ],
|
|
15
|
+
// recommend: { key: 'on', why: 'The evaluation passed.' }, // one of the keys, one line
|
|
16
|
+
// waits_on: 'owner', // a seat: owner | lead:<b> | committee:<b>.<k> | board:<b>
|
|
17
|
+
// if_nobody_answers: 'It stays off.', // one line
|
|
18
|
+
// detail?: '…the long text…' } // rides along; never the ask itself
|
|
19
|
+
//
|
|
20
|
+
// The SERVER REFUSES an ask that breaks the shape and names what to fix
|
|
21
|
+
// (checkAsk → errors[]; refusalLine → one plain paragraph). The rules are the
|
|
22
|
+
// prototype's (modules/hall-ui/public/gov-living.js askFormat, ASK_RULES): the
|
|
23
|
+
// hall's copy and this one are held to the same verdicts, rule by rule, by
|
|
24
|
+
// tests/government_asks.mjs. The server adds what only a STORED ask needs: a
|
|
25
|
+
// seat it can be routed by (task 1004705 routes it), answer keys and acts in a
|
|
26
|
+
// fixed grammar, and caps on every string.
|
|
27
|
+
//
|
|
28
|
+
// WHERE AN ASK LIVES. One row per docket matter, keyed by the matter's docket id
|
|
29
|
+
// (`blockers:12`, `board:34`, `override_requests:5` — docket.js normalizeMatter),
|
|
30
|
+
// in government_asks (government_028). The docket attaches each matter's ask as
|
|
31
|
+
// `ask` (docket.js buildDocket), so every surface reading the docket — the
|
|
32
|
+
// street, the deck, the city — reads the same stored words. Written by POST
|
|
33
|
+
// /blockers (ideas, through the `government` port) and PUT
|
|
34
|
+
// /government/asks/:matter; checked without storing by POST /government/asks/check.
|
|
35
|
+
//
|
|
36
|
+
// PURE: no db, no requires. asks-db.js holds the queries.
|
|
37
|
+
|
|
38
|
+
// The prototype's rules, number for number (gov-living.js ASK_RULES).
|
|
39
|
+
const ASK_RULES = Object.freeze({ question_words: 12, options_min: 2, options_max: 3, label_words: 4, line_words: 14, why_words: 20 });
|
|
40
|
+
// Caps on characters, so a stored ask can never be a wall in a short coat.
|
|
41
|
+
const CAPS = Object.freeze({ question: 160, label: 40, line: 160, why: 200, detail: 4000 });
|
|
42
|
+
// No markup or links in a line a person reads at a glance (gov-living.js MARKUP).
|
|
43
|
+
const MARKUP = /\*\*|`|^#|\[[^\]]*\]\(|https?:\/\/|<\/?[a-z]/im;
|
|
44
|
+
// Control characters other than tab and line breaks (line breaks have their own rule).
|
|
45
|
+
// eslint-disable-next-line no-control-regex
|
|
46
|
+
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
47
|
+
const BUILDINGS = Object.freeze(['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'review', 'release', 'treasury']);
|
|
48
|
+
// The seats an ask can wait on (gov-living.js seatResolver's grammar). An
|
|
49
|
+
// unseated building falls to the owner when it is routed (owner ruling).
|
|
50
|
+
const SEAT = new RegExp(`^(owner|(lead|board):(${BUILDINGS.join('|')})|committee:(${BUILDINGS.join('|')})\\.[a-z][a-z0-9_]{0,31})$`);
|
|
51
|
+
// An answer's key: what a tap sends. Its act, optional: the matter's own act it
|
|
52
|
+
// takes ('go ahead', 'record', 'force:tested_by_hand'); the deck maps it onto
|
|
53
|
+
// the desk row's real route and refuses one the row does not offer.
|
|
54
|
+
const KEY = /^[a-z][a-z0-9_-]{0,23}$/;
|
|
55
|
+
const ACT = /^[a-z][a-z_ ]{0,31}(:[a-z_]{1,31})?$/;
|
|
56
|
+
// A docket matter's id, as docket.js normalizeMatter writes it: `<source>:<id>`.
|
|
57
|
+
const MATTER_ID = /^[a-z][a-z_]{1,39}:[A-Za-z0-9._-]{1,64}$/;
|
|
58
|
+
const MAX_OPTIONS_READ = 8;
|
|
59
|
+
|
|
60
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
61
|
+
const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : null);
|
|
62
|
+
function words(text) { return str(text).trim().split(/\s+/).filter(Boolean); }
|
|
63
|
+
|
|
64
|
+
// Sentences end at . ! ? followed by a space and a capital (or a digit, a quote,
|
|
65
|
+
// a bracket), or by the end; a version (1.20.82) or a decimal does not end one.
|
|
66
|
+
// The hall's own rule, character for character (gov-living.js sentences).
|
|
67
|
+
function sentences(text) {
|
|
68
|
+
const t = str(text).replace(/\s+/g, ' ').trim();
|
|
69
|
+
const out = [];
|
|
70
|
+
let start = 0;
|
|
71
|
+
for (let i = 0; i < t.length; i++) {
|
|
72
|
+
const ch = t[i];
|
|
73
|
+
if (ch !== '.' && ch !== '!' && ch !== '?') continue;
|
|
74
|
+
const next = t[i + 1];
|
|
75
|
+
if (next !== undefined && next !== ' ') continue;
|
|
76
|
+
const after = t[i + 2];
|
|
77
|
+
if (next === ' ' && after !== undefined && !/[A-Z0-9"'(*[]/.test(after)) continue;
|
|
78
|
+
out.push(t.slice(start, i + 1).trim());
|
|
79
|
+
start = i + 2;
|
|
80
|
+
}
|
|
81
|
+
if (start < t.length) out.push(t.slice(start).trim());
|
|
82
|
+
return out.filter((s) => /[A-Za-z0-9]/.test(s));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// checkAsk(raw) → { ok, errors: [{ field, rule, message }], question_words, ask }
|
|
86
|
+
// `ask` is the stored form (trimmed strings, only the known fields) when ok,
|
|
87
|
+
// else null. Every rule that breaks is named, so the agent fixes them all at once.
|
|
88
|
+
function checkAsk(raw) {
|
|
89
|
+
const errors = [];
|
|
90
|
+
const err = (field, rule, message) => errors.push({ field, rule, message });
|
|
91
|
+
const c = obj(raw);
|
|
92
|
+
if (!c) {
|
|
93
|
+
err('ask', 'missing', 'Send the ask as a card: a question, two or three answers, a pick, a seat and what happens if nobody answers.');
|
|
94
|
+
return { ok: false, errors, question_words: 0, ask: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- the question: the prototype's rules ----
|
|
98
|
+
const q = str(c.question).trim();
|
|
99
|
+
const qWords = words(q).length;
|
|
100
|
+
if (!q) err('question', 'missing', 'Ask one question.');
|
|
101
|
+
else {
|
|
102
|
+
if (qWords > ASK_RULES.question_words) err('question', 'too_long', `The question is ${qWords} words; ${ASK_RULES.question_words} at most.`);
|
|
103
|
+
if (!/\?$/.test(q)) err('question', 'not_a_question', 'End the question with a question mark.');
|
|
104
|
+
if (sentences(q).length > 1 || /\n/.test(q)) err('question', 'more_than_one', 'Ask exactly one question.');
|
|
105
|
+
if (MARKUP.test(q)) err('question', 'markup', 'No markup or links in the question.');
|
|
106
|
+
if (qWords <= ASK_RULES.question_words && (q.length > CAPS.question || CONTROL.test(q))) err('question', 'too_long', `The question is ${q.length} characters; ${CAPS.question} at most.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- the answers ----
|
|
110
|
+
const opts = Array.isArray(c.options) ? c.options : [];
|
|
111
|
+
if (opts.length < ASK_RULES.options_min || opts.length > ASK_RULES.options_max) {
|
|
112
|
+
err('options', 'count', `Offer ${ASK_RULES.options_min} or ${ASK_RULES.options_max} tap answers, not ${opts.length}.`);
|
|
113
|
+
}
|
|
114
|
+
const keys = new Set();
|
|
115
|
+
const outOpts = [];
|
|
116
|
+
opts.slice(0, MAX_OPTIONS_READ).forEach((o, i) => {
|
|
117
|
+
const f = `options[${i}]`;
|
|
118
|
+
if (!obj(o)) { err(f, 'missing', 'An answer needs a key, a label and a result.'); return; }
|
|
119
|
+
const k = str(o.key).trim();
|
|
120
|
+
if (!k) err(f, 'no_key', 'An answer needs a key.');
|
|
121
|
+
else if (keys.has(k)) err(f, 'duplicate', `Two answers share the key "${k}".`);
|
|
122
|
+
else if (!KEY.test(k)) err(f, 'bad_key', `The key "${k.slice(0, 30)}" is not one short lower-case word.`);
|
|
123
|
+
keys.add(k);
|
|
124
|
+
const label = str(o.label).trim();
|
|
125
|
+
const lw = words(label).length;
|
|
126
|
+
if (!lw) err(f, 'no_label', 'An answer needs a label to tap.');
|
|
127
|
+
else if (lw > ASK_RULES.label_words) err(f, 'label_too_long', `"${label}" is ${lw} words; a tap label is ${ASK_RULES.label_words} at most.`);
|
|
128
|
+
else if (label.length > CAPS.label || CONTROL.test(label) || /\n/.test(label)) err(f, 'label_too_long', `A tap label is ${CAPS.label} characters at most, on one line.`);
|
|
129
|
+
lineRule(f + '.result', o.result, 'Say in one line what this answer does.');
|
|
130
|
+
let act = null;
|
|
131
|
+
if (o.act != null && o.act !== '') {
|
|
132
|
+
act = str(o.act).trim();
|
|
133
|
+
if (!ACT.test(act)) err(f, 'bad_act', 'An answer\'s act names one act of the matter, like "go ahead" or "record".');
|
|
134
|
+
}
|
|
135
|
+
outOpts.push({ key: k, label, result: str(o.result).trim(), ...(act ? { act } : {}) });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---- the pick, and why ----
|
|
139
|
+
const rec = obj(c.recommend);
|
|
140
|
+
if (!rec) err('recommend', 'missing', 'Recommend an answer and say why.');
|
|
141
|
+
else {
|
|
142
|
+
if (!keys.has(str(rec.key))) err('recommend', 'unknown_key', 'Recommend one of the answers offered.');
|
|
143
|
+
const why = str(rec.why);
|
|
144
|
+
const ww = words(why).length;
|
|
145
|
+
if (!ww) err('recommend', 'no_why', 'Say why in one line.');
|
|
146
|
+
else if (ww > ASK_RULES.why_words || /\n/.test(why)) err('recommend', 'why_too_long', `The why is ${ww} words; ${ASK_RULES.why_words} at most, one line.`);
|
|
147
|
+
else if (why.trim().length > CAPS.why || CONTROL.test(why)) err('recommend', 'why_too_long', `The why is ${CAPS.why} characters at most.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- who it waits on: a seat the direction desk can route ----
|
|
151
|
+
const seat = str(c.waits_on).trim();
|
|
152
|
+
if (!seat) err('waits_on', 'missing', 'Say who this waits on.');
|
|
153
|
+
else if (!SEAT.test(seat)) err('waits_on', 'unknown_seat', `"${seat.slice(0, 40)}" is not a seat: say owner, lead:<building>, committee:<building>.<name> or board:<building>.`);
|
|
154
|
+
|
|
155
|
+
// ---- what happens if nobody answers ----
|
|
156
|
+
lineRule('if_nobody_answers', c.if_nobody_answers, 'Say what happens if nobody answers.');
|
|
157
|
+
|
|
158
|
+
// ---- the long text: it may ride along, never as the ask ----
|
|
159
|
+
const detail = c.detail == null ? '' : (typeof c.detail === 'string' ? c.detail.trim() : null);
|
|
160
|
+
if (detail === null) err('detail', 'not_text', 'The detail is plain text.');
|
|
161
|
+
else if (detail.length > CAPS.detail) err('detail', 'too_long', `The detail is ${detail.length} characters; ${CAPS.detail} at most.`);
|
|
162
|
+
|
|
163
|
+
const ok = errors.length === 0;
|
|
164
|
+
return {
|
|
165
|
+
ok,
|
|
166
|
+
errors,
|
|
167
|
+
question_words: qWords,
|
|
168
|
+
ask: ok ? {
|
|
169
|
+
question: q,
|
|
170
|
+
options: outOpts,
|
|
171
|
+
recommend: { key: str(rec.key), why: str(rec.why).trim() },
|
|
172
|
+
waits_on: seat,
|
|
173
|
+
if_nobody_answers: str(c.if_nobody_answers).trim(),
|
|
174
|
+
detail: detail || null,
|
|
175
|
+
} : null,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function lineRule(field, text, missingMessage) {
|
|
179
|
+
const t = str(text).trim();
|
|
180
|
+
const n = words(t).length;
|
|
181
|
+
if (!n) err(field, 'missing', missingMessage);
|
|
182
|
+
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.`);
|
|
183
|
+
else if (MARKUP.test(t)) err(field, 'markup', 'No markup or links.');
|
|
184
|
+
else if (t.length > CAPS.line || CONTROL.test(t)) err(field, 'not_one_line', `One line of ${CAPS.line} characters at most.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The refusal, in plain words: what was refused and every fix, in one paragraph.
|
|
189
|
+
function refusalLine(errors) {
|
|
190
|
+
const list = Array.isArray(errors) ? errors : [];
|
|
191
|
+
if (!list.length) return '';
|
|
192
|
+
const fixes = list.slice(0, 8).map((e) => String(e.message || '').trim()).filter(Boolean);
|
|
193
|
+
const more = list.length > 8 ? ` And ${list.length - 8} more.` : '';
|
|
194
|
+
return `This ask was refused: a person must be able to answer it at a glance. ${fixes.join(' ')}${more}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// task 1004704 — AGENTS WRITE ASKS. When an AI session files a blocker by hand
|
|
198
|
+
// without a card, the route answers this (400 `ask_required`): what to send, in
|
|
199
|
+
// plain words, and the helper that checks a card before filing.
|
|
200
|
+
const ASK_REQUIRED_MESSAGE = 'An AI session files a blocker with an ask: a question of at most '
|
|
201
|
+
+ `${ASK_RULES.question_words} words, ${ASK_RULES.options_min} or ${ASK_RULES.options_max} tap answers each with a one-line result, `
|
|
202
|
+
+ 'a pick and why, who it waits on, and what happens if nobody answers. Put the long text in detail. '
|
|
203
|
+
+ 'Check the card first with: node scripts/gds/ask.js check <file>';
|
|
204
|
+
|
|
205
|
+
// How a seat reads to a person.
|
|
206
|
+
function seatWords(seat) {
|
|
207
|
+
const s = str(seat);
|
|
208
|
+
if (s === 'owner') return 'the owner';
|
|
209
|
+
const m = /^(lead|board|committee):([a-z]+)(?:\.([a-z0-9_]+))?$/.exec(s);
|
|
210
|
+
if (!m) return s;
|
|
211
|
+
const building = m[2].charAt(0).toUpperCase() + m[2].slice(1);
|
|
212
|
+
if (m[1] === 'lead') return `the ${building} lead`;
|
|
213
|
+
if (m[1] === 'board') return `the ${building} board`;
|
|
214
|
+
return `the ${building} ${String(m[3] || '').replace(/_/g, ' ')} committee`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// cardText(ask) → the card as a person reads it, in plain text: the question,
|
|
218
|
+
// the answers (the pick marked), why, who it waits on, what happens if nobody
|
|
219
|
+
// answers. What an agent prints in chat for anything that is not a stored
|
|
220
|
+
// matter — a "please confirm" at the end of a run, a help request, a review
|
|
221
|
+
// request — so every ask reads the same wherever it lands. `detail` is left out:
|
|
222
|
+
// it rides along, it is never the ask.
|
|
223
|
+
function cardText(ask) {
|
|
224
|
+
const a = obj(ask);
|
|
225
|
+
if (!a) return '';
|
|
226
|
+
const rec = obj(a.recommend) || {};
|
|
227
|
+
const lines = [str(a.question).trim()];
|
|
228
|
+
(Array.isArray(a.options) ? a.options : []).forEach((o, i) => {
|
|
229
|
+
const opt = obj(o) || {};
|
|
230
|
+
const pick = str(opt.key) && str(opt.key) === str(rec.key) ? ' (recommended)' : '';
|
|
231
|
+
lines.push(` ${i + 1}. ${str(opt.label).trim()}${pick}: ${str(opt.result).trim()}`);
|
|
232
|
+
});
|
|
233
|
+
if (str(rec.why).trim()) lines.push(`Why: ${str(rec.why).trim()}`);
|
|
234
|
+
lines.push(`Waits on ${seatWords(a.waits_on)}. If nobody answers: ${str(a.if_nobody_answers).trim()}`);
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A stored row → the ask as every reader gets it (docket matters, blockers).
|
|
239
|
+
function fromRow(row) {
|
|
240
|
+
if (!row) return null;
|
|
241
|
+
return {
|
|
242
|
+
question: row.question,
|
|
243
|
+
options: Array.isArray(row.options) ? row.options : [],
|
|
244
|
+
recommend: { key: row.recommend_key, why: row.recommend_why },
|
|
245
|
+
waits_on: row.waits_on,
|
|
246
|
+
if_nobody_answers: row.if_nobody_answers,
|
|
247
|
+
detail: row.detail || null,
|
|
248
|
+
written_at: row.written_at instanceof Date ? row.written_at.toISOString() : (row.written_at || null),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isMatterId(id) { return typeof id === 'string' && MATTER_ID.test(id); }
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
ASK_RULES,
|
|
256
|
+
CAPS,
|
|
257
|
+
BUILDINGS,
|
|
258
|
+
SEAT,
|
|
259
|
+
MATTER_ID,
|
|
260
|
+
ASK_REQUIRED_MESSAGE,
|
|
261
|
+
checkAsk,
|
|
262
|
+
refusalLine,
|
|
263
|
+
cardText,
|
|
264
|
+
seatWords,
|
|
265
|
+
fromRow,
|
|
266
|
+
isMatterId,
|
|
267
|
+
sentences,
|
|
268
|
+
words,
|
|
269
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/board-idea-reads.js — the board's two reads about a Full
|
|
4
|
+
// Idea: the feedback a returned idea carries back to its author (R13), and
|
|
5
|
+
// whether the board CARRIED it (BV2.R17, task 1004096). Moved out of board.js
|
|
6
|
+
// unchanged (task 1004706) to keep that file under the 1,500-line ratchet;
|
|
7
|
+
// board.js re-exports all three by name, so every caller and the government
|
|
8
|
+
// port read them where they always did. Reads only — db is looked up by property
|
|
9
|
+
// at call time, so a test's monkeypatched db reaches them.
|
|
10
|
+
|
|
11
|
+
const db = require('./db');
|
|
12
|
+
|
|
13
|
+
// ── R13's read: the feedback a returned idea carries back to its author ─────
|
|
14
|
+
//
|
|
15
|
+
// One reasoned objection is what returns an idea (§3), so the feedback IS that
|
|
16
|
+
// vote's reason + flagged section, straight off the most recent returned item.
|
|
17
|
+
// Null when this idea has never been returned by the board — the caller (the
|
|
18
|
+
// ideas resubmit path) falls back to the sub-bar weakest-section flag, which is
|
|
19
|
+
// derivable from the stored field_values at any time (R09). Shape mirrors the
|
|
20
|
+
// vote payload so the return flow renders ONE thing for both rejection sources.
|
|
21
|
+
async function latestRejectionForIdea(ideaId) {
|
|
22
|
+
const id = Number(ideaId);
|
|
23
|
+
if (!Number.isFinite(id) || id <= 0) return null;
|
|
24
|
+
const row = await db.getLatestReturnedItemWithObjection('full_idea', id);
|
|
25
|
+
if (!row) return null;
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
source: 'board',
|
|
28
|
+
item_id: row.item_id,
|
|
29
|
+
section_key: row.section_key || null,
|
|
30
|
+
reason_md: row.reason_md || null,
|
|
31
|
+
closed_at: row.closed_at,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── BV2.R17's read: did the board CARRY this idea? (task 1004096) ───────────
|
|
36
|
+
//
|
|
37
|
+
// The ratification half of the ideator surface turns a Full Idea into a goal,
|
|
38
|
+
// and spec decision 9 is that the ideator RECOMMENDS while the board DECIDES.
|
|
39
|
+
// The ideas module holds the author's recommendation on its own grade row, so
|
|
40
|
+
// without this read "the board decides" would be a sentence in a comment: the
|
|
41
|
+
// only fact stopping a ratification from being one person's opinion is that a
|
|
42
|
+
// sitting actually closed `passed`.
|
|
43
|
+
//
|
|
44
|
+
// Null means "no passing decision on record", which the caller must treat as a
|
|
45
|
+
// refusal and never as an absence it may work around. A returned item, an item
|
|
46
|
+
// still open, and an idea that never reached the board all read the same here,
|
|
47
|
+
// on purpose — none of them is a mandate, and distinguishing them would invite a
|
|
48
|
+
// caller to accept one of them.
|
|
49
|
+
//
|
|
50
|
+
// Frozen, like latestRejectionForIdea, so a caller cannot decorate the record of
|
|
51
|
+
// a decision and pass it on as though the board had said more than it did.
|
|
52
|
+
async function boardPassForIdea(ideaId) {
|
|
53
|
+
const id = Number(ideaId);
|
|
54
|
+
if (!Number.isFinite(id) || id <= 0) return null;
|
|
55
|
+
const row = await db.getLatestPassedItem('full_idea', id);
|
|
56
|
+
if (!row) return null;
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
item_id: row.item_id,
|
|
59
|
+
outcome: 'passed',
|
|
60
|
+
opened_at: row.opened_at,
|
|
61
|
+
closed_at: row.closed_at,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// boardPassForIdea for a list, in one read (task 1004560).
|
|
66
|
+
const boardPassedIdeaIds = async (ids) => new Set(await db.listPassedSubjectIds('full_idea', ids));
|
|
67
|
+
|
|
68
|
+
module.exports = { latestRejectionForIdea, boardPassForIdea, boardPassedIdeaIds };
|
|
@@ -63,6 +63,7 @@ const charter = require('./charter');
|
|
|
63
63
|
const recommendations = require('./recommendations');
|
|
64
64
|
const decisionRules = require('./decision-rules');
|
|
65
65
|
const decisionBoard = require('./decision-board');
|
|
66
|
+
const lawBoard = require('./law-board');
|
|
66
67
|
|
|
67
68
|
// ADR 0175 §5: the completeness bar a Full Idea must clear to reach the board.
|
|
68
69
|
// Held equal to economy's IDEA_FULL_PASS_BAR by test — see the header.
|
|
@@ -631,11 +632,14 @@ async function closeItem(item, { cause = 'vote' } = {}) {
|
|
|
631
632
|
: null;
|
|
632
633
|
const rulesAmendment = !!amendment && amendment.kind === 'decision_rules';
|
|
633
634
|
let appliedDecisionRules = null;
|
|
635
|
+
// task 1004706: a laws amendment is written the same way — inside the close.
|
|
636
|
+
const lawsAmendment = !!amendment && amendment.kind === 'laws';
|
|
637
|
+
let appliedLaws = null;
|
|
634
638
|
|
|
635
639
|
const closed = await db.closeBoardItem({
|
|
636
640
|
itemId: item.id,
|
|
637
641
|
outcome,
|
|
638
|
-
afterClose: (payMoney || rulesAmendment)
|
|
642
|
+
afterClose: (payMoney || rulesAmendment || lawsAmendment)
|
|
639
643
|
? async (client, closedRow) => {
|
|
640
644
|
if (payMoney) {
|
|
641
645
|
credit = await reward.awardIdeaGovernancePassCredit(client, {
|
|
@@ -645,6 +649,7 @@ async function closeItem(item, { cause = 'vote' } = {}) {
|
|
|
645
649
|
});
|
|
646
650
|
}
|
|
647
651
|
if (rulesAmendment) appliedDecisionRules = await decisionBoard.applyPassedDecisionRules(client, closedRow, amendment);
|
|
652
|
+
if (lawsAmendment) appliedLaws = await lawBoard.applyPassedLaws(client, closedRow, amendment);
|
|
648
653
|
}
|
|
649
654
|
: null,
|
|
650
655
|
});
|
|
@@ -664,7 +669,8 @@ async function closeItem(item, { cause = 'vote' } = {}) {
|
|
|
664
669
|
// failure, never silently absorbed. The apply is re-drivable from the passed
|
|
665
670
|
// item + amendment rows. A returned amendment applies nothing.
|
|
666
671
|
let appliedConstitution = null;
|
|
667
|
-
|
|
672
|
+
// A rules or laws amendment changes its own table, never the board block.
|
|
673
|
+
if (outcome === 'passed' && closed.subject_type === 'constitutional_amendment' && !rulesAmendment && !lawsAmendment) {
|
|
668
674
|
try {
|
|
669
675
|
if (amendment) {
|
|
670
676
|
appliedConstitution = governmentConfig.applyBoardAmendment(amendment.proposed);
|
|
@@ -708,6 +714,7 @@ async function closeItem(item, { cause = 'vote' } = {}) {
|
|
|
708
714
|
...(credit ? { credit } : {}),
|
|
709
715
|
...(appliedConstitution ? { applied_constitution: appliedConstitution } : {}),
|
|
710
716
|
...(appliedDecisionRules ? { applied_decision_rules: appliedDecisionRules } : {}),
|
|
717
|
+
...(appliedLaws ? { applied_laws: appliedLaws } : {}),
|
|
711
718
|
...(acted ? { acted: acted.result } : {}),
|
|
712
719
|
};
|
|
713
720
|
}
|
|
@@ -907,6 +914,10 @@ const withOpenNudge = (fn) => async (args) => {
|
|
|
907
914
|
};
|
|
908
915
|
const proposeDecisionRules = withOpenNudge((a) => decisionBoard.proposeDecisionRules(a));
|
|
909
916
|
const openMatter = withOpenNudge((a) => decisionBoard.openMatter(a));
|
|
917
|
+
// task 1004711: a Lead's build-out, built at once or filed as a vote (decision-board.js buildOut)
|
|
918
|
+
const buildOut = withOpenNudge((a) => decisionBoard.buildOut(a));
|
|
919
|
+
// task 1004706: the project's laws — an amendment of kind 'laws' (law-board.js).
|
|
920
|
+
const proposeLaws = withOpenNudge((a) => lawBoard.proposeLaws(a));
|
|
910
921
|
|
|
911
922
|
// ── the board's third subject: closing a genesis stage (task 1004429) ───────
|
|
912
923
|
//
|
|
@@ -1293,7 +1304,8 @@ async function constitutionView({ historyLimit = 50, type = null, headcount: ask
|
|
|
1293
1304
|
// there is a missed warning rather than a wrong one.
|
|
1294
1305
|
// Only board-block amendments say what the board block should be (a rules
|
|
1295
1306
|
// amendment changes the table, task 1004529).
|
|
1296
|
-
|
|
1307
|
+
// Nor do laws amendments (task 1004706): they change the law book.
|
|
1308
|
+
divergence: constitutionDivergence(live, history.filter((h) => h.kind !== 'decision_rules' && h.kind !== 'laws')),
|
|
1297
1309
|
// Who decides what at each building, as today or set by a passed amendment.
|
|
1298
1310
|
decision_rules: decisionBoard.constitutionBlock(rulesTable),
|
|
1299
1311
|
// The genesis charter + legislation stages' payload (task 1004427): every
|
|
@@ -1318,60 +1330,8 @@ async function constitutionView({ historyLimit = 50, type = null, headcount: ask
|
|
|
1318
1330
|
};
|
|
1319
1331
|
}
|
|
1320
1332
|
|
|
1321
|
-
// ── R13's
|
|
1322
|
-
|
|
1323
|
-
// One reasoned objection is what returns an idea (§3), so the feedback IS that
|
|
1324
|
-
// vote's reason + flagged section, straight off the most recent returned item.
|
|
1325
|
-
// Null when this idea has never been returned by the board — the caller (the
|
|
1326
|
-
// ideas resubmit path) falls back to the sub-bar weakest-section flag, which is
|
|
1327
|
-
// derivable from the stored field_values at any time (R09). Shape mirrors the
|
|
1328
|
-
// vote payload so the return flow renders ONE thing for both rejection sources.
|
|
1329
|
-
async function latestRejectionForIdea(ideaId) {
|
|
1330
|
-
const id = Number(ideaId);
|
|
1331
|
-
if (!Number.isFinite(id) || id <= 0) return null;
|
|
1332
|
-
const row = await db.getLatestReturnedItemWithObjection('full_idea', id);
|
|
1333
|
-
if (!row) return null;
|
|
1334
|
-
return Object.freeze({
|
|
1335
|
-
source: 'board',
|
|
1336
|
-
item_id: row.item_id,
|
|
1337
|
-
section_key: row.section_key || null,
|
|
1338
|
-
reason_md: row.reason_md || null,
|
|
1339
|
-
closed_at: row.closed_at,
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
// ── BV2.R17's read: did the board CARRY this idea? (task 1004096) ───────────
|
|
1344
|
-
//
|
|
1345
|
-
// The ratification half of the ideator surface turns a Full Idea into a goal,
|
|
1346
|
-
// and spec decision 9 is that the ideator RECOMMENDS while the board DECIDES.
|
|
1347
|
-
// The ideas module holds the author's recommendation on its own grade row, so
|
|
1348
|
-
// without this read "the board decides" would be a sentence in a comment: the
|
|
1349
|
-
// only fact stopping a ratification from being one person's opinion is that a
|
|
1350
|
-
// sitting actually closed `passed`.
|
|
1351
|
-
//
|
|
1352
|
-
// Null means "no passing decision on record", which the caller must treat as a
|
|
1353
|
-
// refusal and never as an absence it may work around. A returned item, an item
|
|
1354
|
-
// still open, and an idea that never reached the board all read the same here,
|
|
1355
|
-
// on purpose — none of them is a mandate, and distinguishing them would invite a
|
|
1356
|
-
// caller to accept one of them.
|
|
1357
|
-
//
|
|
1358
|
-
// Frozen, like latestRejectionForIdea, so a caller cannot decorate the record of
|
|
1359
|
-
// a decision and pass it on as though the board had said more than it did.
|
|
1360
|
-
async function boardPassForIdea(ideaId) {
|
|
1361
|
-
const id = Number(ideaId);
|
|
1362
|
-
if (!Number.isFinite(id) || id <= 0) return null;
|
|
1363
|
-
const row = await db.getLatestPassedItem('full_idea', id);
|
|
1364
|
-
if (!row) return null;
|
|
1365
|
-
return Object.freeze({
|
|
1366
|
-
item_id: row.item_id,
|
|
1367
|
-
outcome: 'passed',
|
|
1368
|
-
opened_at: row.opened_at,
|
|
1369
|
-
closed_at: row.closed_at,
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
// boardPassForIdea for a list, in one read (task 1004560).
|
|
1374
|
-
const boardPassedIdeaIds = async (ids) => new Set(await db.listPassedSubjectIds('full_idea', ids));
|
|
1333
|
+
// ── R13's and BV2.R17's reads about a Full Idea: board-idea-reads.js ───────
|
|
1334
|
+
const { latestRejectionForIdea, boardPassForIdea, boardPassedIdeaIds } = require('./board-idea-reads');
|
|
1375
1335
|
|
|
1376
1336
|
// ── the summons: what is waiting on THIS member's vote (task 1003271) ───────
|
|
1377
1337
|
//
|
|
@@ -1456,6 +1416,10 @@ async function pendingVotesFor(builderId) {
|
|
|
1456
1416
|
module.exports = {
|
|
1457
1417
|
proposeDecisionRules,
|
|
1458
1418
|
openMatter,
|
|
1419
|
+
buildOut,
|
|
1420
|
+
proposeLaws,
|
|
1421
|
+
lawsView: (...a) => lawBoard.lawsView(...a),
|
|
1422
|
+
leadsView: (...a) => decisionBoard.leadsView(...a),
|
|
1459
1423
|
decisionRulesView: (...a) => decisionBoard.decisionRulesView(...a),
|
|
1460
1424
|
decisionRulesWriteAllowed: decisionBoard.decisionRulesWriteAllowed,
|
|
1461
1425
|
applyPassedDecisionRules: decisionBoard.applyPassedDecisionRules,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/build-rights.js — WHO MAY BUILD WITHOUT A VOTE: whether the
|
|
4
|
+
// holder of a building (its Lead, or the owner when it has none) may change the
|
|
5
|
+
// building's structure at once, or must file it as a vote (task 1004710, goal
|
|
6
|
+
// 1000111; ADR 0375).
|
|
7
|
+
//
|
|
8
|
+
// THE OWNER'S RULING (2026-10-09). It comes from the FORM OF GOVERNMENT, and in
|
|
9
|
+
// future will often be shaped by goals. It is NOT a per-Lead flag: a Lead's
|
|
10
|
+
// charter is one line of words (leads.js), and the prototype's
|
|
11
|
+
// `can_build_without_vote` is not stored anywhere.
|
|
12
|
+
//
|
|
13
|
+
// WHAT IS BUILT. A build-out is one of four capsules (the hall's office storey):
|
|
14
|
+
// a committee room, a new floor, an annex, or growing into a district. This file
|
|
15
|
+
// answers, per building and per capsule, { without_vote, why, ground }. Carrying a
|
|
16
|
+
// build-out out is task 1004711; this is the rule it will ask.
|
|
17
|
+
//
|
|
18
|
+
// THE RULE, in the order it is applied (first match decides):
|
|
19
|
+
// 1. A DISTRICT always goes to a vote: it is a new part of the city, not a
|
|
20
|
+
// change inside one building. ground 'city'
|
|
21
|
+
// 2. A VACANT building (its row says no one holds it) builds nothing. ground 'building'
|
|
22
|
+
// 3. A GOAL that LIMITS the building: a vote. ground 'goal'
|
|
23
|
+
// 4. A building handed to a SITTING of its own (decision-rules.js): under a
|
|
24
|
+
// rule that lets a seated member act alone ('decide', 'first') the holder
|
|
25
|
+
// builds at once if that sitting seats them; under any other rule
|
|
26
|
+
// (consent, majority, unanimous, sign-off) its sitting votes. ground 'building'
|
|
27
|
+
// 5. A GOAL that GRANTS the building: at once. ground 'goal'
|
|
28
|
+
// 6. Otherwise the FORM OF GOVERNMENT answers (FORM_ANSWERS): ground 'form'
|
|
29
|
+
// Monarchy / BDFL at once — the founder decides alone, and whoever
|
|
30
|
+
// holds a building acts for them in it.
|
|
31
|
+
// Timocratic Republic at once when the holder has earned a vote (sits
|
|
32
|
+
// on the board); otherwise a vote.
|
|
33
|
+
// Direct Democracy a vote — every builder decides changes together.
|
|
34
|
+
// Foundation a vote — the council of trustees decides.
|
|
35
|
+
// Custom a vote — settings that match no named form grant
|
|
36
|
+
// nothing beyond what they say.
|
|
37
|
+
// A form that is not available yet (Presidential, Cooperative, Federation)
|
|
38
|
+
// can never be in force (charter.js refuses it), so it has no answer here.
|
|
39
|
+
//
|
|
40
|
+
// THE GOALS HOOK. `goals` is a list of terms a goal sets on a building:
|
|
41
|
+
// { goal_id, building: <key> | '*', capsules?: [<capsule>...], allow: bool, why? }
|
|
42
|
+
// A limit (allow false) beats the building's own sitting and the form; a grant
|
|
43
|
+
// (allow true) beats the form only — never a district, a vacant building, or a
|
|
44
|
+
// sitting the project voted in. No goal stores terms yet, so the server passes
|
|
45
|
+
// an empty list today; the hook is here so a goal can grant or limit later
|
|
46
|
+
// without a second rule.
|
|
47
|
+
//
|
|
48
|
+
// FAIL CLOSED. Anything this file cannot read — an unknown form, a missing
|
|
49
|
+
// decision row, a holder nobody can name — answers "a vote", never "at once".
|
|
50
|
+
//
|
|
51
|
+
// PURE: no db. The caller resolves, per request and uncached (ADR 0016), the
|
|
52
|
+
// constitution in force, each building's decision row, and who sits on the
|
|
53
|
+
// board and on each sitting, and hands them in. Requires only charter.js (to
|
|
54
|
+
// name the form from the settings) and decision-rules.js (its rules).
|
|
55
|
+
|
|
56
|
+
const { readCharter } = require('./charter');
|
|
57
|
+
const { AREA_KEYS, DECISION_RULES } = require('./decision-rules');
|
|
58
|
+
|
|
59
|
+
// The four build-outs, smallest first; held equal to the hall's CAPSULES
|
|
60
|
+
// (modules/hall-ui/public/street-office.js) by tests/government_build_rights.mjs.
|
|
61
|
+
const CAPSULES = Object.freeze(['committee', 'floor', 'annex', 'district']);
|
|
62
|
+
|
|
63
|
+
// The documented answer of each form (the table above, as data). `holder`:
|
|
64
|
+
// 'any' builds at once, 'board' builds at once only when the holder sits on the
|
|
65
|
+
// board, 'none' always goes to a vote.
|
|
66
|
+
const FORM_ANSWERS = Object.freeze({
|
|
67
|
+
monarchy: Object.freeze({ holder: 'any', yes: 'The founder decides alone, so whoever holds this building builds it out at once.' }),
|
|
68
|
+
'timocratic-republic': Object.freeze({
|
|
69
|
+
holder: 'board',
|
|
70
|
+
yes: 'Its holder has earned a vote, so they build it out at once.',
|
|
71
|
+
no: 'Its holder has not earned a vote yet, so the board votes on it.',
|
|
72
|
+
}),
|
|
73
|
+
'direct-democracy': Object.freeze({ holder: 'none', no: 'Every builder decides changes together, so it goes to a vote.' }),
|
|
74
|
+
foundation: Object.freeze({ holder: 'none', no: 'The council of trustees decides changes, so it goes to a vote.' }),
|
|
75
|
+
custom: Object.freeze({ holder: 'none', no: 'These settings match no named form, so a change goes to a vote.' }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const ACTS_ALONE = Object.freeze(DECISION_RULES.filter((r) => r.actsAlone).map((r) => r.key));
|
|
79
|
+
|
|
80
|
+
const answer = (withoutVote, why, by) => Object.freeze({ without_vote: !!withoutVote, why, ground: by });
|
|
81
|
+
|
|
82
|
+
// A goal's term matches this building and capsule.
|
|
83
|
+
function termMatches(t, building, capsule) {
|
|
84
|
+
if (!t || typeof t !== 'object') return false;
|
|
85
|
+
if (t.building !== '*' && t.building !== building) return false;
|
|
86
|
+
if (Array.isArray(t.capsules) && t.capsules.length && !t.capsules.includes(capsule)) return false;
|
|
87
|
+
return typeof t.allow === 'boolean';
|
|
88
|
+
}
|
|
89
|
+
const goalWhy = (t, fallback) => (typeof t.why === 'string' && t.why.trim() ? t.why.trim() : fallback);
|
|
90
|
+
|
|
91
|
+
// The answer for one capsule at one building.
|
|
92
|
+
// form — readCharter(board).form ('monarchy', …, 'custom')
|
|
93
|
+
// decision — the building's resolved row (decision-rules.resolveRow), or null
|
|
94
|
+
// holder — { seat, on_board: bool, in_sitting: bool }: who holds it, and
|
|
95
|
+
// whether the board / the building's own sitting seats them
|
|
96
|
+
// goals — the goals hook (see above); [] today
|
|
97
|
+
function buildRight({ building, capsule, form, decision, holder, goals = [] } = {}) {
|
|
98
|
+
if (!AREA_KEYS.includes(building) || !CAPSULES.includes(capsule)) return answer(false, 'This cannot be built.', 'city');
|
|
99
|
+
if (capsule === 'district') return answer(false, 'A district is a new part of the city, so the board votes on it.', 'city');
|
|
100
|
+
if (!decision) return answer(false, 'How this building decides could not be read, so it goes to a vote.', 'building');
|
|
101
|
+
if (decision.rule === 'vacant') return answer(false, 'No one holds this building yet, so nothing is built here.', 'building');
|
|
102
|
+
|
|
103
|
+
const terms = Array.isArray(goals) ? goals.filter((t) => termMatches(t, building, capsule)) : [];
|
|
104
|
+
const limit = terms.find((t) => t.allow === false);
|
|
105
|
+
if (limit) return answer(false, goalWhy(limit, 'A goal limits building here, so it goes to a vote.'), 'goal');
|
|
106
|
+
|
|
107
|
+
const h = holder || {};
|
|
108
|
+
if (decision.sitting) {
|
|
109
|
+
const by = `${decision.name} is decided by ${decision.who}`;
|
|
110
|
+
if (ACTS_ALONE.includes(decision.rule)) {
|
|
111
|
+
return h.in_sitting
|
|
112
|
+
? answer(true, `${by}, and its holder is one of them, so they build it out at once.`, 'building')
|
|
113
|
+
: answer(false, `${by}, and its holder is not one of them, so it goes to them.`, 'building');
|
|
114
|
+
}
|
|
115
|
+
return answer(false, `${by} (${decision.rule_words}), so it goes to them as a vote.`, 'building');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const grant = terms.find((t) => t.allow === true);
|
|
119
|
+
if (grant) return answer(true, goalWhy(grant, 'A goal lets its holder build here at once.'), 'goal');
|
|
120
|
+
|
|
121
|
+
const f = FORM_ANSWERS[form] || FORM_ANSWERS.custom;
|
|
122
|
+
if (f.holder === 'any') return answer(true, f.yes, 'form');
|
|
123
|
+
if (f.holder === 'board' && h.on_board) return answer(true, f.yes, 'form');
|
|
124
|
+
return answer(false, f.no, 'form');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Every capsule at one building: { form, form_name, holder, capsules: { committee: {…}, … } }.
|
|
128
|
+
// `board` is the constitution's board block in force.
|
|
129
|
+
function buildRightsFor({ building, board, decision, holder, goals = [] } = {}) {
|
|
130
|
+
const read = readCharter(board);
|
|
131
|
+
const capsules = {};
|
|
132
|
+
for (const capsule of CAPSULES) {
|
|
133
|
+
capsules[capsule] = buildRight({ building, capsule, form: read.form, decision, holder, goals });
|
|
134
|
+
}
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
form: read.form,
|
|
137
|
+
form_name: read.name,
|
|
138
|
+
holder: holder && holder.seat ? holder.seat : null,
|
|
139
|
+
capsules: Object.freeze(capsules),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
CAPSULES,
|
|
145
|
+
FORM_ANSWERS,
|
|
146
|
+
buildRight,
|
|
147
|
+
buildRightsFor,
|
|
148
|
+
};
|