@bongos/core 1.21.7 → 1.21.9

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.
Files changed (43) hide show
  1. package/.bongos-core.json +98 -38
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +8 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +52 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +182 -3
  10. package/docs/api-reference.md +4 -2
  11. package/docs/copy-inventory.md +20 -5
  12. package/docs/copy-registry.json +163 -16
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/page-inventory.json +33 -4
  15. package/docs/page-readings.json +44 -2
  16. package/modules/discord/board-broadcast.js +2 -0
  17. package/modules/government/board.js +82 -23
  18. package/modules/government/db.js +81 -6
  19. package/modules/government/decision-board.js +300 -0
  20. package/modules/government/decision-rules.js +312 -0
  21. package/modules/government/docket.js +49 -9
  22. package/modules/government/migrations/government_023_decision_rules.sql +190 -0
  23. package/modules/government/routes/government.js +115 -0
  24. package/modules/hall-ui/public/city-draw.js +11 -2
  25. package/modules/hall-ui/public/street-draw.js +585 -0
  26. package/modules/hall-ui/public/street.css +188 -0
  27. package/modules/hall-ui/public/street.html +118 -0
  28. package/modules/hall-ui/public/street.js +364 -0
  29. package/modules/hall-ui/public/street.states.json +43 -0
  30. package/modules/hall-ui/records/governor-street.md +14 -0
  31. package/package-lock.json +2 -2
  32. package/package.json +1 -1
  33. package/release-notes.json +16 -0
  34. package/src/bongos/route-rank-check.js +6 -0
  35. package/src/bongos/serve-internal.js +12 -0
  36. package/src/module-api.js +1 -1
  37. package/tests/government_abuse_matrix.mjs +3 -0
  38. package/tests/government_decision_rules.mjs +701 -0
  39. package/tests/government_routes.mjs +4 -0
  40. package/tests/government_seed.mjs +1 -0
  41. package/tests/hall_audit.mjs +9 -1
  42. package/tests/hall_page_gate_map.mjs +3 -0
  43. package/tests/hall_street.mjs +521 -0
@@ -122,6 +122,10 @@ const governmentPort = {
122
122
  // `charter` block. Opening a sitting decides nothing; the vote does.
123
123
  proposeCharter: board.proposeCharter,
124
124
  genesisStageStatus: board.genesisStageStatus,
125
+ // task 1004529 — who decides what. The table, resolved, and the check a
126
+ // building's own direct route runs before letting one builder act alone.
127
+ decisionRulesView: board.decisionRulesView,
128
+ checkActAlone: board.checkActAlone,
125
129
  };
126
130
 
127
131
  function registerSeams() {
@@ -225,6 +229,31 @@ module.exports = function buildGovernmentRouter() {
225
229
  }
226
230
  return rank;
227
231
  }
232
+ // WHO DECIDES AT PEOPLE (task 1004529). Giving or taking a custom rank is
233
+ // People's own act, and the project's decision-rules table says who may do it
234
+ // alone. As today (the default row) the route's own permission is the whole
235
+ // gate, unchanged. Once a passed amendment hands People to a sitting, one
236
+ // builder may no longer do it alone — the route refuses and names the matter
237
+ // route instead — unless the row's rule lets a seated member decide alone and
238
+ // this builder is seated. Server-side, per request, uncached (ADR 0016).
239
+ // Fail-CLOSED: an unreadable row refuses rather than waving the act through.
240
+ async function peopleMayActAlone(req, res) {
241
+ let out;
242
+ try {
243
+ out = await board.checkActAlone('people', req.builder.id);
244
+ } catch (err) {
245
+ log.error('[gds] people decision rule unreadable', err);
246
+ res.fail('decision_rule_unreadable', { status: 503, message: 'who decides at People could not be read just now' });
247
+ return false;
248
+ }
249
+ if (out.ok) return true;
250
+ res.fail('decided_by_sitting', 409, {
251
+ area: out.area, rule: out.rule, who: out.who,
252
+ what_this_means: out.what_this_means, open_with: out.open_with,
253
+ });
254
+ return false;
255
+ }
256
+
228
257
  const ARCHON = 'archon';
229
258
  const ARCHON_REASON = 'the Archon rank holds every permission and is immutable — it is the rank that can never be locked out of this tab (ADR 0172)';
230
259
  const ASSIGN_REASON = 'a seeded rank is assigned by rank (R95b sync); change the builder\'s rank instead';
@@ -447,6 +476,7 @@ module.exports = function buildGovernmentRouter() {
447
476
  try {
448
477
  const rank = await loadAssignableRank(res, rankKey);
449
478
  if (rank === null) return;
479
+ if (!(await peopleMayActAlone(req, res))) return;
450
480
  const assigned = await db.assignRank({
451
481
  builderId: req.body.builder_id,
452
482
  rankKey,
@@ -472,6 +502,7 @@ module.exports = function buildGovernmentRouter() {
472
502
  try {
473
503
  const rank = await loadAssignableRank(res, rankKey);
474
504
  if (rank === null) return;
505
+ if (!(await peopleMayActAlone(req, res))) return;
475
506
  const removed = await db.unassignRank({ builderId, rankKey });
476
507
  res.json({ ok: true, builder_id: builderId, rank_key: rankKey, removed });
477
508
  } catch (err) {
@@ -678,6 +709,90 @@ module.exports = function buildGovernmentRouter() {
678
709
  }
679
710
  });
680
711
 
712
+ // rank: perm board.item.open — propose a change to WHO DECIDES WHAT (task
713
+ // 1004529): one or more buildings' rows of the decision-rules table, filed as
714
+ // an amendment of kind 'decision_rules' and sat under the constitution IN
715
+ // FORCE, like every amendment. Nothing here changes the table — only the
716
+ // sitting passing does, inside its closing transaction. The same atom as
717
+ // proposing any amendment.
718
+ //
719
+ // Body: { rules: [{ area, decided_by, rule?, who? }], rationale_md? }
720
+ // area ∈ rules disputes people planning automation review release treasury
721
+ // (the charter is the board — change it with a constitution amendment)
722
+ // decided_by 'gate' (as today) | 'nobody' (vacant) | rank:<key> | rank:<key>+ | rank:a,b
723
+ // rule decide | first | consent | majority | unanimous | signoff (vacant with 'nobody';
724
+ // none with 'gate')
725
+ // who the seat in words, display only ("Mira under $50, the council above")
726
+ router.post('/government/board/decision-rules', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
727
+ if (validateOrRespond(req, res, {
728
+ rules: { required: true, type: 'array' },
729
+ rationale_md: { type: 'string', maxLength: 5000 },
730
+ })) return;
731
+ try {
732
+ const out = await board.proposeDecisionRules({
733
+ rules: req.body.rules,
734
+ rationaleMd: req.body.rationale_md ?? null,
735
+ proposedBy: req.builder.id,
736
+ });
737
+ if (!out.ok) {
738
+ if (out.reason === 'no_change') return res.fail('no_change', 409, { reason: 'every building named is already decided this way' });
739
+ return res.fail('invalid_decision_rules', 400, {
740
+ reason: out.reason,
741
+ ...(out.area ? { area: out.area } : {}),
742
+ ...(out.field ? { field: out.field } : {}),
743
+ ...(out.expected ? { expected: out.expected } : {}),
744
+ ...(out.rank_key ? { rank_key: out.rank_key } : {}),
745
+ });
746
+ }
747
+ res.status(201).json({ ok: true, amendment: out.amendment, item: out.item, rules: out.rules });
748
+ } catch (err) {
749
+ log.error('[gds] POST /government/board/decision-rules', err);
750
+ res.fail('propose_failed', { status: 500, message: 'internal error' });
751
+ }
752
+ });
753
+
754
+ // rank: perm board.item.open — put a MATTER to a building's sitting (task
755
+ // 1004529): the board's new subjects, one per building other than the
756
+ // charter. Refused unless the building's row names a sitting — under the
757
+ // default rows every building is decided as today and this route opens
758
+ // nothing. The sitting is snapshotted from the row (who and how).
759
+ //
760
+ // Body: { area, title, detail_md?, action? }
761
+ // action (People only): { type: 'assign_rank' | 'unassign_rank', builder_id, rank_key }
762
+ // — carried out when the sitting passes, under the same walls as the direct route.
763
+ router.post('/government/board/matters', api.requireBuilder, api.requirePermission('board.item.open'), async (req, res) => {
764
+ if (validateOrRespond(req, res, {
765
+ area: { required: true, type: 'string', maxLength: 32 },
766
+ title: { required: true, type: 'string', maxLength: 200 },
767
+ detail_md: { type: 'string', maxLength: 5000 },
768
+ action: { type: 'object' },
769
+ })) return;
770
+ try {
771
+ const out = await board.openMatter({
772
+ area: req.body.area,
773
+ title: req.body.title,
774
+ detailMd: req.body.detail_md ?? null,
775
+ action: req.body.action ?? null,
776
+ proposedBy: req.builder.id,
777
+ });
778
+ if (!out.ok) {
779
+ if (out.reason === 'decided_by_gate' || out.reason === 'no_one_holds_this') {
780
+ return res.fail(out.reason, 409, { area: out.area, ...(out.gate ? { gate: out.gate } : {}), what_this_means: out.what_this_means });
781
+ }
782
+ return res.fail('invalid_matter', 400, {
783
+ reason: out.reason,
784
+ ...(out.field ? { field: out.field } : {}),
785
+ ...(out.expected ? { expected: out.expected } : {}),
786
+ ...(out.rank_key ? { rank_key: out.rank_key } : {}),
787
+ });
788
+ }
789
+ res.status(201).json({ ok: true, matter: out.matter, item: out.item, decided: out.decided });
790
+ } catch (err) {
791
+ log.error('[gds] POST /government/board/matters', err);
792
+ res.fail('matter_failed', { status: 500, message: 'internal error' });
793
+ }
794
+ });
795
+
681
796
  // rank: perm board.vote.cast — cast a vote on an open board item (R08 of goal
682
797
  // 1000069, ADR 0175). The atom FLOORS AT XENOS deliberately (the R04 carry-
683
798
  // note): the permission is only the coarse gate, and the real wall is the
@@ -314,6 +314,15 @@
314
314
  return out;
315
315
  }
316
316
 
317
+ // Rules lights its band (the prototype's st.fixes, set when the small-fixes
318
+ // rule passed) when a standing rule is at work: the docket's "rules at work"
319
+ // reading is live and counts at least one. Shared with street-draw.js.
320
+ function rulesAtWork(docket) {
321
+ const rs = docket && Array.isArray(docket.readings) ? docket.readings : [];
322
+ const r = rs.find((x) => x && x.key === 'rules');
323
+ return !!r && r.status === 'live' && Number(r.value) > 0;
324
+ }
325
+
317
326
  function pinState(state) {
318
327
  return state === 'needs' || state === 'moving' || state === 'settled' ? state : 'moving';
319
328
  }
@@ -364,7 +373,7 @@
364
373
  const blds = [];
365
374
  CIVIC_SLOTS.forEach((s) => {
366
375
  const polys = [];
367
- isoInstitution(polys, k, s[0], s[1], s[2], { fixes: false });
376
+ isoInstitution(polys, k, s[0], s[1], s[2], { fixes: rulesAtWork(docket) });
368
377
  blds.push({ key: s[0], depth: s[1] + s[2] + 2, polys: polys });
369
378
  });
370
379
  districts.forEach((D) => {
@@ -429,7 +438,7 @@
429
438
  const api = {
430
439
  isoTheme: isoTheme, isoKit: isoKit, isoBox: isoBox, isoGable: isoGable, isoHip: isoHip, isoSaw: isoSaw,
431
440
  isoTree: isoTree, isoLight: isoLight, isoInstitution: isoInstitution, isoWork: isoWork, glyph: glyph, rnd: rnd,
432
- lotsFor: lotsFor, districtWork: districtWork, cityModel: cityModel,
441
+ lotsFor: lotsFor, districtWork: districtWork, cityModel: cityModel, rulesAtWork: rulesAtWork,
433
442
  CIVIC_SLOTS: CIVIC_SLOTS, ROOF: ROOF, BASE_LOTS: BASE_LOTS, MAX_DISTRICTS: MAX_DISTRICTS, MAX_PINS_PER_BUILDING: MAX_PINS_PER_BUILDING,
434
443
  FALLBACK_LINE: FALLBACK_LINE, STREET_HREF: STREET_HREF,
435
444
  };