@bongos/core 1.21.112 → 1.21.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +616 -186
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/bongos-feedback/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -0
- package/GOVERNMENT.md +128 -11
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +28 -0
- package/clients/bongos-client/index.cjs +28 -0
- package/clients/bongos-client/index.d.ts +45 -2
- package/clients/bongos-client/index.mjs +28 -0
- package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
- package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
- package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
- package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
- package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
- package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
- package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
- package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
- package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +989 -87
- package/docs/api-reference.md +18 -4
- package/docs/copy-inventory.md +343 -262
- package/docs/copy-registry.json +1223 -426
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/engineer.md +2 -1
- package/docs/page-inventory.json +91 -6
- package/docs/page-readings.json +5017 -4458
- package/modules/builder-settings/law-locks.js +202 -0
- package/modules/builder-settings/routes/settings.js +66 -6
- package/modules/builder-settings/settings.js +7 -0
- package/modules/discord/ship-broadcast.js +22 -13
- package/modules/government/asks-db.js +54 -0
- package/modules/government/asks.js +269 -0
- package/modules/government/board-idea-reads.js +68 -0
- package/modules/government/board.js +21 -57
- package/modules/government/build-rights.js +148 -0
- package/modules/government/buildouts.js +160 -0
- package/modules/government/counts.js +81 -0
- package/modules/government/db.js +242 -0
- package/modules/government/decision-board.js +234 -8
- package/modules/government/decision-rules.js +2 -0
- package/modules/government/docket.js +114 -6
- package/modules/government/law-board.js +131 -0
- package/modules/government/laws.js +241 -0
- package/modules/government/leads.js +170 -0
- package/modules/government/migrations/government_027_laws.sql +143 -0
- package/modules/government/migrations/government_028_asks.sql +57 -0
- package/modules/government/migrations/government_029_leads.sql +135 -0
- package/modules/government/migrations/government_030_buildouts.sql +111 -0
- package/modules/government/migrations/government_031_treasury.sql +107 -0
- package/modules/government/module.json +2 -1
- package/modules/government/pollers/treasury-borrow.js +62 -0
- package/modules/government/positions.js +107 -0
- package/modules/government/presets.js +149 -0
- package/modules/government/routes/government.js +425 -2
- package/modules/government/routing.js +154 -0
- package/modules/government/treasury-db.js +166 -0
- package/modules/government/treasury.js +241 -0
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/board-room.js +2 -0
- package/modules/hall-ui/public/city-draw.js +604 -11
- package/modules/hall-ui/public/city.css +157 -1
- package/modules/hall-ui/public/city.html +37 -7
- package/modules/hall-ui/public/city.js +441 -21
- package/modules/hall-ui/public/city.states.json +110 -5
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +112 -29
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagram-viewer.css +12 -0
- package/modules/hall-ui/public/diagram-viewer.js +4 -0
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/gov-feeds.js +141 -0
- package/modules/hall-ui/public/gov-living.js +1002 -0
- package/modules/hall-ui/public/gov-positions.js +97 -0
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-render.js +3 -0
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.css +334 -0
- package/modules/hall-ui/public/my-agents.html +72 -0
- package/modules/hall-ui/public/my-agents.js +603 -0
- package/modules/hall-ui/public/my-agents.states.json +37 -0
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile-connections.js +40 -28
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/room.css +8 -0
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +116 -16
- package/modules/hall-ui/public/street-card.js +18 -1
- package/modules/hall-ui/public/street-deck.css +299 -0
- package/modules/hall-ui/public/street-deck.js +1145 -0
- package/modules/hall-ui/public/street-depth.js +712 -0
- package/modules/hall-ui/public/street-desk.js +5 -2
- package/modules/hall-ui/public/street-draw.js +109 -16
- package/modules/hall-ui/public/street-inside.js +137 -22
- package/modules/hall-ui/public/street-office.js +705 -0
- package/modules/hall-ui/public/street.css +161 -11
- package/modules/hall-ui/public/street.html +41 -11
- package/modules/hall-ui/public/street.js +78 -66
- package/modules/hall-ui/public/street.states.json +129 -21
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/style.css +5 -1
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/watch.js +14 -1
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/living-city.md +57 -0
- package/modules/hall-ui/records/my-agents.md +21 -0
- package/modules/hall-ui/records/oversight-family.md +1 -0
- package/modules/hall-ui/records/profile-connections.md +10 -0
- package/modules/hall-ui/records/reading-rooms.md +2 -0
- package/modules/ideas/routes/blockers.js +92 -3
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/moving.js +17 -1
- package/modules/lifecycle/routes/moving.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +23 -0
- package/modules/onboarding/approval-broadcast.js +8 -0
- package/modules/onboarding/routes/access-requests.js +16 -6
- package/modules/platform-identity/connections.js +13 -2
- package/modules/platform-identity/routes/connections.js +7 -1
- package/modules/public-landing/public/account-private.states.json +2 -2
- package/modules/public-landing/public/account.html +65 -5
- package/modules/public-landing/public/account.states.json +3 -3
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
- package/modules/public-landing/public/assets/world.css +4 -4
- package/modules/public-landing/public/connections-empty.states.json +14 -0
- package/modules/public-landing/public/connections-inactive.states.json +14 -0
- package/modules/public-landing/public/connections.html +450 -0
- package/modules/public-landing/public/connections.probes.json +414 -0
- package/modules/public-landing/public/connections.states.json +17 -0
- package/modules/public-landing/public/contact.html +2 -2
- package/modules/public-landing/public/guild.html +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/privacy.html +2 -2
- package/modules/public-landing/public/projects.html +2 -2
- package/modules/public-landing/public/projects.probes.json +6 -2
- package/modules/public-landing/public/terms.html +2 -2
- package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
- package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
- package/modules/ui-design/kit/fixtures/connections.json +22 -0
- package/modules/ui-design/kit/serve.js +95 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +128 -0
- package/scripts/discord/README.md +8 -5
- package/scripts/discord/post.js +12 -1
- package/scripts/gds/artifact-audit.js +20 -3
- package/scripts/gds/ask.js +164 -0
- package/scripts/gds/autobongos-run.js +20 -5
- package/scripts/gds/autobongos-tweak-lane.js +262 -0
- package/scripts/gds/autobongos-verify.js +21 -2
- package/scripts/gds/claim.js +2 -0
- package/scripts/gds/government-walk.js +638 -4
- package/scripts/gds/pulse.js +9 -6
- package/scripts/gds/ship-escalation.js +22 -3
- package/scripts/gds/ship-finish.js +5 -4
- package/scripts/gds/ship-merge-gate.js +18 -6
- package/scripts/gds/tweak-halt.js +1 -0
- package/scripts/hall-preview/README.md +41 -1
- package/scripts/hall-preview/pref-writes.js +140 -0
- package/scripts/hall-preview/refresh-fixtures.js +101 -7
- package/scripts/hall-preview/scrub-identity.js +366 -7
- package/scripts/hall-preview/server.js +27 -1
- package/scripts/render-diagrams.sh +4 -2
- package/src/bongos/auth-idp.js +6 -1
- package/src/bongos/project-settings.js +41 -0
- package/src/bongos/route-rank-check.js +27 -0
- package/src/bongos/routes/me.js +68 -7
- package/src/bongos/serve-internal.js +5 -1
- package/src/module-api.js +7 -1
- package/src/platform-server.js +10 -0
- package/tests/application_lifecycle.mjs +47 -5
- package/tests/autobongos_tweak_lane.mjs +208 -0
- package/tests/conductor_main_e2e.mjs +21 -0
- package/tests/connections_api.mjs +18 -0
- package/tests/craft_mode_nav.mjs +5 -3
- package/tests/craft_mode_tallies.mjs +52 -16
- package/tests/discord_approvals.mjs +11 -0
- package/tests/discord_channels.mjs +20 -0
- package/tests/fixtures/gov-seated.mjs +64 -0
- package/tests/fixtures/government-ask-cards.json +1284 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/government_agent_asks.mjs +322 -0
- package/tests/government_asks.mjs +408 -0
- package/tests/government_build_rights.mjs +253 -0
- package/tests/government_buildouts.mjs +382 -0
- package/tests/government_counts.mjs +143 -0
- package/tests/government_decision_rules.mjs +5 -1
- package/tests/government_laws.mjs +488 -0
- package/tests/government_laws_session.mjs +321 -0
- package/tests/government_leads.mjs +375 -0
- package/tests/government_positions.mjs +127 -0
- package/tests/government_presets.mjs +215 -0
- package/tests/government_routes.mjs +8 -1
- package/tests/government_routing.mjs +118 -0
- package/tests/government_seed.mjs +5 -0
- package/tests/government_treasury.mjs +394 -0
- package/tests/governor_docket.mjs +26 -2
- package/tests/hall_buildouts.mjs +208 -0
- package/tests/hall_city.mjs +14 -6
- package/tests/hall_city_living.mjs +617 -0
- package/tests/hall_gov_counts.mjs +336 -0
- package/tests/hall_gov_living.mjs +636 -0
- package/tests/hall_gov_real_site.mjs +655 -0
- package/tests/hall_matter_card.mjs +6 -1
- package/tests/hall_my_agents.mjs +481 -0
- package/tests/hall_nav.mjs +107 -4
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/hall_preview_governor_fixtures.mjs +30 -3
- package/tests/hall_preview_scrub.mjs +59 -1
- package/tests/hall_preview_security.mjs +169 -0
- package/tests/hall_reading_world.mjs +44 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_street.mjs +18 -2
- package/tests/hall_street_amend.mjs +1 -1
- package/tests/hall_street_deck.mjs +609 -0
- package/tests/hall_street_depth.mjs +559 -0
- package/tests/hall_street_desk.mjs +1 -1
- package/tests/hall_street_inside.mjs +29 -5
- package/tests/hall_tweak_editor.mjs +4 -1
- package/tests/hub_account_page.mjs +94 -3
- package/tests/hub_connections_page.mjs +507 -0
- package/tests/hub_invite_badge.mjs +1 -1
- package/tests/idp_device_exchange.mjs +18 -0
- package/tests/landing_legal_pages.mjs +8 -5
- package/tests/landing_page.mjs +23 -4
- package/tests/nav_permission_atoms.mjs +4 -1
- package/tests/profile_connections_ui.mjs +88 -25
- package/tests/projects_hub.mjs +15 -0
- package/tests/render_check.mjs +2 -2
- package/tests/ship_broadcast_routing.mjs +57 -16
- package/tests/ship_broadcast_server.mjs +40 -7
- package/tests/ship_merge_gate.mjs +19 -0
- package/tests/ui_design_kit.mjs +2 -2
- package/tests/watch_applications_queue.mjs +23 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/builder-settings/law-locks.js — a project LAW's lock on a builder's
|
|
4
|
+
// personal setting, ENFORCED (task 1004707, goal 1000111 "government is how a
|
|
5
|
+
// project steers its AI").
|
|
6
|
+
//
|
|
7
|
+
// A law (modules/government/laws.js) may lock personal settings it overrides: a
|
|
8
|
+
// lock names a setting and either FIXES its value ({ setting, value }) or CAPS it
|
|
9
|
+
// ({ setting, max }). This file is where such a lock bites. It is enforced in two
|
|
10
|
+
// places, and the read is the one that cannot be dodged:
|
|
11
|
+
//
|
|
12
|
+
// 1. ON EVERY READ of the effective value. lockedPrefs() rewrites the builder's
|
|
13
|
+
// behavior_prefs so the effective profile obeys every lock, and describe()
|
|
14
|
+
// shapes the /me slices (interaction, wandering, model_allocation) from that.
|
|
15
|
+
// So a value stored BEFORE the law passed, or one written while the laws
|
|
16
|
+
// could not be read, still resolves to what the law says.
|
|
17
|
+
// 2. ON EVERY WRITE. checkPatch() refuses a PATCH that would set a locked
|
|
18
|
+
// setting to anything the law does not allow (409 locked_by_law), so the
|
|
19
|
+
// settings page cannot quietly store a choice that will never take effect.
|
|
20
|
+
// Clearing a setting back to its default (null) is always allowed: the read
|
|
21
|
+
// clamps the default too.
|
|
22
|
+
//
|
|
23
|
+
// WHICH SETTINGS. The personal settings a table really holds, spelled as the
|
|
24
|
+
// hall and laws.js spell them:
|
|
25
|
+
// technical_level, toolchain_familiarity, verbosity, machinery (interaction)
|
|
26
|
+
// wandering (rank-clamped)
|
|
27
|
+
// model.main, model.subagentDefault, model.subagentRoutine (model allocation)
|
|
28
|
+
// A lock naming any other setting (on_stuck, ask_format …) has nothing to bite on
|
|
29
|
+
// yet: it is reported `enforced: false, reason: 'not_a_personal_setting'`, and
|
|
30
|
+
// the law's own words still reach every session (the laws block).
|
|
31
|
+
//
|
|
32
|
+
// ORDER FOR A CAP. `max` reads along each setting's own ordered list: the
|
|
33
|
+
// interaction values in their declared order, wandering's four levels (or the
|
|
34
|
+
// level number), the model tiers cheap→capable. A value past the cap is pulled
|
|
35
|
+
// back to it; a value under it stands.
|
|
36
|
+
//
|
|
37
|
+
// The RANK still has the last word on wandering: a law can fix or cap it inside
|
|
38
|
+
// the rank's ceiling, never lift a builder past it (ADR 0016 — a law is a project
|
|
39
|
+
// rule, not a grant of authority).
|
|
40
|
+
//
|
|
41
|
+
// PURE: no db, no requires beyond the sibling pref libs. The routes and /me hand
|
|
42
|
+
// the laws in (the government port's lawsForSession), read per request.
|
|
43
|
+
|
|
44
|
+
const interactionPrefs = require('./interaction-prefs');
|
|
45
|
+
const wanderingPrefs = require('./wandering-prefs');
|
|
46
|
+
const modelAllocPrefs = require('./model-alloc-prefs');
|
|
47
|
+
|
|
48
|
+
const MODEL_PREFIX = 'model.';
|
|
49
|
+
|
|
50
|
+
// Each lockable setting: its ordered values, how to read its effective value off
|
|
51
|
+
// a prefs blob, and how to write one into a prefs blob (returning a new blob).
|
|
52
|
+
function settingSpec(setting) {
|
|
53
|
+
if (interactionPrefs.FIELD_KEYS.includes(setting)) {
|
|
54
|
+
return {
|
|
55
|
+
route: 'interaction',
|
|
56
|
+
order: Object.keys(interactionPrefs.FIELDS[setting].values),
|
|
57
|
+
read: (prefs) => interactionPrefs.effectiveProfile(prefs)[setting],
|
|
58
|
+
write: (prefs, v) => ({ ...(prefs || {}), [interactionPrefs.RESERVED_KEY]: { ...interactionPrefs.storedProfile(prefs), [setting]: v } }),
|
|
59
|
+
normalize: (v) => v,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (setting === wanderingPrefs.RESERVED_KEY) {
|
|
63
|
+
return {
|
|
64
|
+
route: 'wandering',
|
|
65
|
+
order: wanderingPrefs.LEVELS.slice(),
|
|
66
|
+
read: (prefs, ctx) => wanderingPrefs.levelKey(wanderingPrefs.effectiveLevel(prefs, ctx.rank)),
|
|
67
|
+
write: (prefs, v) => ({ ...(prefs || {}), [wanderingPrefs.RESERVED_KEY]: wanderingPrefs.levelIndex(v) }),
|
|
68
|
+
// a level may be written as its key ('focused') or its number (1)
|
|
69
|
+
normalize: (v) => (wanderingPrefs.isLevelInt(v) ? wanderingPrefs.levelKey(v) : v),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (setting.startsWith(MODEL_PREFIX)) {
|
|
73
|
+
const slot = setting.slice(MODEL_PREFIX.length);
|
|
74
|
+
if (!Object.prototype.hasOwnProperty.call(modelAllocPrefs.SLOTS, slot)) return null;
|
|
75
|
+
return {
|
|
76
|
+
route: 'model_allocation',
|
|
77
|
+
order: modelAllocPrefs.ALLOWED_TIERS.slice(),
|
|
78
|
+
read: (prefs, ctx) => modelAllocPrefs.effectiveAllocation(prefs, ctx.instanceModels)[slot],
|
|
79
|
+
write: (prefs, v) => ({ ...(prefs || {}), [modelAllocPrefs.RESERVED_KEY]: { ...modelAllocPrefs.storedOverride(prefs), [slot]: v } }),
|
|
80
|
+
normalize: (v) => v,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Every lock the laws in force carry, flattened: { setting, value|max, law_id,
|
|
87
|
+
// law_no, law_text }. `laws`: the government port's lawsForSession().laws.
|
|
88
|
+
// laws.js refuses a setting locked by two laws at once, so the first wins here
|
|
89
|
+
// only as a belt.
|
|
90
|
+
function locksOf(laws) {
|
|
91
|
+
const out = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
(Array.isArray(laws) ? laws : []).forEach((law, i) => {
|
|
94
|
+
(law && Array.isArray(law.locks) ? law.locks : []).forEach((l) => {
|
|
95
|
+
if (!l || typeof l.setting !== 'string' || seen.has(l.setting)) return;
|
|
96
|
+
seen.add(l.setting);
|
|
97
|
+
const lock = { setting: l.setting, law_id: law.id == null ? null : String(law.id), law_no: law.no || i + 1, law_text: law.text || '' };
|
|
98
|
+
if (Object.prototype.hasOwnProperty.call(l, 'value')) lock.value = l.value;
|
|
99
|
+
else lock.max = l.max;
|
|
100
|
+
out.push(lock);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// What a lock allows for a value: the fixed value, or the cap applied.
|
|
107
|
+
// Returns { ok: true, value } or { ok: false, reason }.
|
|
108
|
+
function lockTarget(spec, lock, current) {
|
|
109
|
+
if (Object.prototype.hasOwnProperty.call(lock, 'value')) {
|
|
110
|
+
const v = spec.normalize(lock.value);
|
|
111
|
+
return spec.order.includes(v) ? { ok: true, value: v } : { ok: false, reason: 'value_not_allowed' };
|
|
112
|
+
}
|
|
113
|
+
const cap = spec.normalize(lock.max);
|
|
114
|
+
const j = spec.order.indexOf(cap);
|
|
115
|
+
if (j < 0) return { ok: false, reason: 'value_not_allowed' };
|
|
116
|
+
const i = spec.order.indexOf(current);
|
|
117
|
+
return { ok: true, value: i > j ? cap : current };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Every value a lock lets a builder choose, in the setting's own order: the one
|
|
121
|
+
// fixed value, or every value up to and including the cap (task 1004714 — so a
|
|
122
|
+
// surface can offer the values under a cap instead of locking the whole row).
|
|
123
|
+
function allowedFor(spec, lock) {
|
|
124
|
+
if (Object.prototype.hasOwnProperty.call(lock, 'value')) {
|
|
125
|
+
const v = spec.normalize(lock.value);
|
|
126
|
+
return spec.order.includes(v) ? [v] : null;
|
|
127
|
+
}
|
|
128
|
+
const j = spec.order.indexOf(spec.normalize(lock.max));
|
|
129
|
+
return j < 0 ? null : spec.order.slice(0, j + 1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The builder's prefs with every lock applied, and what each lock did.
|
|
133
|
+
// ctx: { rank, instanceModels }
|
|
134
|
+
// Returns { prefs, locks: [{ setting, value|max, law_id, law_no, law_text,
|
|
135
|
+
// enforced, reason?, route?, from?, to?, allowed? }] } — `from` is what the
|
|
136
|
+
// builder's own setting resolves to without the laws, `to` what it resolves to
|
|
137
|
+
// under them, `allowed` every value the lock still lets them pick.
|
|
138
|
+
function lockedPrefs(behaviorPrefs, laws, ctx = {}) {
|
|
139
|
+
let prefs = behaviorPrefs && typeof behaviorPrefs === 'object' ? behaviorPrefs : {};
|
|
140
|
+
const report = [];
|
|
141
|
+
for (const lock of locksOf(laws)) {
|
|
142
|
+
const spec = settingSpec(lock.setting);
|
|
143
|
+
if (!spec) { report.push({ ...lock, enforced: false, reason: 'not_a_personal_setting' }); continue; }
|
|
144
|
+
const from = spec.read(behaviorPrefs || {}, ctx);
|
|
145
|
+
const target = lockTarget(spec, lock, spec.read(prefs, ctx));
|
|
146
|
+
if (!target.ok) { report.push({ ...lock, route: spec.route, enforced: false, reason: target.reason, from }); continue; }
|
|
147
|
+
prefs = spec.write(prefs, target.value);
|
|
148
|
+
report.push({ ...lock, route: spec.route, enforced: true, from, to: spec.read(prefs, ctx), allowed: allowedFor(spec, lock) });
|
|
149
|
+
}
|
|
150
|
+
return { prefs, locks: report };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// A PATCH, checked against the locks. `patch` maps setting → value (null clears,
|
|
154
|
+
// always allowed). Returns null when every key is allowed, else the first
|
|
155
|
+
// refusal: { setting, law_id, law_no, law_text, allowed }.
|
|
156
|
+
function checkPatch(patch, laws) {
|
|
157
|
+
const locks = locksOf(laws);
|
|
158
|
+
for (const [setting, raw] of Object.entries(patch || {})) {
|
|
159
|
+
if (raw === null || raw === undefined) continue;
|
|
160
|
+
const lock = locks.find((l) => l.setting === setting);
|
|
161
|
+
if (!lock) continue;
|
|
162
|
+
const spec = settingSpec(setting);
|
|
163
|
+
if (!spec) continue;
|
|
164
|
+
const v = spec.normalize(raw);
|
|
165
|
+
const target = lockTarget(spec, lock, v);
|
|
166
|
+
if (!target.ok) continue; // a lock naming no real value has nothing to enforce
|
|
167
|
+
if (target.value !== v) {
|
|
168
|
+
return { setting, law_id: lock.law_id, law_no: lock.law_no, law_text: lock.law_text, allowed: allowedFor(spec, lock) };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The /me slices with the laws applied. The EFFECTIVE values and the injected
|
|
175
|
+
// contracts come from the locked prefs; what the builder has STORED is still
|
|
176
|
+
// reported as theirs, so a surface can say "yours: X, the law: Y".
|
|
177
|
+
// ctx: { rank, instanceModels }
|
|
178
|
+
function describe(behaviorPrefs, laws, ctx = {}) {
|
|
179
|
+
const raw = behaviorPrefs && typeof behaviorPrefs === 'object' ? behaviorPrefs : {};
|
|
180
|
+
const { prefs, locks } = lockedPrefs(raw, laws, ctx);
|
|
181
|
+
const interaction = interactionPrefs.describeForApi(prefs);
|
|
182
|
+
interaction.stored = interactionPrefs.storedProfile(raw);
|
|
183
|
+
const wandering = wanderingPrefs.describeForApi(prefs, ctx.rank);
|
|
184
|
+
wandering.override_level = wanderingPrefs.overrideLevel(raw);
|
|
185
|
+
const modelAllocation = modelAllocPrefs.describeForApi(prefs, ctx.instanceModels || {});
|
|
186
|
+
modelAllocation.override = modelAllocPrefs.storedOverride(raw);
|
|
187
|
+
return { interaction, wandering, model_allocation: modelAllocation, locks };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The slice of a describe() a single settings route answers, with its locks.
|
|
191
|
+
function locksFor(route, locks) {
|
|
192
|
+
return (Array.isArray(locks) ? locks : []).filter((l) => l.route === route);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
settingSpec,
|
|
197
|
+
locksOf,
|
|
198
|
+
lockedPrefs,
|
|
199
|
+
checkPatch,
|
|
200
|
+
describe,
|
|
201
|
+
locksFor,
|
|
202
|
+
};
|
|
@@ -27,6 +27,7 @@ const eventSoundPrefs = require('../event-sound-prefs');
|
|
|
27
27
|
const wanderingPrefs = require('../wandering-prefs');
|
|
28
28
|
const interactionPrefs = require('../interaction-prefs');
|
|
29
29
|
const modelAllocPrefs = require('../model-alloc-prefs');
|
|
30
|
+
const lawLocks = require('../law-locks');
|
|
30
31
|
const renderPrefs = require('../render-prefs');
|
|
31
32
|
const profileNudgePrefs = require('../profile-nudge-prefs');
|
|
32
33
|
const displayName = require('../display-name');
|
|
@@ -214,6 +215,58 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
214
215
|
}
|
|
215
216
|
});
|
|
216
217
|
|
|
218
|
+
// ---- the project's LAWS on these settings (task 1004707) ----
|
|
219
|
+
//
|
|
220
|
+
// A law passed by the project's government may LOCK a personal setting (fix it,
|
|
221
|
+
// or cap it). The lock is enforced here and in GET /me, never only shown:
|
|
222
|
+
// * every read below resolves the EFFECTIVE value with the locks applied
|
|
223
|
+
// (law-locks.js), and reports them as `law_locks` — so a value stored before
|
|
224
|
+
// the law passed still resolves to what the law says;
|
|
225
|
+
// * every write refuses a value a lock does not allow (409 locked_by_law).
|
|
226
|
+
// The laws are read per request through the government PORT (this module never
|
|
227
|
+
// requires another); with the government module off there are no laws.
|
|
228
|
+
async function lawsInForce() {
|
|
229
|
+
const gov = api.resolveOptional('government');
|
|
230
|
+
if (!gov || typeof gov.lawsForSession !== 'function') return [];
|
|
231
|
+
const out = await gov.lawsForSession();
|
|
232
|
+
return out && Array.isArray(out.laws) ? out.laws : [];
|
|
233
|
+
}
|
|
234
|
+
// A READ fails soft: the laws unreadable → the builder's own settings, said so
|
|
235
|
+
// (laws_read:false), rather than a broken settings page. A WRITE fails closed
|
|
236
|
+
// (lawsAllowingWrite): it is never let through on a guess.
|
|
237
|
+
async function lawsForRead() {
|
|
238
|
+
try { return { laws: await lawsInForce(), read: true }; } catch (err) {
|
|
239
|
+
log.warn('[gds] laws unreadable for a settings read', err);
|
|
240
|
+
return { laws: [], read: false };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// `known`: the laws this request already read (a write's check), so one
|
|
244
|
+
// request reads the book once.
|
|
245
|
+
async function lockedSlice(slice, stored, rank, known) {
|
|
246
|
+
const { laws, read } = known ? { laws: known, read: true } : await lawsForRead();
|
|
247
|
+
const d = lawLocks.describe(stored, laws, { rank, instanceModels: instanceModels() });
|
|
248
|
+
return { ...d[slice], law_locks: lawLocks.locksFor(slice, d.locks), laws_read: read };
|
|
249
|
+
}
|
|
250
|
+
// The write's check. Answers the refusal and returns null when a law forbids
|
|
251
|
+
// `settingPatch` (setting → value, the names laws use) or the laws cannot be
|
|
252
|
+
// read; otherwise returns the laws it read, for the response to reuse.
|
|
253
|
+
async function lawsAllowingWrite(res, settingPatch) {
|
|
254
|
+
let laws;
|
|
255
|
+
try { laws = await lawsInForce(); } catch (err) {
|
|
256
|
+
log.error('[gds] laws unreadable for a settings write', err);
|
|
257
|
+
res.fail('laws_unreadable', { status: 503, message: 'The project\'s laws could not be read just now, so nothing was saved. Try again.' });
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const hit = lawLocks.checkPatch(settingPatch, laws);
|
|
261
|
+
if (!hit) return laws;
|
|
262
|
+
res.fail('locked_by_law', {
|
|
263
|
+
status: 409,
|
|
264
|
+
message: `Law ${hit.law_no} sets ${hit.setting} ("${hit.law_text}"); allowed: ${hit.allowed.join(', ')}`,
|
|
265
|
+
details: { setting: hit.setting, law_id: hit.law_id, law_no: hit.law_no, law: hit.law_text, allowed: hit.allowed },
|
|
266
|
+
});
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
217
270
|
// Per-builder "wandering" level (task 1268, migration 119). Governs how
|
|
218
271
|
// readily the agent opens a NEW THREAD of work (tangent / investigation /
|
|
219
272
|
// unsolicited suggestion) vs. staying on the claimed task — off-task only,
|
|
@@ -227,7 +280,7 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
227
280
|
try {
|
|
228
281
|
const builder = await api.getBuilderById(req.builder.id);
|
|
229
282
|
const stored = await db.getBuilderBehaviorPrefs(req.builder.id);
|
|
230
|
-
res.json(
|
|
283
|
+
res.json(await lockedSlice('wandering', stored, builder?.rank));
|
|
231
284
|
} catch (err) {
|
|
232
285
|
log.error('[gds] GET /me/wandering', err);
|
|
233
286
|
res.fail('read_failed', { status: 500, message: 'internal error' });
|
|
@@ -250,10 +303,12 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
250
303
|
if (!v.ok) {
|
|
251
304
|
return res.fail('invalid_patch', 422, { errors: v.errors });
|
|
252
305
|
}
|
|
306
|
+
const laws = await lawsAllowingWrite(res, v.patch);
|
|
307
|
+
if (!laws) return;
|
|
253
308
|
const existing = await db.getBuilderBehaviorPrefs(req.builder.id);
|
|
254
309
|
const next = wanderingPrefs.applyPatch(existing, v.patch);
|
|
255
310
|
const saved = await db.setBuilderBehaviorPrefs(req.builder.id, next);
|
|
256
|
-
res.json(
|
|
311
|
+
res.json(await lockedSlice('wandering', saved, builder?.rank, laws));
|
|
257
312
|
} catch (err) {
|
|
258
313
|
log.error('[gds] PATCH /me/wandering', err);
|
|
259
314
|
res.fail('write_failed', { status: 500, message: 'internal error' });
|
|
@@ -271,7 +326,7 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
271
326
|
router.get('/me/interaction', api.requireBuilder, async (req, res) => {
|
|
272
327
|
try {
|
|
273
328
|
const stored = await db.getBuilderBehaviorPrefs(req.builder.id);
|
|
274
|
-
res.json(
|
|
329
|
+
res.json(await lockedSlice('interaction', stored));
|
|
275
330
|
} catch (err) {
|
|
276
331
|
log.error('[gds] GET /me/interaction', err);
|
|
277
332
|
res.fail('read_failed', { status: 500, message: 'internal error' });
|
|
@@ -287,11 +342,13 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
287
342
|
if (!v.ok) {
|
|
288
343
|
return res.fail('invalid_patch', 422, { errors: v.errors });
|
|
289
344
|
}
|
|
345
|
+
const laws = await lawsAllowingWrite(res, v.patch);
|
|
346
|
+
if (!laws) return;
|
|
290
347
|
const existing = await db.getBuilderBehaviorPrefs(req.builder.id);
|
|
291
348
|
const nextInteraction = interactionPrefs.applyPatch(interactionPrefs.storedProfile(existing), v.patch);
|
|
292
349
|
const nextPrefs = { ...(existing || {}), [interactionPrefs.RESERVED_KEY]: nextInteraction };
|
|
293
350
|
const saved = await db.setBuilderBehaviorPrefs(req.builder.id, nextPrefs);
|
|
294
|
-
res.json(
|
|
351
|
+
res.json(await lockedSlice('interaction', saved, undefined, laws));
|
|
295
352
|
} catch (err) {
|
|
296
353
|
log.error('[gds] PATCH /me/interaction', err);
|
|
297
354
|
res.fail('write_failed', { status: 500, message: 'internal error' });
|
|
@@ -311,7 +368,7 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
311
368
|
router.get('/me/model-allocation', api.requireBuilder, async (req, res) => {
|
|
312
369
|
try {
|
|
313
370
|
const stored = await db.getBuilderBehaviorPrefs(req.builder.id);
|
|
314
|
-
res.json(
|
|
371
|
+
res.json(await lockedSlice('model_allocation', stored));
|
|
315
372
|
} catch (err) {
|
|
316
373
|
log.error('[gds] GET /me/model-allocation', err);
|
|
317
374
|
res.fail('read_failed', { status: 500, message: 'internal error' });
|
|
@@ -327,11 +384,14 @@ module.exports = function buildBuilderSettingsRouter() {
|
|
|
327
384
|
if (!v.ok) {
|
|
328
385
|
return res.fail('invalid_patch', 422, { errors: v.errors });
|
|
329
386
|
}
|
|
387
|
+
// a slot is locked by its law name, model.<slot>
|
|
388
|
+
const laws = await lawsAllowingWrite(res, Object.fromEntries(Object.entries(v.patch).map(([k, t]) => [`model.${k}`, t])));
|
|
389
|
+
if (!laws) return;
|
|
330
390
|
const existing = await db.getBuilderBehaviorPrefs(req.builder.id);
|
|
331
391
|
const nextOverride = modelAllocPrefs.applyPatch(modelAllocPrefs.storedOverride(existing), v.patch);
|
|
332
392
|
const nextPrefs = { ...(existing || {}), [modelAllocPrefs.RESERVED_KEY]: nextOverride };
|
|
333
393
|
const saved = await db.setBuilderBehaviorPrefs(req.builder.id, nextPrefs);
|
|
334
|
-
res.json(
|
|
394
|
+
res.json(await lockedSlice('model_allocation', saved, undefined, laws));
|
|
335
395
|
} catch (err) {
|
|
336
396
|
log.error('[gds] PATCH /me/model-allocation', err);
|
|
337
397
|
res.fail('write_failed', { status: 500, message: 'internal error' });
|
|
@@ -32,6 +32,7 @@ const interactionPrefs = require('./interaction-prefs');
|
|
|
32
32
|
const modelAllocPrefs = require('./model-alloc-prefs');
|
|
33
33
|
const renderPrefs = require('./render-prefs');
|
|
34
34
|
const builderNeeds = require('./builder-needs');
|
|
35
|
+
const lawLocks = require('./law-locks');
|
|
35
36
|
|
|
36
37
|
module.exports = {
|
|
37
38
|
// --- the raw behavior_prefs blob read (GET /me resolves this once, then shapes
|
|
@@ -45,6 +46,12 @@ module.exports = {
|
|
|
45
46
|
describeModelAllocation: (...a) => modelAllocPrefs.describeForApi(...a),
|
|
46
47
|
describeRender: (...a) => renderPrefs.describeForApi(...a),
|
|
47
48
|
computeNeeds: (...a) => builderNeeds.computeNeeds(...a),
|
|
49
|
+
// task 1004707: the interaction / wandering / model-allocation slices with the
|
|
50
|
+
// project's laws' locks APPLIED (law-locks.js), plus what each lock did:
|
|
51
|
+
// (behaviorPrefs, laws, { rank, instanceModels }) → { interaction, wandering,
|
|
52
|
+
// model_allocation, locks }. GET /me uses this so a session reads the locked
|
|
53
|
+
// values, not the builder's own.
|
|
54
|
+
describeWithLaws: (...a) => lawLocks.describe(...a),
|
|
48
55
|
|
|
49
56
|
// --- the per-builder model ceiling for a skill (cascade-dispatch clamps to it;
|
|
50
57
|
// never exceeds it). Mirrors the former `skillPrefs.getForBuilder` call.
|
|
@@ -21,15 +21,24 @@
|
|
|
21
21
|
// branch can NEVER return ship_news or ship_feed — a security fix must not
|
|
22
22
|
// advertise the shape of an exploit on a public/builder channel before it is
|
|
23
23
|
// live (the owner requirement behind task 990).
|
|
24
|
-
// - otherwise → the builder #ship-feed
|
|
24
|
+
// - otherwise → the builder #ship-feed, every ship; and, for a
|
|
25
|
+
// NEWS_KINDS task only, the owner-facing #ship-news too.
|
|
25
26
|
// #ship-feed / #security-ship-feed lines are PREFIXED with `#<taskId>` so the
|
|
26
27
|
// upvote bot can map a 👍 back to the task (discord-reaction.parseShipMarker).
|
|
27
28
|
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
29
|
+
// THE TWO CHANNELS ARE EDITORIALLY DIFFERENT (task 1002846, idea 1000458; the
|
|
30
|
+
// owner's pick 2026-10-12 — a kind filter, not a daily digest). #ship-feed is the
|
|
31
|
+
// builders' ledger: every ship, `#<id>`-prefixed. #ship-news is the owner-facing
|
|
32
|
+
// headline stream (CLAUDE.md §2: short, no jargon): only what a person using the
|
|
33
|
+
// project would notice — a `feature` or a `bug` fix — as the plain value summary,
|
|
34
|
+
// no id. Cleanup, infra, refactor, spike and the rest stay on #ship-feed. A task
|
|
35
|
+
// whose kind is missing or unknown posts no headline: a quiet #ship-news is the
|
|
36
|
+
// safe failure. The headline is the value summary verbatim, author-written like
|
|
37
|
+
// the #ship-feed line; webhook.js posts every channel with allowed_mentions
|
|
38
|
+
// {parse:[]}, so a summary can never ping anyone from either. This replaces task 1749's pause, which muted #ship-news entirely
|
|
39
|
+
// because it then repeated every #ship-feed line (×2 posters = 4 messages a ship);
|
|
40
|
+
// the one poster is now the server's (ship-finish.js defers), so a news ship is
|
|
41
|
+
// two messages, in two channels that say different things.
|
|
33
42
|
//
|
|
34
43
|
// A PAGE TWEAK (task 1004439): an approved Tweak Mode round ships as a task, and
|
|
35
44
|
// its line says what it is — a page approved, rewritten by its ARTIST (the round's
|
|
@@ -37,7 +46,8 @@
|
|
|
37
46
|
// `pageArtist` is the artist's name already resolved, or null when it could not be
|
|
38
47
|
// read; the line then says "page approved" and names nobody rather than the
|
|
39
48
|
// applier. Still `#<id>`-prefixed, so applause on it reaches the round.
|
|
40
|
-
|
|
49
|
+
const NEWS_KINDS = new Set(['feature', 'bug']);
|
|
50
|
+
function chooseBroadcastTargets({ securitySensitive, taskId, summary, kind = null, pageTweak = false, pageArtist = null }) {
|
|
41
51
|
if (!summary) return [];
|
|
42
52
|
if (securitySensitive) {
|
|
43
53
|
return [{ channel: 'security_ship_feed', line: `#${taskId} shipped (security) — ${summary}` }];
|
|
@@ -46,10 +56,9 @@ function chooseBroadcastTargets({ securitySensitive, taskId, summary, pageTweak
|
|
|
46
56
|
const by = pageArtist ? ` — rewritten by ${plainName(pageArtist)}` : '';
|
|
47
57
|
return [{ channel: 'ship_feed', line: `#${taskId} page approved${by}: ${summary}` }];
|
|
48
58
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
];
|
|
59
|
+
const targets = [{ channel: 'ship_feed', line: `#${taskId} shipped — ${summary}` }];
|
|
60
|
+
if (NEWS_KINDS.has(kind)) targets.push({ channel: 'ship_news', line: summary });
|
|
61
|
+
return targets;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
// The page-tweak source string, and the artist's display name for a shipped
|
|
@@ -89,7 +98,7 @@ async function postShipBroadcast(task, deps = {}) {
|
|
|
89
98
|
const log = deps.log || console.log;
|
|
90
99
|
const pageTweak = task.source === PAGE_TWEAK_SOURCE;
|
|
91
100
|
const pageArtist = pageTweak && sec === false ? await pageArtistName(task, deps) : null;
|
|
92
|
-
const targets = chooseBroadcastTargets({ securitySensitive: sec, taskId: task.id, summary, pageTweak, pageArtist });
|
|
101
|
+
const targets = chooseBroadcastTargets({ securitySensitive: sec, taskId: task.id, summary, kind: task.kind, pageTweak, pageArtist });
|
|
93
102
|
for (const { channel, line } of targets) {
|
|
94
103
|
try {
|
|
95
104
|
await postWebhook(channel, line);
|
|
@@ -155,4 +164,4 @@ async function postStrandNotice(info, deps = {}) {
|
|
|
155
164
|
return result;
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
module.exports = { chooseBroadcastTargets, postShipBroadcast, chooseStrandTargets, postStrandNotice };
|
|
167
|
+
module.exports = { chooseBroadcastTargets, NEWS_KINDS, postShipBroadcast, chooseStrandTargets, postStrandNotice };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/government/asks-db.js — where asks are stored and read (task 1004703).
|
|
4
|
+
//
|
|
5
|
+
// One row per docket matter in government_asks (government_028), keyed by the
|
|
6
|
+
// matter's docket id (`blockers:12`). saveAsk replaces a matter's ask whole (an
|
|
7
|
+
// agent rewriting its card after a refusal writes the new card, not a patch);
|
|
8
|
+
// asksFor reads the asks of many matters in one query, for the docket and the
|
|
9
|
+
// blockers list. The shape is checked in asks.js BEFORE anything reaches here,
|
|
10
|
+
// and the table's own CHECKs refuse a hand-written slop row too.
|
|
11
|
+
//
|
|
12
|
+
// The pool comes through the published doorway (ADR 0091 §2). No caching: an ask
|
|
13
|
+
// written a moment ago is in the next read.
|
|
14
|
+
|
|
15
|
+
const { pool } = require('../../src/module-api');
|
|
16
|
+
const asks = require('./asks');
|
|
17
|
+
|
|
18
|
+
const MAX_READ = 500;
|
|
19
|
+
|
|
20
|
+
// Store (or replace) one matter's ask. `ask` is checkAsk's stored form.
|
|
21
|
+
async function saveAsk({ matterId, ask, builderId }) {
|
|
22
|
+
if (!asks.isMatterId(matterId)) throw Object.assign(new Error('bad matter id'), { code: 'BAD_MATTER_ID' });
|
|
23
|
+
const { rows } = await pool.query(
|
|
24
|
+
`INSERT INTO government_asks
|
|
25
|
+
(matter_id, question, options, recommend_key, recommend_why, waits_on, if_nobody_answers, detail, written_by, written_at)
|
|
26
|
+
VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7, $8, $9, now())
|
|
27
|
+
ON CONFLICT (matter_id) DO UPDATE
|
|
28
|
+
SET question = EXCLUDED.question, options = EXCLUDED.options,
|
|
29
|
+
recommend_key = EXCLUDED.recommend_key, recommend_why = EXCLUDED.recommend_why,
|
|
30
|
+
waits_on = EXCLUDED.waits_on, if_nobody_answers = EXCLUDED.if_nobody_answers,
|
|
31
|
+
detail = EXCLUDED.detail, written_by = EXCLUDED.written_by, written_at = now()
|
|
32
|
+
RETURNING matter_id, question, options, recommend_key, recommend_why, waits_on, if_nobody_answers, detail, written_at`,
|
|
33
|
+
[matterId, ask.question, JSON.stringify(ask.options), ask.recommend.key, ask.recommend.why,
|
|
34
|
+
ask.waits_on, ask.if_nobody_answers, ask.detail || null, builderId == null ? null : builderId],
|
|
35
|
+
);
|
|
36
|
+
return asks.fromRow(rows[0]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The asks of these matters → Map(matter_id → ask). Ids that are not matter ids
|
|
40
|
+
// are skipped; an empty list reads nothing.
|
|
41
|
+
async function asksFor(matterIds) {
|
|
42
|
+
const ids = [...new Set((Array.isArray(matterIds) ? matterIds : []).filter(asks.isMatterId))].slice(0, MAX_READ);
|
|
43
|
+
const out = new Map();
|
|
44
|
+
if (!ids.length) return out;
|
|
45
|
+
const { rows } = await pool.query(
|
|
46
|
+
`SELECT matter_id, question, options, recommend_key, recommend_why, waits_on, if_nobody_answers, detail, written_at
|
|
47
|
+
FROM government_asks WHERE matter_id = ANY($1::text[])`,
|
|
48
|
+
[ids],
|
|
49
|
+
);
|
|
50
|
+
for (const r of rows) out.set(r.matter_id, asks.fromRow(r));
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { saveAsk, asksFor, MAX_READ };
|